Fall Moon

Fall Moon is a 2D block pusher puzzler, themed after the Chinese Moon festival. The player has control over a paper dragon, who’s body can be extended and retracted. The player must put the moon in place by pushing it and doing it in as few moves as possible to get the highest score.

Platform:
Engine:
Language:
Development Time:
Team Size:

Android
Unity 5
C#
3 weeks
3 Designers & 2 Artists

My Role

During the game project, I was the Main Scripter. My main responsibilities were:

A Post-Mortem of the project can be found at the bottom of the page.

Grid
Grid

The grid is a collection of imaginary points, sorted in a way to allow each point to have coordinates. This is called a 2D array. Using this it is easy for tiles to move on the grid, as they only have to take their own coordinates and change it slightly to get an adjacent point.

< CODE: Grid >

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(GridManager))]
[System.Serializable]
public class Grid : MonoBehaviour
{
    //Collections
    internal Node[] nodes;

    //References
    [SerializeField] [HideInInspector] private GridManager gridManager;


    public void InsertNode(Vector2 _worldPosition, Vector2 _coordinates)
    {
        SetGridManagerReference();

        //Insert node into array at 2D position
        int index = (int)_coordinates.y * (int)gridManager.nodeAmount.x + (int)_coordinates.x;
        nodes[index].coordinates = _coordinates;
        nodes[index].worldPosition = _worldPosition;
    }

    public Vector2 GetNodeWorldPosition(Vector2 _coordinates)
    {
        SetGridManagerReference();

        //Get node from array at 2D position
        int index = (int)_coordinates.y * (int)gridManager.nodeAmount.x + (int)_coordinates.x;
        return nodes[index].worldPosition;
    }

    public void UpdateGridSize()
    {
        SetGridManagerReference();

        //Make array 2 Dimensional
        nodes = new Node[(int)gridManager.nodeAmount.y * (int)gridManager.nodeAmount.x];
    }

    public void Clear()
    {
        nodes = null;
    }

    private void SetGridManagerReference()
    {
        if (gridManager == null)
        {
            gridManager = GetComponent<GridManager>();

            if (gridManager == null)
                Debug.Log("No GridManager present");
        }
    }

    public int Length()
    {
        return nodes.Length;
    }

    public struct Node
    {
        public Vector2 coordinates;
        public Vector2 worldPosition;
    }
}
Level Editor

Fall Moon being a single screen puzzle game meant it needed a lot of levels to function. One of my main priorities was to make a system in which our two level designers could create levels with ease.

To do this I made a level editor. The editor could easily change the size of the grid, and made it so you could press a button to align all tiles to the grid. These features allowed the level designers to quickly make and iterate levels. A lot of coordinate and location data was also set up here, making the game itself having to setup less when you play it.

< CODE: Grid Manager >< CODE: Tile Manager >

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Grid))]
public class GridManager : MonoBehaviour
{
    public Vector2 nodeAmount;
    [SerializeField] private Sprite nodeSprite;


    //References
    [HideInInspector] public Grid grid;


    public void CreateGrid()
    {
        ClearGrid();
        grid.UpdateGridSize();

        Vector2 bottomLeft = new Vector2(transform.position.x - (nodeAmount.x / 2), transform.position.y - (nodeAmount.y / 2));

        //Loop trough all nodes in the 2D array
        for (int x = 0; x < nodeAmount.x; x++)
        {
            for (int y = 0; y < nodeAmount.y; y++)
            {
                Vector2 position = new Vector2(bottomLeft.x + (x + 0.5f), bottomLeft.y + (y + 0.5f));
                Vector2 coordinates = new Vector2(x, y);

                grid.InsertNode(position, coordinates);
                CreateNodeSprite(position, coordinates);
            }
        }
    }

    public void ClearGrid()
    {
        SetGridReference();
        grid.Clear();

        List<GameObject> sprites = new List<GameObject>();

        for (int i = 0; i < transform.childCount; i++)
        {
            sprites.Add(transform.GetChild(i).gameObject);
        }

        foreach (GameObject sprite in sprites)
        {
            DestroyImmediate(sprite);
        }
    }

