Back

Code quality Challange

Meta data

General

NameTetris 3D
Typesingle person project
Completed2016

Contribution

Programming100%
Art100%
Desing100%

Technical

EngineUnity 5.4
LanguageC#

About the challenge

This was a school exercise where the goal was to create good quality and readable code. We started as a group (all second year developers) and created a C# class which included all code conventions. After that we all started to work on individual projects. I created Tetris 3D. It is Tetris with a third dimension added.

Convention.cs

Below you see the conventions file we created.

using UnityEngine;
using System.Collections;

//pascal case
namespace Aaron {
    //pascal case
    public class ThisIsAConvetionScript : MonoBehaviour {
        //no regions

        //const "variables"
        private const int JUMP_HEIGHT = 10;

        //static variables
        private static int jumpHeight = 10;
        //public variables
        public int myJumpHeight = 10;
        //protected variables
        protected int someJumpHeight = 10;
        //private variables
        private int secretJumpHeight = 10;


        //unity functions
            //awake
            //start
            //update
            //fixedupdate
            //lateupdate
        //same order for functions

        //camelCase
        //access specifiers are mandatory
        private int someVariable = 0;
        private int someOtherVariable = 1;

     // Use this for initialization
     private void Start() {
            ThisIsAFunction(5, 5.6f);
     }
    
        //access specifiers are mandatory
     // Update is called once per frame
     private void Update() {
            int x = 10;
            switch(x) {
                case JUMP_HEIGHT:
                    //do code
                    break;
            }
     }

        //example of parameter spacing
        private void ThisIsAFunction(int someInt, float someFloat) {

        }
    }
}
View code on Github

UML diagram

Brick

The first thing I decided was that a brick was a single brick/block m3. It is not a shape. This is because when the players has a Tetris (one row), we need to change the original shape. If the shape is built of different bricks. I can easily destroy bricks without having to worry about the shape.


View code on Github

BrickGrid

This game consists of two 3D grids. One world grid is fixed (static), and one grid moves (brick that fall). Because both grids works just a little bit different, it is necessary that they both store the bricks in a different way. To make sure other objects can talk with both worlds, grids and moveable grids, (without the need to program some sort of adapter or use as a statement) I've both linked them to the IBrickGrid interface. This interface provides methods needed to work with the grid: add, remove or get. How the grids execute these methods, is up to the class itself.

Requirements of the grids:

MovableGrid WorldGrid
Need to store 1 to 8 bricks Need to store 0 to 1000 bricks
Flexible size Fixed size
Looping though bricks need to be fast Need to know fast if there is a brick at position x
Adding bricks need to be fast

By looking at the requirements, I thought it would be good to store the bricks in a 3D array in WorldGrid. This has the great advantage that you can quickly find out if a block exists in a position (bricks [1, 2, 4]!= null). But it is slow to loop through. In a test it takes around 200ms to loop thought all of them because you are also looping thought the non-existing bricks.

The MovableGrid stores the bricks in a Dictionary where the key is the position/key of the brick. That’s why I don't need to loop over the non-existing bricks. But retrieving a position is slightly slower than an array.

Sounds Manager

SoundManager is a singleton. This because most object has sound, so it needs to be called a lot. When the constructor is called, this object loads all the sound files that are in the resource folder and puts them in a dictionary for fast accessing. This object is not bound to this project, and I can use this class for future projects. I only need to drag the sounds to "resources/sounds/.." and it works.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

namespace Sascha {
    public class SoundManager {

        private static SoundManager _instance = null;
        private Dictionary<string, AudioClip> sounds = new Dictionary<string, AudioClip>();
        private AudioSource audioSource;

        public static SoundManager instance {
            get {
                if(_instance == null)
                    _instance = new SoundManager();

                return _instance;
            }
        }

        public void PlaySound(string name) {
            if(sounds.ContainsKey(name)) {
                audioSource.PlayOneShot(sounds[name]);
            } else {
                Debug.LogError("Sound not found: " + name + "in folder 'Assets/Resources/sounds'");
            }
        }

        private SoundManager() {

            AudioClip[] audioClips = Resources.LoadAll<AudioClip>("sounds") as AudioClip[];
            for(int i = 0; i < audioClips.Length; i++) {
                sounds.Add(audioClips[i].name, audioClips[i]);
            }

            audioSource = new GameObject().AddComponent<AudioSource>();
        }
    }
}
View code on Github

Object Pool

