Click here to Skip to main content
15,880,543 members
Articles / Mobile Apps / Windows Phone 7

UNITY 3D – Game Programming – Part 7

Rate me:
Please Sign up or sign in to vote.
4.90/5 (16 votes)
8 May 2015CPOL15 min read 47K   294   43   4
The seventh article in a series to discuss Unity 3D and how to get started with your own 3D projects.

Introduction

In part seven of the series, we will be expanding on our game idea from Part 5 and Part 6.

If you have not already done so, please take a moment and read:

  1. Unity 3D – Game Programming – Part 1
  2. Unity 3D – Game Programming – Part 2

  3. Unity 3D – Game Programming – Part 3

  4. Unity 3D – Game Programming – Part 4

  5. Unity 3D – Game Programming – Part 5

  6. Unity 3D – Game Programming – Part 6

  7. Unity 3D – Game Programming – Part 7

  8. Unity 3D – Game Programming – Part 8

  9. Unity 3D – Game Programming – Part 9

  10. Unity 3D – Game Programming – Part 10

Unity 3D Networking Article(s):

  1. Unity 3D - Network Game Programming

Unity 3D Leap Motion and Oculus Rift Article(s):

  1. Unity 3D - Leap Motion Integration

In the first part of the series we started by the very basics of the Unity 3D environment. Getting a feel of the IDE and the different sections which you will be working with throughout your project. We also covered how to use the tools in the designer to apply different transformation to a selected Game Object: positioning, rotation and scaling. We finally looked at how to create our first script and using the script apply a rotation transform on the Y-Axis of our cube.

In the second part of the series, we looked at more of the transformation of a given object through coding. We also looked at how to create light sources that are crucial for the rendering of your objects in the scene.

In the third part of the series we looked at how to process user input through the keyboard and based on the key code take particular actions.

In the fourth part of the series, we looked at creating a simple user interface. The user interface that we developed provided us a means to feedback to the user, and also another method for the user to input to our game or simulation.

In the fifth part, we started the idea of a simple game. We also looked at how to import 3D models into the game engine.

In the sixth part, we expanded on our game idea and making it more interesting and more complete. We introduced several concepts regarding game play and game design as well as created a simple user interface for our game.

In part seven, we will be answering some of the questions that got raised in Part 6.

Introduction to Game Programing: Using C# and Unity 3D (Paperback) or (eBook) is designed and developed to help individuals that are interested in the field of computer science and game programming. It is intended to illustrate the concepts and fundamentals of computer programming. It uses the design and development of simple games to illustrate and apply the concepts.
Image 1
Paperback
ISBN: 9780997148404
Edition: First Edition
Publisher: Noorcon Inc.
Language: English
Pages: 274
Binding: Perfect-bound Paperback (Full Color)
Dimensions (inches): 6 wide x 9 tall
Support independent publishing: Buy this book on Lulu.
Image 3
eBook (ePUB)
ISBN: 9780997148428
Edition: First Edition
Publisher: Noorcon Inc.
Language: English
Size: 9.98 MB
Support independent publishing: Buy this e-book on Lulu.
Available From:

Windows Phone 8.x Demo:

I have provided a free phone application that you can download and preview the demos on your Windows Phone. To download the mobile application, follow the link: CodeProjectArticleSample

Image 9
Code Project Articles Sample Mobile App

Live Preview of Article Code and Visuals:

Image 10

Link to live preview: http://www.noorcon.com/CodeProject/CodeProjectArticlePreview.html

Background

NOTE: For this particle, I will be using SketchUp to create some simple building blocks, which I will use to import into Unity! I am not a 3D Modeler or Designer, so please be patient and excuse the mess!

It is assumed that the reader of this article is familiar with programming concepts in general. It is also assumed that the reader has an understanding and experience of the C# language. It is also recommended that the reader of the article is familiar with Object-Oriented Programming and Design Concepts as well. We will be covering them briefly throughout the article as needed, but we will not get into the details as they are separate topics altogether. We also assume that you have a passion to learn 3D programming and have the basic theoretical concepts for 3D Graphics and Vector Math.