    private void CreateNodeSprite(Vector2 position, Vector2 coordinates)
    {
        GameObject nodeObj = new GameObject();
        nodeObj.transform.SetParent(transform);
        nodeObj.name = "Node [" + coordinates.x + " : " + coordinates.y + "]";
        SpriteRenderer rend = nodeObj.AddComponent<SpriteRenderer>();
        rend.sprite = nodeSprite;
        nodeObj.transform.position = new Vector3(position.x, position.y, transform.position.z);
    }

    private void SetGridReference()
    {
        if (grid == null)
        {
            grid = GetComponent<Grid>();

            if (grid == null)
                Debug.Log("No Grid present");
        }
    }
}

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Grid))]
[RequireComponent(typeof(GridManager))]
public class TileManager : MonoBehaviour
{
    [SerializeField] private LayerMask tileMask;
    [SerializeField] [Range(0.1f, 1f)]private float detectionRadius = 0.15f;


    //References
    internal GameObject tilesParent;
    [SerializeField] [HideInInspector] private Grid grid;
    [SerializeField] [HideInInspector] private GridManager gridManager;


    public Contact CheckCoordinateCollisions(Vector2 coordinates)
    {
        SetGridReference();
        SetGridManagerReference();

        Contact collision = new Contact();
        collision.tile = null;
        collision.boundary = false;

        if (coordinates.x < 0 || coordinates.x > gridManager.nodeAmount.x - 1 || coordinates.y < 0 || coordinates.y > gridManager.nodeAmount.y - 1)
        {
            collision.boundary = true;
            return collision;
        }
        Vector2 worldPosition = grid.GetNodeWorldPosition(coordinates);

        Collider2D tile = Physics2D.OverlapCircle(worldPosition, detectionRadius, tileMask);

        if(tile == null)
        {
            return collision;
        }
        else
        {
            collision.tile = tile.GetComponent<BasicTile>();
            return collision;
        }

    }

    public void MoveTile(Vector2 coordinates, BasicTile tile)
    {
        SetGridReference();

        tile.transform.position = grid.GetNodeWorldPosition(coordinates);
        tile.SetLocalCoordinates(coordinates);
    }

    public List<BasicTile> GetAllTiles()
    {
        SetGridReference();
        SetGridManagerReference();
        SetTileParent();

        List<BasicTile> tiles = new List<BasicTile>();

        for (int x = 0; x < gridManager.nodeAmount.x; x++)
        {
            for (int y = 0; y < gridManager.nodeAmount.y; y++)
            {
                Vector2 coordinates = new Vector2(x, y);

                BasicTile tile = CheckCoordinateCollisions(coordinates).tile;

                if (tile != null)
                {
                    tiles.Add(tile);
                }
            }
        }

        return tiles;
    }

    public void AlignTiles()
    {
        SetGridReference();
        SetGridManagerReference();
        SetTileParent();

        List<BasicTile> tiles = new List<BasicTile>();

        //Check each node for any overlapping tiles
        for (int x = 0; x < gridManager.nodeAmount.x; x++)
        {
            for (int y = 0; y < gridManager.nodeAmount.y; y++)
            {
                Vector2 coordinates = new Vector2(x, y);

                BasicTile tile = CheckCoordinateCollisions(coordinates).tile;
                
                if (tile != null)
                {
                    //If a tile is found, updates its local coordinates and position it correctly on the grid
                    tile.transform.position = grid.GetNodeWorldPosition(coordinates);
                    tile.SetLocalCoordinates(coordinates);
                    tile.transform.parent = tilesParent.transform;

                    tiles.Add(tile);
                }
            }
        }
    }

    public void ClearTiles()
    {
        SetTileParent();

        if (tilesParent != null)
        {
            if (tilesParent.transform.childCount > 0)
            {
                List<GameObject> children = new List<GameObject>();

                for (int i = 0; i < tilesParent.transform.childCount; i++)
                {
                    children.Add(tilesParent.transform.GetChild(i).gameObject);
                }

                foreach (GameObject child in children)
                {
                    DestroyImmediate(child);
                }
            }
        }
    }

