Resources... we just need one... Zoltoids, Zoltoids, Zoltoids! :D |
We will start to implement the most basic skeleton of the game. Since the game is about resource collecting and money, I decided to implement this first. Many things are still not definitive but there is already a lot to explain in what I did for this post.
Before we continue I want to make something clear. This is not a step by step tutorial targeted for people that don't know how to program or don't have basic knowledge of Unity. That doesn't mean that if you are that kind of person you should stop reading now. Please don't stop reading :)
The reason behind this decision is that there are already amazing tutorials for that and if I will be expending time on that the scope of the tutorial should be reduced. All in all, every time I will not explain something in detail I will try to add some reference to a tutorial that will explain that topic. If you find yourself lost, check this tutorials.
Let's Roll.
Here is the result of this tutorial, in this post we will talk about how to get here. From design decision to implementation. If you want you can download the full branch of this part of the tutorial in this repository.
What you can see in the demo above is how the game controls the resources in the game. Let's refresh a little bit the game mechanics from the previous post:
- Resources: there is technically one resource, money (Zoltoids). Money is obtained by mining and is spent on building and upgrading.
I want to base the challenge of the game in creating a balanced and sustainable economy with an strategy that reacts to the movements of your rivals. In order to do that, it is important that the player can make their own choices. This choices cannot be trivial.
- Game mechanics: the decisions should be always clear to the player:
- Beginning: players start with an small amount of money to build their basic economy.
- Mining: there are 4 different ore minerals in the game (red, green, yellow and blue). Each of them is worth the same at the beginning, but as you mine and sell always the same, the price will start to drop. Players also have to keep in mind what the rivals are mining to obtain maximum profit.
I made some changes that doesn't really matter at this point and are experiments. We have five resources instead of 4 and the price at the beginning is different. Still, the prices change as times passes. Selling a resource also makes it cheaper, which I think is a cool feature.
Tiled Map
The first think I want to talk about it how to make the tiled map. I believe that the way I have implemented the map is one of the simplest way to do so. In order to do so you will need some things:
- Tiled sprites.
- A generic "Tile" class that will be used by all the tiles in the game.
- A "TilePrefab" for each of the tiles in the game with the values already customised. (What is a prefab?).
- A "MapController" class that will take care of reading the parameters and put the tiles on the screen.
To properly work with tiled sprites, it is good that the size of the sprite matches 1 world unit, makes your life easier.
PixelsToUnits should be equal to the size of the tiled sprite, 1024. |
Let's discuss the "Tile" class a little bit. Here is part of the code:
// Generic squared piece of map public class Tile : MonoBehaviour { // If the tile should have a random resource on it public bool hasResource = false; // The actual resource on the tile (if any) [HideInInspector] public Resource resource = null; // Where the diferent sprites should be placed public SpriteRenderer tileModel; public SpriteRenderer resourceModel; // Initialization public void SetResource(Resource newResource){ resource = newResource; // Update models resourceModel.sprite = resource.MapIcon; resourceModel.color = resource.MapIconColor; } }
This is how it looks in the editor:
Specification can be done though code or the editor. Do it through the editor and prefabs whenever possible. |
What this class does is storing representation variables as sprites in order to allow easy expansion of the game. At the moment there are two tiles, one empty and another with a random resource. You can already notice a pattern that I will use in every prefab/GameObject that I create.
The parent GameObject stores the logic, and under it we have the children GameObject (Model) that takes care of representation (Sprite, animations). It is nice and actually compulsory if you want to animate your game, but we will get to that in a future post. Separating logic and representation should be done in programming in general always.
Now let's check the "Map" class. It is basically a random placer of tiles:
// Create and controlls all tiles in the map public class Map : MonoBehaviour { // Different tiles in the game // Adding the same tile more than once increases the chances of it appearing public Tile[] tilePrefabs; // How big the map should be public Vector2 mapSize = new Vector2(16, 10); void Start(){ GenerateRandomMap(); } // Creates a random symetrical map void GenerateRandomMap(){ Vector3 tilePos = transform.position - (Vector3)mapSize/(2.0f); for (int i = 0; i < mapSize.x/2; i++){ for (int j = 0; j < mapSize.y; j++){ int randomIndex = Random.Range(0, tilePrefabs.Length); // Create a new tile Tile newTile = (Tile)Instantiate(tilePrefabs[randomIndex]); newTile.transform.position = tilePos + new Vector3(i+0.5f, j+0.5f, 0); newTile.transform.parent = transform; if (newTile.hasResource){ // Set a random resource for the tiles that should have newTile.SetResource(resourcesController.resources[Random.Range(0, resourcesController.resources.Length)]); } // Mirror the tile in the other side of the map newTile = (Tile)Instantiate(newTile); newTile.transform.position = tilePos + new Vector3(mapSize.x-i-0.5f, j+0.5f, 0); newTile.transform.parent = transform; } } } }
This is how it looks in the editor:
The tile prefabs array allows repetition for easy customisation of probability of appearance. |
This is how it looks when you hit play:
Every tile has a collider for input detection purposes. |
The "Map" class doesn't have any mystery. The "GenerateRandomMap" can look a little bit intimidating. This is only because instantiating prefabs properly requires setting proper values for the transform and other components (What is Instantiate?). Also the method creates a symmetrical map that I think will be necessary for balance purposes of the game. Placing the dirt tile ten times in the array makes it ten times more likely to appear, simple :)
Resource Management
Resources are unique in our game. That means that we have one instance of each of them at all times. We do it like this because we need to track the price for each of the resources and because we cannot really store resources since they are sold automatically. This is the structure of this part of the project:
- A "ResourcesController" GameObject that stores all the resources in the game and allows for customisation and single entry point for other scripts.
- Our "Tile" classes has to reference now to the particular resource they hold (if any).
- A "Resource" class for each of the resources in the game that can be extracted and sold. Keeps track of the "SellPrice" and updates it.
Again, specification is done through the inspector making future tweaks much faster. |
Here we have the "ResourceController":
// Controller for the resources in the game public class ResourcesController : MonoBehaviour { // List of all resources public Resource[] resources; // Frecuency of update of prices public float updatePriceTime = 5.0f; // Start coroutines void Start(){ StartCoroutine(UpdatePrices()); } // Infinite coroutine that update prices of every resource public IEnumerator UpdatePrices(){ // Repeat forever while (1==1){ yield return new WaitForSeconds(updatePriceTime); foreach(Resource resource in resources){ resource.UpdatePrice(); } } } }
This class is responsible of notifying the particular resources when they need to update their "SellPrice". It is important to understand that we are using a Coroutine here and why (What is a Coroutine?). Coroutines work basically like the "Update" method of every MonoBehaviour. Of course, you need to get used to the syntax and write a little bit more code but what you get in return is a very simple way of defining behaviours in you classes.
This is perfect example of use of coroutines, because you do not want to update the price every frame and thanks to the coroutines you can specify the time between updates and even stop and resume the updates.
Here is part of the code of the "Resource":
// Takes care of controlling the prices changes and selling of a particular resource public class Resource : MonoBehaviour { // The amount of money you get selling the resource public float sellPrice = 100; protected float previousSellPrice; // Min and max random increase values of the price public float priceIncrementMin = 0.0f; public float priceIncrementMax = 0.1f; // Amount of decrement of the price when selling public float priceDecrement = 0.02f; // Representation values asigned in the inspector public Sprite MapIcon; public Color MapIconColor; // Initialization void Awake(){ previousSellPrice = sellPrice; } // Called by the resource manager public void UpdatePrice(){ sellPrice += sellPrice * Random.Range(priceIncrementMin, priceIncrementMax); } public float Sell(){ previousSellPrice = sellPrice; sellPrice -= sellPrice * priceDecrement; return previousSellPrice; } public float GetSellPriceChange(){ return (sellPrice - previousSellPrice); } }
This class is the core of the tutorial of today. We keep the resources unique and every 5 seconds we update the "SellPrice". The increment in the price is random between two perceptual values. For example if we have a resource like the default in the code, if the price is 100, on the next upgrade it will be some random number between 100 and 110. If someone sell this resource the price will go down by 2. If the price is 1000 instead 100 all those values will increase accordingly to avoid situations where changes doesn't have lot of meaning.
In the future we will need to improve this methods to clamp a minimum and maxim price for resources. Along othe polishing details, probably in the part 8 of the tutorial.
Selling a resource returns the price and modifies it. We also keep that of the previous price in order to know if the prices of a particular resource are in general going up or down (necesary for the GUI).
This class is designed in a way that allows a lot of tweaking that will be necessary to balance the game. Of course, I can already imagine things we could add, like mechanism that will change the variation of the prices as the game progresses (prices increase more at the end of the game to avoid long games).
Simple Buildings
In order to test the resources collecting and selling we need some simple way of placing buildings. This is how we tackle this problem:
- We have an abstract "Building" class that will be the parent of all the other buildings
- We have a child "BuildingExtractor" class that will take care of the particular behaviour of the building. Here we cannot apply the specification in the editor because the behaviour of each building in the game will have little in common, so we need a class for each of them
- We have a "Extractor" prefab that will be instantiated when the building is created. This will be done following the same idea like a tile, so I will not explain it again.
This is how we create the buildings at this point of the tutorial. On the next part we will improve the Input and Building classes.
In the "'Tile" class we add a collider and and "OnMouseDown" method that will capture mouse and touch events (this is the fastest method of mouse detection):
// Generic squared piece of map public class Tile : MonoBehaviour { // if there is a building on the tile [HideInInspector] public bool hasBuilding = false; // Prefab of the only building that can be placed in this tile. TODO public Building buildingPrefab; // The actual building on the tile (if any) [HideInInspector] public Building building = null; // Simple building and destoying mechanism. TODO void OnMouseDown(){ // If there is no building and player has money if ((buildingPrefab != null) && (building == null) && (GameObject.FindObjectOfType().money >= buildingPrefab.price)){ building = (Building)Instantiate(buildingPrefab); building.SetTile(this); building.transform.parent = transform; building.transform.position = transform.position; // Player pays the price of the building GameObject.FindObjectOfType ().addMoney(-building.price); } else // If there is a building destroy it if ((buildingPrefab != null) && (building != null)){ Destroy(building.gameObject); } } }
Now every tile has a bool value saying if there can be a building on it a reference to the prefab that can be built there. You can already see the power of prefabs in this project since they make code generated content very easy.
The "OnMouseDown" method is called automatically thanks to the "Monobehaviour" messages (Complete List of messages) and it instantiate the building and makes the "Player" pay the price. The "Player" class and everything next in this tutorial is temporal, that's why there are "TODO" comments in the code.
The "Player" class:
// Takes care of player global variables public class Player : MonoBehaviour { // The number of the player. TODO public int number = 1; // The account money balance public float money = 300.0f; // Add money to the account. Negative values decrease the balance. public void addMoney(float value){ money += value; } }
Now we just have to take a look to the Building classes:
// Interface for all the types of buildings public abstract class Building : MonoBehaviour { // Cost in Zoltoids public float price; // Level of upgrade (1 or 2) public int upgradeLevel = 1; // Tile where the building is placed protected Tile tile; // Should be called when the building is instantiated public abstract void SetTile(Tile tile); }
And the specific class:
// Building that is placed on top of a resource // Automatically extracts and sells it. public class BuildingExtractor : Building { // Frecuency of resources extraction per upgrade level public float[] extractionTimers; // Resource that the building is extracting protected Resource resource; // Call coroutines void Start(){ StartCoroutine(Extract()); } // Infinite coroutine for resource extraction and selling public IEnumerator Extract(){ // Repeat forever while (1==1){ // Wait depending on the upgrade level yield return new WaitForSeconds(extractionTimers[upgradeLevel]); // Sell the reosurce and increase player money balance. TODO GameObject.FindObjectOfType().addMoney(resource.Sell()); } } }
We use the same Coroutine system than in "ResourcesController" that allows us to easily control the frequency of update. The array of "extractionTimers" is a simple way of exposing the variables that will need to be balanced. Each position in the array represents a value for the same level of building upgrade.
Resources are updated in real time and buildings modify "Money" and "Price" labels. |
This is all for today! We made it to the end... but this is just the beginning of it! I hope you are as excited as I am. The hardest parts of a project are always the beginning and the end. I am happy one of them already passed.
This blog post has a lot of what is necessary to actually make the unity project work, but I omitted many thinks like the GUI part. It's on the repository, you can check it and eventually understand it since it is commented like the rest of the code. I will talk about it in detail in the part 7 of the tutorial, until then, you do not really need to know how it works.
To sum up, we created a random Map, populated it with resources and gave the player the prossibility to place buildings and extract resources. The most insteresting part is seeing how the prices changes and the player modifies them.
As usual I want to know what do you think? :) Please leave me your comments here:
If you found something you do not undestand, please comment in this post and I will do my best to explain it to you and will update the post.
Here is once more the link to the repository.
On the next post: Controls. Mouse and touch input, plus new Buildings.
Good night and good luck :)
Enjoyed reading the article to the core! Awaiting more posts in this blog!
ReplyDeleteThanks a lot! :)
DeleteWhere is the rest of the tutorial :(
ReplyDeleteHello Joseph!
DeleteSince I started to run my own company I haven't found time to continue with this tutorial... But I keep it in mind! I would love to share more stuff with you guys! :D
Do you have any suggestion? I would love to hear it! ^^