Lastly, the article uses Unity 3D version 4.6.1 which is the latest public release as of the initial publication date. Most of the topics discussed in the series will be compatible with older versions of the game engine, and perhaps also the new version which is supposed to be release sometime this year. There is however, one topics which is significantly different in the current 4.6.1 version compared to the older version of the game engine, and that is the UI (User Interface) pipeline. This is due to the new UI architecture in the engine which is far superior to what we had prior to this release. I for one, am very happy with the new UI architecture.

Using the code

Downloading the project/source code for article series: Download source.

With each consecutive article that is submitted, the project/source code will be also expanding. The new project files and source files will be inclusive of older parts in the series.

NOTE: To get the latest code, go to the most recent published part in the series and download the code.

Improving Our Design

Continuing on our work so far, let’s go ahead and take a look at some of the drawbacks that we have in our design so far.

One of the main drawbacks in the current design is the following; when a given game is completed, and the player is given the option to Play Again. If you have followed the structure and the logic in the previous articles, you might have noticed that there is a big flaw here. Our current design, does not reset our game for a new session correctly. In order to do so there are several design changes we would need to do so that we can handle this issue.

Since, we are creating our coins at design time, when we collect the coins at runtime, we are actually removing the coins from memory, and we have no other means to regenerate them for a new game session/play. This is a problem because, the current game logic ends the game based on two conditions:

  1. The game will end if the time is up.

  2. The game will end if the player has collected all coins before the time is up.

In the first scenario, assuming the time is up and the player has collected five out of the eight coins. When he or she selects the Play Again option, the game will start with a new set of timer, but the number of total coins to be collected will be the difference between the original game play and the new game play, therefore the new game session will start with three coins to be collected. And so forth.

In the second scenario, when the player collects all of the coins, the game ends. When the player select the Play Again option, based on the current implementation, the game will immediately end, because there are zero coins!

To solve this issue, we need to re-think and redesign our game logic:

  • We need to have a mechanism to create our coins dynamically.

Again, there are more than one design choices and approaches available to handle this criteria. We can either:

  1. Pre-define the locations of all of the coins available in the level as fixed.

  2. Or have them be randomly placed on the level.

From a coding perspective, option one would be simpler. You will pre-define the locations of the coins at design time, and use these transform data to dynamically instantiate coins in the same spot over and over again. Down side, is that the coins will always be in the same location for all game sessions.

Option number two will involve more planning and coding, because you will need to randomly place the coins within the level, but, you have to be careful that you place them with certain constraints, so that they do not be generated within a given wall and etc…

Let’s go ahead and implement option number two. This approach will make the game more random each time the player plays it, and also, implementing option number two, will cover the concepts for both options.

Randomizing Our Coins

First, we need to figure out our boundary for placing the coins in. In our level design, we know that our level is a 10 meter by 10 meter platform. We also know that the level has defined walls within the defined area.

If our level is placed at the origin (0,0,0), then our level cover the following corners in the world for the positive Z direction: (5,0,5) and (-5,0,5) and for the negative Z direction it will be (5,0,-5) and (-5,0,-5). These four points are our level boundaries as defined in the scene.

So whatever coins we generate have to be within these four coordinates in our space. However, we also have pre-defined walls in our level, and we need to take that into consideration as well. Again, this is not a hard task, because you as the designer of the level know exactly how you have subdivided your maze, you at least you should!

Image 11
Figure 1-Displaying Important Level Dimensions

In the figure above, notice that I have indicated the dimensions of the internal wall offsets. Using this information, you will be able to write code to place the coins according to the level design effectively. The easiest way to break it down, would be to dissect the internal area into regions. I would divide the area into four main regions:

Image 12
Figure 2-Highlighting Defined Regions

As you can see in Figure 2, I have labeled the four regions which are available for coin placement. The largest one is Region 1 which can be defined by the following points:

  • Region 1, Outer Bound: (4.5,0,4.5) (-4.5,0,4.5) (4.5,0,-4.5) (-4.5,0,-4.5)

  • Region 1, Inner Bound: (3,0,3) (-3,0,3) (3,0,-3) (-3,0,-3)