    private void SetTileParent()
    {
        if (tilesParent == null)
        {
            GameObject parent = GameObject.Find("Tiles");

            if (parent == null)
            {
                parent = new GameObject("Tiles");
                tilesParent = parent;
            }
            else
            {
                tilesParent = parent;
            }
        }
    }

    private void SetGridReference()
    {
        if (grid == null)
        {
            grid = GetComponent<Grid>();

            if (grid == null)
                Debug.Log("No Grid present");
        }
    }

    private void SetGridManagerReference()
    {
        if (gridManager == null)
        {
            gridManager = GetComponent<GridManager>();

            if (gridManager == null)
                Debug.Log("No Grid Manager present");
        }
    }
}

public struct Contact
{
    public BasicTile tile;
    public bool boundary;
}
Tiles

< CODE: Basic Tile >

Basic Tile

The dragon was supposed to function the same way the entire game, so the puzzles would be constructed from the layout of blockers and the use of different types of tiles. As we weren’t entirely sure what kind of tiles we wanted, i made a Basic Tile script from which all tiles inherited from which could quickly be expanded upon.

Each tile posses the same functions, allowing them to move and push other tiles. It can also be set if they should fall or not and if they should be entirely stationary, to act like a blocker. This all made it easy to add new tiles with slightly different behaviour, and allowed for much easier implementation of special tiles, like the moon that should end the level when it reaches the goal.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(BoxCollider2D))]
[RequireComponent(typeof(ParticleSpawner))]
[RequireComponent(typeof(SpriteRenderer))]
public class BasicTile : MonoBehaviour
{
    public bool movable = false;
    public bool fall = false;
    public bool collide = true;
    public bool disappear = false;


    //Values
    protected float fallSpeed = 0.15f;
    private float currentFallDelay = 0;

    //Bools
    internal bool active = true;
    private bool currentFall = false;

    //Vectors
    internal Vector2 localCoordinates;
    internal Vector2 startCoordinates;

    //References
    protected TileManager tileManager;
    internal BoxCollider2D col;
    internal SpriteRenderer rend;
    internal LevelManager levelManager;
    internal ParticleSpawner particle;


    public virtual void Awake()
    {
        tileManager = FindObjectOfType<TileManager>();
        currentFallDelay = fallSpeed;
        startCoordinates = localCoordinates;
        col = GetComponent<BoxCollider2D>();
        rend = GetComponent<SpriteRenderer>();
        levelManager = FindObjectOfType<LevelManager>();
        particle = GetComponent<ParticleSpawner>();

        if (disappear)
        {
            levelManager.StartRestart += RestartDisappear;
            levelManager.EndRestart += Reappear;
        }
    }

    public virtual void FixedUpdate()
    {
        if (fall && !levelManager.paused && !levelManager.win && !levelManager.restarting)
        {
            Fall();
        }
    }

    public void Fall()
    {
        Vector2 newCoordinates = Direction(MoveDirections.down);

        Contact collision = tileManager.CheckCoordinateCollisions(newCoordinates);

        if (collision.boundary)
        {
            currentFallDelay = fallSpeed;

            if (currentFall)
            {
                levelManager.AdjustFallingTileAmount(false);
                currentFall = false;
            }

            Disappear();
            return;
        }
        if (collision.tile != null && collision.tile.collide)
        {
            currentFallDelay = fallSpeed;

            if (currentFall)
            {
                levelManager.AdjustFallingTileAmount(false);
                currentFall = false;
            }
        }
        else if (currentFallDelay == 0)
        {
            tileManager.MoveTile(newCoordinates, this);

            if (!currentFall)
            {
                levelManager.AdjustFallingTileAmount(true);
                currentFall = true;
            }

            currentFallDelay = fallSpeed;
        }
        else
        {
            currentFallDelay = Mathf.Clamp(currentFallDelay - Time.fixedDeltaTime, 0, fallSpeed);

            if (!currentFall)
            {
                levelManager.AdjustFallingTileAmount(true);
                currentFall = true;
            }
        }
    }

