Introduction
In this tutorial, you'll learn how to create a mobile 3D game using C# and Unity. The objective of the game is to throw the ball into the holes using the touch screen.
You will learn about the following aspects of Unity game development in this tutorial:
- importing 3D models
- swipe gesture controls
- class communication
- physics forces
- trigger colliders
1. Create a New Unity Project
Open Unity and select New Project from the File menu to open the new project dialog. Tell Unity where you want to save the project and set the Set up defaults for: menu to 3D.
2. Build Settings
In the next step, you're presented with Unity's user interface. Set the project up for mobile development by choosing Build Settings from the File menu and selecting your platform of choice.
3. Devices
The first thing we need to do after selecting the target platform is choosing the size of the artwork we'll be using in the game. This will help us select a proper size for the 3D textures and 2D GUI without making the artwork blurry or use textures that are too large for the target device. For example, the artwork needs to have a higher resolution if you're targeting an iPad with a retina display than a Lumia 520.
iOS
- iPad without Retina: 1024px x 768px
- iPad with Retina: 2048px x 1536px
- 3.5" iPhone/iPod Touch without Retina: 320px x 480px
- 3.5" iPhone/iPod with Retina: 960px x 640px
- 4" iPhone/iPod Touch: 1136px x 640px
Android
Because Android is an open platform, there's a wide range of devices, screen resolutions, and pixel densities. A few of the more common ones are listed below.
- Asus Nexus 7 Tablet: 800px x 1280px, 216 ppi
- Motorola Droid X: 854px x 480px, 228 ppi
- Samsung Galaxy SIII: 720px x 1280px, 306 ppi
Windows Phone & BlackBerry
- Blackberry Z10: 720px x 1280px, 355 ppi
- Nokia Lumia 520: 400px x 800px, 233 ppi
- Nokia Lumia 1520: 1080px x 1920px, 367 ppi
Note that the code we'll write in this tutorial can be used to target any of the platforms.
4. Export Graphics
Depending on the devices you're targeting, you may need to convert the artwork to the recommended size and pixel density. You can do this in your favorite image editor. I've used the Adjust Size... function under the Tools menu in OS X's Preview application.
5. Unity User Interface
Before we get started, make sure the 2D button in the Scene panel is not highlighted. You can also modify the resolution that's being displayed in the Game panel.
You're then presented with the workspace panels, which we'll also use in this tutorial. Take a moment to look at the main interface panels, such as the Scene, Game, Hierarchy, Project, Assets, and Inspector. We'll use them frequently in this tutorial.
6. Game Interface
The user interface of the game is straightforward. The screenshot below gives you an idea of the artwork we'll be using and how the final user interface will end up looking. You can find the artwork and additional resources in the tutorial's source files on GitHub.
7. Programming Language
You can use one of three programming languages when using Unity, C#, UnityScript, a variation of JavaScript, and Boo. Each programming language has its pros and cons, and it's up to you to decide which one you prefer. My personal preference goes to the C# programming language so that's the language I'll be using in this tutorial.
If you decide to use another programming language, then make sure to take a look at Unity's Script Reference for examples.
8. Sound Effects
I'll use a number of sounds to improve the audial experience of the game. The sound effects used in this tutorial were obtained from PlayOnLoop and Freesound.
9. 3D Models
To create the game, we first need to get a few 3D models. I recommend 3docean for high quality models and textures, but if you're testing or learning, then free models will work just as fine. The models in this tutorial were downloaded from SketchUp 3D Warehouse where you can find a wide variety of 3D models.
Because Unity doesn't recognize the SketchUp file format, we need to convert it to something Unity can import. We first need to download the free version of SketchUp, which is called SketchUp Make.
Open the 3D model in SketchUp Make, select Export > 3D Model from the File menu, and choose Collada (*.dae). Choose a name and location, and click Save. This will create a file and a folder for the 3D model. The file contains the data for the 3D object while the folder contains the model's textures. In the next step, we'll import the model into Unity.
10. Import Assets
Before we start coding, we need to add the assets to the Unity project. You can do this one of several ways:
- select Import New Asset from the Assets menu
- drag and drop the assets in the project window
- add the items to the project's assets folder
After completing this step, you should see the assets in your project's Assets folder in the Project panel.
11. Setup Camera
Before we continue, let's position the main camera to create the view we want. Select the main camera from the Hierarchy panel and adjust the Transform values in the Inspector to match the ones shown below.
Don't worry if you don't see any changes. We haven't created anything for the camera to see yet.
12. Adding Light
For our objects to be visible in the 3D world, we need to add light to the scene. Select Create Other from the GameObject menu and select Directional Light. This will create an object that produces a beam of light. Change its Transform values as shown in the following screenshot to make it illuminate the area.
The light should be visible on the scene as shown in the following screenshot.
13. Add Alley Bowlers
The alley bowlers are the main components of the game's scene. The player will use the touch screen to throw a ball, aiming for one of the holes.
The model used in this tutorial was downloaded and imported using the method described in Step 9.
Even though we'll add three models to the scene, the play will only interact with the one in the center. Drag and drop an instance of the model on the Scene or Hierarchy panel and change its Transform values to the ones shown in the screenshot below.
Use the same method to add the other two instances or duplicate the first instance by pressing Command-D. Use the Transform Tools to position them as shown below.
14. Create Colliders
With the main alley in place, it's time to add colliders to the model to help us move the ball across its surface. Since this is a complex model with lots of groups and polygons, it would take us a long time identifying the various shapes and adding a collider to each of them. To make this step easier, we'll use a third-party script to automatically create a collider that fits our model.
The script will add a new menu item named Wizards to the Unity menu. Select the model to which you want to add the collider and select Add Mesh Colliders from the Wizards menu. This will bring up the following window.
Click Add Colliders in the bottom right to add colliders to the model. This will create a Mesh Collider for every group or object of the model. You can verify this by expanding the ballbowler model in the Hierarchy and selecting an item.
15. Spotlights
We've added a light source to our 3D world, but we need a bit more light to make the scene more interesting. We do this by adding a number of spotlights.
Select Create Other from the GameObject menu and select Spotlight. This will create an object that produces a beam of light directed to one spot. The first spotlight we add needs to illuminate the holes. Change its Transform values as shown in the following screenshot.
Add a second spotlight using the following transform values.
16. Ball
The ball is the most important component of the game. The player will use the touch screen to try and get the ball in one of the holes.
The ball is going to be a simple Sphere primitive. Select Create Other > Sphere from the GameObject menu to create the primitive and modify the Transform values in the Inspector as shown below.
We'll convert the ball to a Prefab later as it will help us create instances of it in code. But first, let's add a RigidBody to it.
17. Ball RigidBody
To detect a collision with the ball, we need to attach a RigidBody to it. To add one, select Add Component from the Inspector panel, followed by Physics > RigidBody. You can leave the settings at their defaults.
18. Ball Sensors
We need to detect when the ball falls into a hole. We'll use Trigger Colliders for this purpose. A trigger collider is a physics object that detects a collision with another object without reacting physically. This will help us detect when the ball enters the hole without making it bounce back.
Since we don't need an actual model or 2D graphic to do this, we'll create an Empty GameObject. Select GameObject > Create Empty from the menu bar to create it. Click the Add Component button in the Inspector and choose Physics > Box Collider.
This will add a box collider to the game object. Repeat this process for every hole and make sure to check the Trigger checkbox.
Position the colliders as shown in the screenshot below.
19. Scoreboard
To display the game's scoreboard, we'll use Unity's GUI Textures. By default, images imported to the Assets folder are converted to Texture instances that can be applied to 3D objects. We need to change these Texture instances to GUI Texture instances for the images we want to use in the game's user interface.
Select the image you want to convert in the Assets panel and open the Inspector. Select GUI from the Texture Type menu.
You can now drag and drop the image to the Scene. The image will always appear in front of every object on the stage and will be treated as a 2D element.
20. Scoreboard Text
Inside the scoreboard GUI element, we'll display numbers indicating the player's score and the number of balls the player has left.
Select Create Other > GUI Text from the GameObject menu to create a text object, place it at the center of the GUI element, and change the text in the Hierarchy panel to 00. Follow the same process for the balls text to complete the scoreboard.
21. Adding Scripts
It's time to write some code. With the user interface in place, we can start writing the necessary code to add interaction to the game. We do this by means of scripts, which can be attached to game objects. Follow the next steps to learn how to add interaction to the level we've just created.
22. ThrowBall Class
Step 1: Declaring Variables
We'll start by creating the class that handles most of the game mechanics. Select the main camera, click the Add Component button in the Inspector panel, and choose New Script. Name the script ThrowBall
and don't forget to change the language to C#. Open the newly created file and add the following code snippet.
using UnityEngine; using System.Collections; public class ThrowBall : MonoBehaviour { private Vector3 throwSpeed = new Vector3(0, 0, 1100); public GameObject ballReference; private Vector2 startPos; private Vector2 endPos; private Vector2 minDistance = new Vector2(0, 100); private Vector3 ballPos = new Vector3(0, 0.38f, -11.41f); public bool ballOnStage = false; public int ballsLeft = 5; public GUIText ballsTextReference; public AudioClip throwSound;
We start by creating a number of variables that we'll use in the game. Let's take a look at each one.
-
throwSpeed
: the initial velocity of the ball when placed on the alley -
ballReference
: a reference to the ball prefab, set in the inspector -
startPos
: start position of the first touch, used to detect a swipe gesture -
endPos
: end position of the touch -
minDistance
: minimum movement needed for the finger to be considered a swipe -
ballPos
: initial ball position -
ballOnStage
:true
when a ball is currently on the alley, to prevent multiple shots at the same time -
ballsLeft
: the number of remaining balls -
ballsTextReference
: reference to the balls textfield, set in the inspector -
throwSound
: reference to the sound played when the ball is thrown, set in the inspector
Step 2: Detecting Touches
The following code snippet shows the beginning of the Update
method. This method detects if the user is touching the screen.
void Update() { if(Input.touchCount > 0) { Touch touch = Input.GetTouch(0);
We check the touchCount
property of the Input
class to get the current number of touches on the screen. If touchCount
is greater than 0
, we keep a reference to the first Touch
object. We'll use this object later.
Step 3: Detecting a Swipe
With a reference to the Touch
object, we have access to its phase
property, which helps us determine if the touch has started, moved, or ended.
Because we need a starting point to detect a swipe gesture, we need access to the starting point of the Touch
object, which we have access to when the Touch
object's phase
is equal to TouchPhase.Began
. The position of the Touch
object is stored in its position
property.
We have access to the endpoint of the gesture by accessing the Touch
object's position
property when its phase
property is equal to TouchPhase.Ended
.
if(touch.phase == TouchPhase.Began) { startPos = touch.position; } if(touch.phase == TouchPhase.Ended) { endPos = touch.position; } /* Compare positions */ if(endPos.y - startPos.y >= minDistance.y && !ballOnStage && ballsLeft > 0) {
Now that we have the coordinates of the start and endpoint of the touch, we can calculate the distance and check whether it's a valid swipe gesture by comparing it with the value stored in the minDistance
object.
We also inspect the value of ballOnStage
to check if a ball is already on the scene and we make sure that the player has enough balls left to continue playing. The number of balls left is stored in the ballsLeft
variable.
Step 4: Detecting the Screen Zone
Screen zones are created to calculate where the horizontal position of the ball is going to be when the ball is created. We do this to have control over the ball's position and it tells us where the ball is going to end and prevent throwing it out of bounds. We divide the screen in three parts as you can see below.
The math we use is quite simple as you can see in the code block below. We divide the screen in three equal parts and check which part contains the user's touch.
/* Divide screen in 3 parts */ /* Left */ if(touch.position.x >= 0 && touch.position.x <= Screen.width / 3) { ballPos.x = Random.Range(-0.87f, -0.35f); } /* Center */ else if(touch.position.x > Screen.width / 3 && touch.position.x <= (Screen.width / 3) * 2) { ballPos.x = Random.Range(-0.35f, 0.22f); } /* Right */ else if(touch.position.x > (Screen.width / 3) * 2 && touch.position.x <= Screen.width) { ballPos.x = Random.Range(0.22f, 0.36f); }
After detecting the correct screen zone, we calculate a random position between the start and end of the zone and assign that position to the ballPos
variable.
Step 5: Throwing the Ball
After determining the starting position of the ball, we create a new GameObject
instance using the Instantiate
method, passing in the ballReference
and ballPos
variables.
GameObject ball = Instantiate(ballReference, ballPos, transform.rotation) as GameObject; ball.rigidbody.AddForce(throwSpeed);
With the ball on the scene, we add a force to make it move through the correct alley. We do this by invoking the AddForce
method on the ball's rigidbody
property, passing in the throwSpeed
variable we declared earlier.
Step 6: Playing a Sound Effect
We play a sound effect when the ball is thrown in the alley. The next line of code takes care of that. For this tot work, remember to set the throwSound
variable in the Inspector.
AudioSource.PlayClipAtPoint(throwSound, transform.position);
Step 7: Resetting the Swipe Variables
After throwing the ball, we need to reset the variables that detect the player's touches. Not doing this would result in multiple swipes being detected. We also set the ballOnStage
variable to true
, preventing that another ball is thrown at the same time.
endPos = new Vector2(0, 0); startPos = new Vector2(0, 0); ballOnStage = true;
This completes the ThrowBall
class. The implementation of the class should look like th one shown below.
using UnityEngine; using System.Collections; public class ThrowBall : MonoBehaviour { private Vector3 throwSpeed = new Vector3(0, 0, 1100); public GameObject ballReference; private Vector2 startPos; private Vector2 endPos; private Vector2 minDistance = new Vector2(0, 100); private Vector3 ballPos = new Vector3(0, 0.38f, -11.41f); public bool ballOnStage = false; public int ballsLeft = 5; public GUIText ballsTextReference; public AudioClip throwSound; void Update() { if(Input.touchCount > 0) { Touch touch = Input.GetTouch(0); if(touch.phase == TouchPhase.Began) { startPos = touch.position; } if(touch.phase == TouchPhase.Ended) { endPos = touch.position; } /* Compare positions */ if(endPos.y - startPos.y >= minDistance.y && !ballOnStage && ballsLeft > 0) { /* Divide screen in 3 parts */ /* Left */ if(touch.position.x >= 0 && touch.position.x <= Screen.width / 3) { ballPos.x = Random.Range(-0.87f, -0.35f); } /* Center */ else if(touch.position.x > Screen.width / 3 && touch.position.x <= (Screen.width / 3) * 2) { ballPos.x = Random.Range(-0.35f, 0.22f); } /* Right */ else if(touch.position.x > (Screen.width / 3) * 2 && touch.position.x <= Screen.width) { ballPos.x = Random.Range(0.22f, 0.36f); } GameObject ball = Instantiate(ballReference, ballPos, transform.rotation) as GameObject; ball.rigidbody.AddForce(throwSpeed); AudioSource.PlayClipAtPoint(throwSound, transform.position); endPos = new Vector2(0, 0); startPos = new Vector2(0, 0); ballOnStage = true; } } } }
23. BallSensor Class
The following script contains the implementation of the BallSensor
class and is attached to the ball sensors, handling any collisions with the ball sensors.
Step 1: Declaring Variables
We start by declaring a variable named sensorValue
, which stores the number of points the ball sensor will add to the score. Because the ball sensors are all the same prefab, we declare this variable as public
. This will allow us to set this value in the editor using the Inspector.
using UnityEngine; using System.Collections; public class BallSensor : MonoBehaviour { public int sensorValue = 0; public GUIText scoreReference; public GameObject alertReference;
The other two variables store references to the score and alert game objects, which we also set in Unity's Inspector. These references will be used to increase the score and display the alert when the game ends.
Step 2: Destroying the Ball
The OnTriggerEnter
method detects when a collision has occurred and accepts the Collider
object the sensor is colliding with, the ball in this case. When a collision is detected, the ball is destroyed to prevent it from bouncing and potentially falling in another hole.
void OnTriggerEnter(Collider other) { Destroy(other.gameObject);
Step 3: Enabling Throw
After destroying the ball, we are sure that it isn't on the alley anymore. This means we can throw another ball without causing an unwanted behavior. The following line of code communicates with the ThrowBall
class and gets the ballOnStage
variable, setting it to false
.
Camera.main.GetComponent<ThrowBall>().ballOnStage = false;
Let's take a closer look at how we access a variable declared in another class. As you can tell by now, one way to access a variable or object declared outside of the current class is to set a reference variable using public
or [SerializeField]
, and then use the Inspector to give it a value.
The public
keyword plays an important role in this. Using it makes the variable not only accessible from the Inspector, but also through code. To do this, we first need access to the GameObject that has the Script Component attached to it, which is the Main Camera in this case.
There is only one camera in this game, which makes things easier. We can get a reference to the main camera through Camera.main
. Using GetComponent
we get the script attached to the camera, using the name of the class. We then have access to the public variable named ballOnStage
.
Step 4: Changing the Score
The following code block updates the score, using the reference we declared earlier.
scoreReference.text = (int.Parse(scoreReference.text) + sensorValue).ToString();
We convert the value stored in scoreReference
to an integer and add it to the value stored in sensorValue
. We convert the result to a string and assign it to the text
property of scoreReference
.
Step 5: Updating the Remaining Balls
In this step, we decrease the number of remaining balls. We use the method described in Step 3 to access the ballsLeft
and ballsTextReference
variables of the ThrowBall
class.
Camera.main.GetComponent<ThrowBall>().ballsLeft--; Camera.main.GetComponent<ThrowBall>().ballsTextReference.text = Camera.main.GetComponent<ThrowBall>().ballsLeft.ToString();
We decrement the value stored in ballsLeft
and update the text
property of ballsTextReference
.
Step 6: Checking for Game Over
The game is over when the player runs out of available shots. The next lines of code check if the ballsLeft
variable is equal to 0
and create an alert if true
. We then call the Invoke
method to invoke the Reload
method, which restarts the game by reloading the current scene.
The second parameter of the Invoke
method defines the delay with which the Reload
method will be invoked. We do this to give the player some time before we start a new game.
if (Camera.main.GetComponent<ThrowBall>().ballsLeft == 0) { Instantiate(alertReference, new Vector3(0.5f, 0.5f, 0), transform.rotation); Invoke("Reload", 3); } }
Step 7: Reloading the Game
This is the last part of the BallSensor
class. In the Reload
method, we call LoadLevel
on the Application
class and reload the current level, resetting every object and variable to its initial state.
void Reload() { Application.LoadLevel(Application.loadedLevel); } }
This is what the class looks like when finished.
using UnityEngine; using System.Collections; public class BallSensor : MonoBehaviour { public int sensorValue = 0; public GUIText scoreReference; public GameObject alertReference; void OnTriggerEnter(Collider other) { Destroy(other.gameObject); Camera.main.GetComponent<ThrowBall>().ballOnStage = false; scoreReference.text = (int.Parse(scoreReference.text) + sensorValue).ToString(); Camera.main.GetComponent<ThrowBall>().ballsLeft--; Camera.main.GetComponent<ThrowBall>().ballsTextReference.text = Camera.main.GetComponent<ThrowBall>().ballsLeft.ToString(); if (Camera.main.GetComponent<ThrowBall>().ballsLeft == 0) { Instantiate(alertReference, new Vector3(0.5f, 0.5f, 0), transform.rotation); Invoke("Reload", 3); } } void Reload() { Application.LoadLevel(Application.loadedLevel); } }
24. Testing
It's time to test the game. Press Command-P to play the game in Unity. If everything works as expected, then you're ready for the final steps.
25. Player Settings
When you're happy with your game, it's time to select Build Settings from the File menu and click the Player Settings button. This should bring up the Player Settings in the Inspector panel where you can set the parameters for your application.
These settings are application specific data that includes the creator or company, app resolution and display mode, rendering mode (CPU, GPU), device OS compatibility, etc. Configure the settings according to the devices you're targeting and the store or market where you plan to publish the app.
26. Icons and Splash Images
Using the graphics you created earlier, you can now create a nice icon and a splash image for your game. Unity shows you the required sizes, which depend on the platform you're building for.
27. Build and Play
Once your project is properly configured, it's time to revisit the Build Settings and click the Build Button. That's all it takes to build your game for testing and/or distribution.
Conclusion
In this tutorial, we've learned how to use physics forces, swipe gestures, class communication, and other aspects of game development in Unity. I encourage you to experiment with the result and customize the game to make it your own. I hope you liked this tutorial and found it helpful.
Comments