Region 2, 3 and 4 can be defined with the following points:

  • Region 2: (2,0,2) (-2,0,2) (2,0,1.5) (-2,0,1.5)

  • Region 3: (2,0,0.5) (-2,0,0.5) (2,0,-0.5) (-2,0,-0.5)

  • Region 4: (2,0,-2) (-2,0,-2) (2,0,-1.5) (-2,0,-1.5)

NOTE: Since the walls have a thickness of 0.5, you need to take that into consideration with your boundaries.

The next question, is how many coins per region we would like to generate. Looking at the regions that we have defined, region one is the largest, and therefore can hold the most coins. Let’s assign eight coins to Region 1. Region 2, 3 and 4 have the same square area, and are relatively smaller, therefore let’s assign two coins to each of the remaining regions.

Let’s now look at how our script will change based on the new rules. We will be updating our inputPlayer.cs script to the following:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class playerInput : MonoBehaviour {

...

 public GameObject coinPrefab; // variable used to store the coin prefab

...

 // Use this for initialization
 void Start () {

  #region COIN CREATION
  // we need to create the coins dynamically per region
  // Coins for Region 1
  for (int i=0; i<16; i++) {
   // Fill the Top part of Region 1
   if(i<4){
    GameObject coin = GameObject.Instantiate(this.coinPrefab,
                      new Vector3(Random.Range(-4.5f, 4.5f), 0.25f, Random.Range(3.0f, 4.5f)),
                      this.coinPrefab.transform.rotation) as GameObject;
    coin.name = "R1TopC"+i;
   }
   // Fill the Bottom part of Region 1
   if(i>3 && i<8){
    GameObject coin = GameObject.Instantiate(this.coinPrefab,
                      new Vector3(Random.Range(-4.5f, 4.5f), 0.25f, Random.Range(-4.5f, -3.0f)),
                      this.coinPrefab.transform.rotation) as GameObject;
    coin.name = "R1BottomC"+i;
   }
   // Fill the Left part of Region 1
   if(i>7 && i<12){
    GameObject coin = GameObject.Instantiate(this.coinPrefab,
                      new Vector3(Random.Range(-4.5f, -3.0f), 0.25f, Random.Range(-4.5f, 4.5f)),
                      this.coinPrefab.transform.rotation) as GameObject;
    coin.name = "R1LeftC"+i;
   }
   if(i>11 & i<16){
    GameObject coin = GameObject.Instantiate(this.coinPrefab,
                      new Vector3(Random.Range(3.0f, 4.5f), 0.25f, Random.Range(-4.5f, 4.5f)),
                      this.coinPrefab.transform.rotation) as GameObject;
    coin.name = "R1RightC"+i;
   }

  }

  // Coins for Region 2
  for (int i=0; i<4; i++) {
   GameObject coin = GameObject.Instantiate(this.coinPrefab,
                         new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(1.5f, 2.0f)),
                         this.coinPrefab.transform.rotation) as GameObject;
   coin.name = "R2C"+i;
  }

  // Coins for Region 3
  for (int i=0; i<4; i++) {
   GameObject coin = GameObject.Instantiate(this.coinPrefab,
                         new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(-0.5f, 0.5f)),
                         this.coinPrefab.transform.rotation) as GameObject;
   coin.name = "R3C"+i;
  }

  // Coins for Region 4
  for (int i=0; i<4; i++) {
   GameObject coin = GameObject.Instantiate(this.coinPrefab,
                         new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(-2.0f, -1.5f)),
                         this.coinPrefab.transform.rotation) as GameObject;
   coin.name = "R4C"+i;
  }
  #endregion

...

 }

...
}

We have introduced a new variable called coinPrefab as a GameObject, that is used to store the, you guessed it, coin prefab we already defined in our previous article. The variable is public so that you can drag and drop the prefab to assign it to the script.

The next major change is in our Start() function. We have defined a loop for each region. The loop for Region 2, 3 and 4 are pretty straight forward, but the loop for Region 1 is a little more complex. That is because our Region 1 itself technically is composed of 4 virtual regions: Top, Bottom, Left and Right.

For Regions 2, 3 and 4, we assign 4 coins per region respectively. For Region 1, we assign 16 coins, broken down to 4 coins per virtual region: Top, Bottom, Left and Right.

So there are a total of 12 + 16 = 28 coins in the game now that are randomly placed within the level. You can now remove the design time coins from your Hierarchy Window. After the removal of your design time coins, your level should look something like the following:

Image 13
Figure 3-Level without Design Time Coins

When you run the game, your level will be filled with the 28 randomly placed coins as defined in your code logic:

Image 14
Figure 4-Coins Dynamically Generated at Runtime

So at this point, we have solved our issue of creating and placing our coins randomly at runtime. It is all good and dandy, but, before you get too excited, there are few more scenarios and improvements that you need to do before you can really celebrate!

A True Reset

If you have come so far, before you move forward, go ahead and run the game. Play the game a few times, and pay attention to your design time environment. What do you notice?

Well, each time you create a new session, the coins from the previous session get carried over to the new session. Therefore you end up with more coins. This is not what we actually want in this case for our game play. When we rest our game, i.e., start a new session, we want it to start from scratch! Therefore, we need to do some cleanup before we start a new session.

So to handle this scenario, we need to somehow detect the existing coins object in the scene, and remove them from the scene before we start our new game session. To do so, we would need to update our script one more time to handle that situation. You can handle this in two places, either in the Start() function or the butPlayAgain_Click() function. I will be placing the logic to handle this scenario in the butPlayAgain_Click() function, and the listing will be modified as follows:

... 
 public void butPlayAgain_Click(){

  // get all object of type coin
  GameObject[] coins = GameObject.FindGameObjectsWithTag ("coin");

  // remove eahc object from the scene
  foreach (GameObject coin in coins){
   Destroy(coin);
  }

  Start ();
 }
...

This update will ensure that we always remove previously created coins from the scene before starting a new session.