    public virtual void SetLocalCoordinates(Vector2 newCoordinates)
    {
        localCoordinates = newCoordinates;
    }

    public virtual bool Move(MoveDirections moveDirection)
    {
        Vector2 newCoordinates = Direction(moveDirection);

        Contact collision = tileManager.CheckCoordinateCollisions(newCoordinates);

        if (collision.boundary)
        {
            return (false);
        }

        BasicTile otherTile = collision.tile;

        if (otherTile == null || otherTile.collide == false)
        {
            tileManager.MoveTile(newCoordinates, this);
            return (true);
        }
        else if (otherTile.collide == true && otherTile.movable == true)
        {
            //If the other tile can be moved, call the same Move function in it
            if (otherTile.Move(moveDirection))
            {
                //If the other tile successfully moves, move this one and send the message on
                tileManager.MoveTile(newCoordinates, this);
                return (true);
            }
        }
        return (false);
    }

    private void RestartDisappear()
    {
        Disappear(true);
    }

    public virtual void Disappear(bool restart = false)
    {
        if (restart)
        {
            if(localCoordinates == startCoordinates)
            {
                return;
            }
        }
        else
        {
            if (active == true)
            {
                active = false;
                col.enabled = false;
                rend.enabled = false;

                particle.TriggerParticle(transform.position);
            }
        }
    }

    private void Reappear()
    {
        tileManager.MoveTile(startCoordinates, this);

        if (active == false)
        {
            active = true;
            col.enabled = true;
            rend.enabled = true;

            particle.TriggerParticle(transform.position);
        }
    }

    public Vector2 Direction(MoveDirections moveDirection)
    {
        switch (moveDirection)
        {
            case MoveDirections.left:
                return (new Vector2(localCoordinates.x - 1, localCoordinates.y));

            case MoveDirections.right:
                return (new Vector2(localCoordinates.x + 1, localCoordinates.y));

            case MoveDirections.up:
                return (new Vector2(localCoordinates.x, localCoordinates.y + 1));

            case MoveDirections.down:
                return (new Vector2(localCoordinates.x, localCoordinates.y - 1));
        }

        return localCoordinates;
    }

    public MoveDirections ReverseDirection(MoveDirections direction)
    {
        switch (direction)
        {
            case MoveDirections.left:
                return MoveDirections.right;

            case MoveDirections.right:
                return MoveDirections.left;

            case MoveDirections.up:
                return MoveDirections.down;

            case MoveDirections.down:
                return MoveDirections.up;
        }
         return MoveDirections.right;
    }

    public enum MoveDirections
    {
        left,
        right,
        up,
        down
    }
}
Dragon
Dragon Input

The dragon has two control methods to accommodate different playstyles. If you touch the dragon’s head directly, the dragon will try to follow your finger. This is a more accurate way of controlling the dragon but requires you to move your fingers a lot.

If you instead touch anywhere else on the grid the dragon will move with your finger, so if you swipe to the left, the dragon will move that much to the left. This allows the player to play more casually by simply moving their thumb, but with less accuracy.

Dragon Body

The dragon’s head inherits from the Basic Tile script and can be moved in any direction. It also spawns a body part behind it everytime it moves. All body parts are added to a list and if the player is trying to move into the last placed body part, the one behind the head, it will instead remove that tile.

The body parts keep track of what direction the previous one and the next body part is relative to it. It uses these directions to choose from a array of sprites, all with directions attached to them, which one is most suitable to use.

All body parts are also kept in a pool, as instantiating objects drains a phone’s battery power. By keeping them in a pool, the amount of times needed to instantiate a dragon part is massively decreased