In this game, many bricks need to be created and destroyed again (if the player is good). Because creating and destroying objects are heavy in Unity, I thought it would be smart to use an object pool here.

I have written the object pool as a rental. You can go there and ask for a particular kind of object. If it is not there, it will be created for you. When you're done with it, you need to return it. The object that has asked for it, is therefore responsible for returning it.

All objects with the IReusableObject interface can be used in the object pool. However, there must be a prefab of it in the resource folder so when it needs more objects, it can be created.

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using Sascha;

namespace Sascha {
    public class ObjectPool {

        private static ObjectPool _instance;

        private Vector3 hidePosition = new Vector3(-100, -100, -100);
        private Dictionary<IReusableObject, bool> brickList = new Dictionary<IReusableObject, bool>();
        
        public static ObjectPool Instance {
            get {
                if(_instance == null)
                    _instance = new ObjectPool();
                return _instance;
            }
        }

        /// <summary>
        ///  Returns a Script with a GameObject. When there are no more object available, it creates a new one from the Resources folder.
        /// </summary>
        /// <typeparam name="T">Type of the object</typeparam>
        /// <param name="prefabName">Name of the prefab</param>
        /// <returns></returns>
        public IReusableObject GetObject<T>(string prefabName) {

            foreach(KeyValuePair<IReusableObject, bool> reusableObject in brickList) {
                if(!reusableObject.Value) {
                    brickList[reusableObject.Key] = true;
                    return reusableObject.Key;
                }
            }

            IReusableObject newBrick = UnityEngine.Object.Instantiate(Resources.Load(prefabName) as GameObject).GetComponent<IReusableObject>();
            brickList.Add(newBrick, true);

            return newBrick;
        }

        public void ReturnObject(Brick brick) {
            brick.gameObject.transform.position = hidePosition;
            brickList[brick] = false;
        }


        private ObjectPool() {

        }
    }
}
View code on Github

IntVector3

This is a Vector3 but it works with integer instead of floats. Because this game exists completely out of grids, float positions does not exist. An Object can't be placed 1.5 on a grid, it needs to be on either 1 or 2. I could use Vector3, but that would mean I need to convert the values to integer every time I want to use it.

To ensure that the IntVector3 also could be used as a key in a dictionary (needed for the MovableGrid), I wrote my own GetHash method. This generated a hash based on the x, y, and z values of the vector. This allows you to compare two different IntVector3.

If I didn't do that, you get this problem


View code on Github

Game Manager

In the game, the sequence of actions is very important. Bricks move -> check if they hit something -> hit something? add to grid -> check if there is a Tetris and so on. The Game Manager takes care of the main loop and calls the objects in the correct sequences. It also provides the necessary information such as references to other objects.

The biggest advantage is that, you can easily pause the game or adjust the speed of the game. When you press the spacebar, the game loop is, for example 0. 4 seconds.

using UnityEngine;
using System.Collections;

namespace Sascha {
    public class GameManager : MonoBehaviour {

        const float GAME_SPEED = 0.5f;
        const float ADD_SPEED = 0.45f;
        const float SEQUENCES_TIME = 1.5f;

        public GameObject movableGridObj;
        public GameObject worldGridObj;
        public GameObject camera;
        public Hud hud;

        private WorldGrid worldGrid;
        private MovableGrid movableGrid;

        private void Start() {
            StartGame();

            StartCoroutine(MainUpdateLoop());
        }

        public void StartGame() {
            movableGrid = movableGridObj.GetComponent<MovableGrid>();
            worldGrid = worldGridObj.GetComponent<WorldGrid>();

            movableGrid.CreateRandomGroup();
            movableGrid.Init(worldGrid, camera);
            worldGrid.Init(hud);

        }

        public void GameOver() {
            hud.GameOver();
        }

        private IEnumerator MainUpdateLoop() {
            while(true) {

                movableGrid.Move(IntVector3.down);

                if(movableGrid.CheckColision()) {
                    worldGrid.Merge(movableGrid);

                    if(worldGrid.CheckForSequences()) {
                        yield return new WaitForSeconds(SEQUENCES_TIME);
                    }

                    if(!worldGrid.CheckIfGameOver()) {
                        movableGrid.CreateRandomGroup();
                    } else {
                        GameOver();
                        yield break;
                    }
                }

                yield return new WaitForSeconds(GAME_SPEED - (Input.GetAxis("speed up") * ADD_SPEED));
            }
        }

    }
}
View code on Github