The full listing will be as follows:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class playerInput : MonoBehaviour {

 public Text lblScore;   		// text UI element for displaying score
 public Text lblTimer;   		// text UI element for displaying timer

 public Canvas endGameCanvas; 		// Canvas holding UI elements for End of Game
 public Text lblEndOfGameScore;
 public Text lblEndOfGameTime;
 public Text lblEndOfGameCoinCont;

 public GameObject coinPrefab; 		// variable used to store the coin prefab

 private int score;    			// internal score variable

 public int SCORE
 {
  get{ return this.score; }
 }

 private float levelTime;   		// variable holing time to complete level
 private float timeLeft;    		// variable for the actual timer count down

 public bool END_GAME;    		// variable indicating end of game

 public int numOfCoinsInLevel;  	// will be initialized at the Start of the game
 public int numOfCoinsCollected;  	// will be incremented each time we collect a coin

 // Use this for initialization
 void Start () {

  #region COIN CREATION
  // we need to create the coins dynamically per region
  // Coins for Region 1
  for (int i=0; i<16; i++) {
   // Fill the Top part of Region 1
   if(i<4){
    GameObject coin = GameObject.Instantiate(this.coinPrefab,
                      new Vector3(Random.Range(-4.5f, 4.5f), 0.25f, Random.Range(3.0f, 4.5f)),
                      this.coinPrefab.transform.rotation) as GameObject;
    coin.name = "R1TopC"+i;
   }
   // Fill the Bottom part of Region 1
   if(i>3 && i<8){
    GameObject coin = GameObject.Instantiate(this.coinPrefab,
                      new Vector3(Random.Range(-4.5f, 4.5f), 0.25f, Random.Range(-4.5f, -3.0f)),
                      this.coinPrefab.transform.rotation) as GameObject;
    coin.name = "R1BottomC"+i;
   }
   // Fill the Left part of Region 1
   if(i>7 && i<12){
    GameObject coin = GameObject.Instantiate(this.coinPrefab,
                      new Vector3(Random.Range(-4.5f, -3.0f), 0.25f, Random.Range(-4.5f, 4.5f)),
                      this.coinPrefab.transform.rotation) as GameObject;
    coin.name = "R1LeftC"+i;
   }
   if(i>11 & i<16){
    GameObject coin = GameObject.Instantiate(this.coinPrefab,
                      new Vector3(Random.Range(3.0f, 4.5f), 0.25f, Random.Range(-4.5f, 4.5f)),
                      this.coinPrefab.transform.rotation) as GameObject;
    coin.name = "R1RightC"+i;
   }

  }

  // Coins for Region 2
  for (int i=0; i<4; i++) {
   GameObject coin = GameObject.Instantiate(this.coinPrefab,
                         new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(1.5f, 2.0f)),
                         this.coinPrefab.transform.rotation) as GameObject;
   coin.name = "R2C"+i;
  }

  // Coins for Region 3
  for (int i=0; i<4; i++) {
   GameObject coin = GameObject.Instantiate(this.coinPrefab,
                         new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(-0.5f, 0.5f)),
                         this.coinPrefab.transform.rotation) as GameObject;
   coin.name = "R3C"+i;
  }

  // Coins for Region 4
  for (int i=0; i<4; i++) {
   GameObject coin = GameObject.Instantiate(this.coinPrefab,
                         new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(-2.0f, -1.5f)),
                         this.coinPrefab.transform.rotation) as GameObject;
   coin.name = "R4C"+i;
  }
  #endregion

  this.score = 0;
  this.levelTime = Time.time + 30.00f;
  this.numOfCoinsCollected = 0;

  this.END_GAME = false;

  if (this.endGameCanvas != null) {
   this.endGameCanvas.gameObject.SetActive (false);
  }

  // check to make sure labels are defined before updating
  if (this.lblScore != null)
   this.lblScore.text = this.score.ToString();

  if (this.lblTimer != null)
   this.lblTimer.text = string.Format("{0:F2}", this.levelTime - Time.time);

  // get number of coins in the scene at the start of the game
  this.numOfCoinsInLevel = GameObject.FindGameObjectsWithTag ("coin").Length;

 }

 // Update is called once per frame
 void Update () {
  if (!this.END_GAME) {

   // compute time left
   this.timeLeft = this.levelTime - Time.time;

   // update UI label for timer
   if (this.lblTimer != null){
    this.lblTimer.text = string.Format("{0:F2}", this.timeLeft);
   }

   // check to see if we need to end the game based on the timer
   if(this.timeLeft<=0.00f || this.numOfCoinsInLevel<=this.numOfCoinsCollected){
    this.END_GAME = true;

    if (this.lblTimer != null && this.lblEndOfGameTime != null){

     if(this.timeLeft>=0.00f){
      this.lblTimer.text = string.Format("{0:F2}", this.timeLeft);
      this.lblEndOfGameTime.text = string.Format("{0:F2}", this.timeLeft);
     }else{
      // this else block is written to ensure that if the timer is up, we always get 0.00
      // and not positive or negative values, i.e. 0.01, or -0.01 and etc...
      this.lblTimer.text = string.Format("{0:F2}", 0.00f);
      this.lblEndOfGameTime.text = string.Format("{0:F2}", 0.00f);
     }
    }

    if(this.lblEndOfGameScore != null && this.lblEndOfGameCoinCont != null){
     this.lblEndOfGameScore.text = this.SCORE.ToString();
     this.lblEndOfGameCoinCont.text = this.numOfCoinsCollected.ToString();
    }

   }

   // code for the movement of player (CP) forward
   if(Input.GetKey(KeyCode.UpArrow)){
    this.transform.Translate(Vector3.forward * Time.deltaTime);
   }
   // code for the movement of player (CP) backward
   if(Input.GetKey(KeyCode.DownArrow)){
    this.transform.Translate(Vector3.back * Time.deltaTime);
   }
   // code for the movement of player (CP) left
   if(Input.GetKey(KeyCode.LeftArrow)){
    this.transform.Rotate(Vector3.up, -5);
   }
   // code for the movement of player (CP) right
   if(Input.GetKey(KeyCode.RightArrow)){
    this.transform.Rotate(Vector3.up, 5);
   } 
  }else{
   if(this.endGameCanvas != null){
    this.endGameCanvas.gameObject.SetActive(true);
   }
  }

 }

 // This event will be raised by object that have their Is Trigger attributed enabled.
 // In our case, the coin GameObject has Is Trigger set to true on its collider.
 void OnTriggerEnter(Collider c){
  if(c.tag.Equals("coin")){
   Coin coin = c.GetComponent<Coin>();

   // increase score
   this.score += coin.VALUE;
   this.numOfCoinsCollected += 1;

   // update score on the UI
   if (this.lblScore != null)
    this.lblScore.text = this.score.ToString();

   // remove the Coin object from the scene
   Destroy(c.gameObject);
  }
 }

 public void butPlayAgain_Click(){

  // get all object of type coin
  GameObject[] coins = GameObject.FindGameObjectsWithTag ("coin");

  // remove eahc object from the scene
  foreach (GameObject coin in coins){
   Destroy(coin);
  }

  Start ();
 }
}