< CODE: Dragon Input >< CODE: Dragon Head Tile >< CODE: Dragon Pooling >< CODE: Dragon Tile >

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DragonInput : TouchReceiver
{
    [SerializeField][Range(0.1f, 2f)]private float threshold = 0.75f;

    //References
    private TouchInput touchInput;
    private DragonHeadTile dragonHead;
    private LevelManager levelManager;

    //Bools
    private bool followInput;
    private bool anywhereTouch;

    //Values
    private int touchIndex;

    //Vectors
    private Vector2 oldTouchPos;
    private Vector2 touchDifference;


    private void Start()
    {
        touchInput = Camera.main.GetComponent<TouchInput>();

        //An event that triggers everytime the player touches empty space
        touchInput.startTouch += TouchAnywhere;

        dragonHead = FindObjectOfType<DragonHeadTile>();
        levelManager = FindObjectOfType<LevelManager>();
    }

    private void Update()
    {
        if (followInput)
        {
            Vector2 touchPos = touchInput.TouchPosition(touchIndex);

            if (!levelManager.IfFalling() && !levelManager.paused && !levelManager.win && !levelManager.restarting)
            {
                //If the touch was not on the dragons head
                if (anywhereTouch)
                {
                    //Detect how much the fingers move and move the Dragon that much

                    touchDifference.x += touchPos.x - oldTouchPos.x;
                    touchDifference.y += touchPos.y - oldTouchPos.y;

                    if (Mathf.Abs(touchDifference.x) >= Mathf.Abs(touchDifference.y))
                    {
                        if (touchDifference.x >= threshold)
                        {
                            touchDifference.x -= threshold;
                            dragonHead.MoveDragon(BasicTile.MoveDirections.right);
                        }
                        else if (touchDifference.x <= -threshold)
                        {
                            touchDifference.x += threshold;
                            dragonHead.MoveDragon(BasicTile.MoveDirections.left);
                        }
                    }
                    else
                    {
                        if (touchDifference.y >= threshold)
                        {
                            touchDifference.y -= threshold;
                            dragonHead.MoveDragon(BasicTile.MoveDirections.up);
                        }
                        else if (touchDifference.y <= -threshold)
                        {
                            touchDifference.y += threshold;
                            dragonHead.MoveDragon(BasicTile.MoveDirections.down);
                        }
                    }
                }
                else
                {
                    //If the finger is to far away, move it in that direction

                    float distanceX = touchPos.x - transform.position.x;
                    float distanceY = touchPos.y - transform.position.y;

                    if (Mathf.Abs(distanceX) >= Mathf.Abs(distanceY))
                    {
                        if (Mathf.Abs(distanceX) > threshold)
                        {
                            dragonHead.MoveDragon((Mathf.Sign(distanceX) == 1) ? BasicTile.MoveDirections.right : BasicTile.MoveDirections.left);
                        }
                    }
                    else
                    {
                        if (Mathf.Abs(distanceY) > threshold)
                        {
                            dragonHead.MoveDragon((Mathf.Sign(distanceY) == 1) ? BasicTile.MoveDirections.up : BasicTile.MoveDirections.down);
                        }
                    }
                }
            }
           
            oldTouchPos = touchPos;
        }
    }

    private void TouchAnywhere(int touch)
    {
        anywhereTouch = true;
        SetTouchFollow(touch);
    }

    public override void TouchBegin(int touch)
    {
        anywhereTouch = false;
        SetTouchFollow(touch);
    }

    private void SetTouchFollow(int touch)
    {
        if (!followInput)
        {
            touchInput.endTouch += TouchEnd;

            followInput = true;
            touchIndex = touch;
            touchDifference = Vector2.zero;
            oldTouchPos = touchInput.TouchPosition(touchIndex);
        }
    }

    public override void TouchEnd(int touch)
    {
        if(followInput == true && touchIndex == touch)
        {
            touchInput.endTouch -= TouchEnd;
            followInput = false;
        }
    }
}

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DragonHeadTile : BasicTile
{
    [Space]
    [SerializeField] private MoveDirections startDirection;
    [Space]
    [SerializeField] private Sprite up;
    [SerializeField] private Sprite down;
    [SerializeField] private Sprite forward;

    //Collections
    private List<DragonTile> bodyParts = new List<DragonTile>();

    //References
    private GameObject dragonParent;
    private DragonPooling dragonPool;

    [SerializeField] private AudioClip moveSound;
    private SoundPlayer theSP;

    //Values
    private int facingDirection;
    internal int moves;

    //Bools
    private bool retracting;


    public override void Awake()
    {
        base.Awake();
        dragonPool = GetComponent<DragonPooling>();
        dragonParent = new GameObject("Dragon");
        dragonParent.transform.SetParent(tileManager.transform);
        transform.SetParent(dragonParent.transform);
        theSP = GetComponent<SoundPlayer>();
    }

    private void Start()
    {
        MoveDirections reverseStart = ReverseDirection(startDirection);
        SetFacingDirection(reverseStart);
        SetDragonSprite(reverseStart);

        //Make Dragon retract when the "Restart" event triggers
        levelManager.StartRestart += StartRetract;
    }

    public void MoveDragon(MoveDirections direction)
    {
        if (retracting)
            return;

        Vector2 newCoordinates = Direction(direction);
        Vector2 oldCoordinates = localCoordinates;

        Contact collision = tileManager.CheckCoordinateCollisions(newCoordinates);

        if (collision.boundary)
        {
            return;
        }

        BasicTile otherTile = collision.tile;

        if ((otherTile == null || otherTile.collide == false))
        {
            tileManager.MoveTile(newCoordinates, this);

            AddDragonPart(oldCoordinates, direction);

            SetFacingDirection(direction);
            SetDragonSprite(direction);

            //Add 1 to the overall moves this turn
            moves += 1;
        }
        else if (bodyParts.Count > 0 && otherTile == bodyParts[bodyParts.Count - 1])
        {
            //If the other tile is the latest placed Dragon Body Part
            //Remove the body part and move the dragon head
            MoveBack();

            //Add 1 to the overall moves this turn
            moves += 1;
        }
        else if (otherTile.collide == true && otherTile.movable == true)
        {
            //If the other tile can be moved
            if (otherTile.Move(direction))
            {
                tileManager.MoveTile(newCoordinates, this);

                AddDragonPart(oldCoordinates, direction);

                SetFacingDirection(direction);
                SetDragonSprite(direction);

                //Add 1 to the overall moves this turn
                moves += 1;
            }
        }
    }

    private void MoveBack()
    {
        DragonTile dragonTile = bodyParts[bodyParts.Count - 1].GetComponent<DragonTile>();

        facingDirection = dragonTile.savedFacingDirection;
        SetDragonSprite(ReverseDirection(dragonTile.from));
        Vector2 dragonTileCoordinates = dragonTile.localCoordinates;
        RemoveDragonPart(dragonTile);

        tileManager.MoveTile(dragonTileCoordinates, this);
    }

    private void AddDragonPart(Vector2 coordinates, MoveDirections direction)
    {
        theSP.PlaySound(moveSound, 1f, (.7f + (0.03f*bodyParts.Count)));

        //Take Dragon Body Part from pool and place it on the right place on the grid
        DragonTile tile = dragonPool.RemoveFromPool();
        bodyParts.Add(tile);

        tileManager.MoveTile(coordinates, tile);
        tile.transform.SetParent(dragonParent.transform);

        SetDragonPartSprite(direction, tile);
    }

    private void SetDragonPartSprite(MoveDirections direction, DragonTile dragonTile)
    {
        if(bodyParts.Count == 1)
        {
            dragonTile.SetSprite(startDirection, direction, facingDirection);
        }
        else if (bodyParts.Count > 1)
        {
            dragonTile.SetSprite(ReverseDirection(bodyParts[bodyParts.Count- 2].to), direction, facingDirection);
        }
    }

    private void RemoveDragonPart(DragonTile part)
    {
        theSP.PlaySound(moveSound, 1f, (.7f + (0.03f * bodyParts.Count)));
        
        //Add the dragon tile back to the pool
        bodyParts.Remove(part);
        dragonPool.AddToPool(part);
    }

    private void SetFacingDirection(MoveDirections direction)
    {
        switch (direction)
        {
            case MoveDirections.right:
                facingDirection = 1;
                break;

            case MoveDirections.left:
                facingDirection = -1;
                break;
        }
    }
    
    private void SetDragonSprite(MoveDirections direction)
    {
        switch (direction)
        {
            case MoveDirections.up:
                rend.sprite = up;
                rend.flipX = (facingDirection == 1) ? true : false;
                break;

            case MoveDirections.down:
                rend.sprite = down;
                rend.flipX = (facingDirection == 1) ? true : false;
                break;

            case MoveDirections.left:
                rend.sprite = forward;
                rend.flipX = false;
                break;

            case MoveDirections.right:
                rend.sprite = forward;
                rend.flipX = true;
                break;
        }
    }

    private void StartRetract()
    {
        StartCoroutine(Retract());
    }

    public IEnumerator Retract()
    {
        retracting = true;
        moves = 0;

        //Retract until the entire body is retracted
        while (bodyParts.Count > 0)
        {
            MoveBack();
            yield return new WaitForSeconds(0.1f);
        }

        retracting = false;
        levelManager.FinishRestart();
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DragonPooling : MonoBehaviour {

    [SerializeField] private DragonTile bodyPart;


    //Collections
    internal Stack<DragonTile> pooledBodyParts = new Stack<DragonTile>();

    //Vectors
    private Vector2 pooledPosition = new Vector2(1000, 1000);

    //References
    private GameObject pooledParent;


    private void Start()
    {
        pooledParent = new GameObject("Pooled Objects");

        //Create a pool of Dragon Body Parts
        //Do this to prevent having to instanciate to many objects, saving mobile battery power
        CreateNewPool();
    }

    private void CreateNewPool()
    {
        while(pooledBodyParts.Count > 0)
        {
            DestroyImmediate(pooledBodyParts.Pop().gameObject);
        }

        for(int i = 0; i < 16; i++)
        {
            CreatePoolTile();
        }
    }

    private void CreatePoolTile()
    {
        DragonTile tile = Instantiate(bodyPart, pooledPosition, Quaternion.Euler(0, 0, 0));
        tile.name = "Dragon Tile";

        AddToPool(tile);
    }

    public void AddToPool(DragonTile tile)
    {
        tile.gameObject.SetActive(false);
        pooledBodyParts.Push(tile);

        tile.transform.position = pooledPosition;
        tile.transform.SetParent(pooledParent.transform);
    }

    public DragonTile RemoveFromPool()
    {
        DragonTile tile = pooledBodyParts.Pop();
        tile.gameObject.SetActive(true);

        //If pool is empty, add a new Dragon Body Part to the pool
        if(pooledBodyParts.Count == 0)
            CreatePoolTile();

        return tile;
    }
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))]
public class DragonTile : BasicTile
{
    [SerializeField] private SpriteStructure[] sprites;