Everybody Loves Gold Coins!

So now that we have addressed the dynamic creation of the coins and also the rest of the game, let’s now look at how to improve the look and feel of the game a little bit. The first thing I would like to do is to make my coins to look and feel like gold coins.

In order to achieve this, I will need to create a material that would have the texture of representing gold. Let’s see how we can achieve this.

The steps:

  • Create a Materials folder within the project, for organization and management purposes.

  • Within the folder, right-click and select Create->Material from the Context Menu. Name the new material object: gold_coin

  • Assign a texture representing golden surface, or choose a color to be applied to the material.

  • Assign the new material to the coin prefab!

NOTE: materials are used to assign textures and or colors to GameObjects.

In this case, since we already have a texture we using to represent our coins collected, we can use the same texture to apply to our material. If you apply the same texture, you will have something like the following:

Image 15
Figure 5-New Material Created

Once, you have the material defined, you will need to select the coin prefab listed under the prefabs folder. Then drag and drop the new material in the Inspector Window to assign the material to the coin prefab.

NOTE: This article does not cover texturing and etc… so the texture will not look pretty! Just demoing the concept.

Image 16
Figure 6-Coins with New Material

When you run the program, you will now see that the coins are now using the newly created material. Since, I am not a graphics designer, you can see that the coin does not look as good as it could. But nevertheless you should get the idea.

If you have done everything up to this point, you will have a game play experience that look like the following:

Image 17

Points of Interest

Our game is taking shape slowly. We have come a long way with defining and fine tuning our small little game thus far. As the series progresses, you notice that I spent less time explaining the basics and more time on the newer concepts.

There is still room to significantly improve our game. In our next part, we will look at how to create a start scree, be able to save and load data into our game, and perhaps even improve our game play by introducing more challenges for the player.

History

This is the seventh article of a series which I would slowly contribute to the Code Project community.

  1. Unity 3D – Game Programming – Part 1
  2. Unity 3D – Game Programming – Part 2

  3. Unity 3D – Game Programming – Part 3

  4. Unity 3D – Game Programming – Part 4

  5. Unity 3D – Game Programming – Part 5

  6. Unity 3D – Game Programming – Part 6

  7. Unity 3D – Game Programming – Part 7

  8. Unity 3D – Game Programming – Part 8

  9. Unity 3D – Game Programming – Part 9

  10. Unity 3D – Game Programming – Part 10

Unity 3D Networking Article(s):

  1. Unity 3D - Network Game Programming

Unity 3D Leap Motion and Oculus Rift Article(s):

  1. Unity 3D - Leap Motion Integration

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Noorcon Inc.
United States United States
Published Books:

Introduction to Game Programing: Using C# and Unity 3D designed and developed to help individuals that are interested in the field of computer science and game programming. It is intended to illustrate the concepts and fundamentals of computer programming. It uses the design and development of simple games to illustrate and apply the concepts.

Book Preview:
Preview all chapters

Available from:
Amazon.com
Barnes and Noble Book Store
Amazon Kindle (eBook)
iTunes - iBook (eBook)

Vahé Karamian
www.noorcon.com
www.facebook.com/NoorconInc

Comments and Discussions

 
GeneralExcellent!! Pin
Sunasara Imdadhusen26-Mar-15 19:30
professionalSunasara Imdadhusen26-Mar-15 19:30 
GeneralRe: Excellent!! Pin
Vahe Karamian29-Mar-15 8:43
Vahe Karamian29-Mar-15 8:43 
GeneralMy vote of 5 Pin
Isaac RF24-Mar-15 3:09
professionalIsaac RF24-Mar-15 3:09 
GeneralRe: My vote of 5 Pin
Vahe Karamian24-Mar-15 12:43
Vahe Karamian24-Mar-15 12:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.