    //Enums
    internal MoveDirections from;
    internal MoveDirections to;

    //Values
    internal int savedFacingDirection;


    public override void Awake()
    {
        base.Awake();
        rend = GetComponent<SpriteRenderer>();
    }

    public void SetSprite(MoveDirections from, MoveDirections to, int facingDirection)
    {
        savedFacingDirection = facingDirection;

        foreach (SpriteStructure spriteStruct in sprites)
        {
            if (spriteStruct.sprite != null && spriteStruct.directions.Length > 0)
            {
                foreach (Directions direction in spriteStruct.directions)
                {
                    if ((direction.a == from && direction.b == to))
                    {
                        rend.sprite = spriteStruct.sprite;

                        this.to = to;
                        this.from = from;
                    }
                }
            }
            else
            {
                continue;
            }
        }
    }

    [System.Serializable]
    public struct SpriteStructure
    {
        public Sprite sprite;
        public Directions[] directions;
    }

    [System.Serializable]
    public struct Directions
    {
        public MoveDirections a;
        public MoveDirections b;
    }
}
Post-Mortem

This project gave me a great chance to make tools and systems for the other designers to use, like the Level Editor. Making a game for a mobile device was also a fun and interesting challenge as it is something i have never done before, forcing me to quickly learn and adapt to something new. As we were also coming up with ideas as we went on, it made me create scripts that were easy to iterate and implement further upon.