Colorless

Game Description:

In Colorless, an isometric 2D puzzle game, players utilize movement and teleportation to guide a colorless woman to the paint palettes that will restore her colors. The game combines challenging puzzles with breathtaking art to provide a thoughtful, beautiful gameplay experience.

Development Specification:

  • Role: Gameplay Programmer
  • Engine: Unity 2017.1.0p4
  • Development Time: 2 Months
  • Team Size: 4
  • Number of Programmers: 1

Responsibilities

  • Built a level generator tool for designers to layout tiles into an isometric level using data from XML files.
  • Wrote all the gameplay code. Implemented isometric tile-based movement, movable tile highlighting, portals and other interactable objects.
  • Designed and implemented tutorials.
  • Created the trailer for the game.

Level Generator

Designing levels in isometric view would lead to long iterations and thus is not an effective strategy to implement when working on a short-term project. To alleviate this issue, I wrote a tool to help the level designers generate levels from XML data. The tool takes the necessary prefabs and xml data as input and generates the layout in isometric space using transformations as necessary. In this way the level designers can convert a paper sketch to a level in unity in just 15 minutes (including time to write xml files). The tool itself is able to be run in editor, so that further passes can be done in the level using the editor. 
This enabled:

  • Faster level iterations.
  • Easier level visualizations.
  • More time to focus on aesthetic passes.

Isometric Projection

We decided to go with an Isometric perspective for aesthetic reasons. This meant that I needed to figure out how it would be implemented and what the best method would be, given our short time frame. I ended up coding a coordinate space converter that could be used for all the automated behaviors like moving player, scripting objects, etc.

 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
//edited from 0.5f to 0.7f to line up edges for 45* angles on tiles
    public Vector2 CartToIso(Vector2 cartCoords)
    {
        Vector2 temp;
        temp.x = cartCoords.x - cartCoords.y;
        temp.y = (cartCoords.x + cartCoords.y) * 0.7f;
        return temp;
    }

    //edited from 0.5f to 0.7f to line up edges for 45* angles on tiles
    public Vector2 IsotoCart(Vector2 isoCoords)
    {
        Vector2 temp;
        temp.x = (isoCoords.y / 0.7f + isoCoords.x ) * 0.5f;
        temp.y = (isoCoords.y / 0.7f - isoCoords.x ) * 0.5f;
        return temp;
    }

    GameObject PlaceTile(GameObject tileType, Vector2 isoCoords, Vector2 tileCoords, Vector3 rotation)
    {
        GameObject newTile = GameObject.Instantiate(tileType, isoCoords, Quaternion.Euler(rotation));
        
        // Sort depth using the sum of the x and y coordinates
        newTile.transform.position = new Vector3(isoCoords.x, isoCoords.y, (tileCoords.x + tileCoords.y));
        return newTile;
    }

Movable Tile Highlighting - Conveyance

It was pointed out in our initial play tests that the game did not convey which tiles were movable. To solidify this conveyance, we used a highlighting system that:

  • Shows all possible move tiles.
  • Highlights tiles until a fence is reached.
  • Highlights adjacent tiles to teleporter if the teleporter is linked.
 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
oid HighlightMovableArea( Vector2 directionToHighlight )
    {
        int i = curRow;
        int j = curCol;
        
        GameObject currentTile;
        while ( i < numRows || j < numCols ) //Highlights the north side of character
        {
            currentTile = GetTileFromCoords(new Vector2(i, j));
            if(currentTile)
            {
                if (currentTile.GetComponent<Tile_TileBehavior>().isWall)
                {
                    break; // Stop highlighting if wall is reached
                }

                if (currentTile.tag == "Tiles" || currentTile.tag == "Key" || currentTile.tag == "Pickup_Turn" || currentTile.tag == "Goal")
                {
                    currentTile.transform.GetChild(1).gameObject.SetActive(true);
                    currentTile.transform.gameObject.layer = LayerMask.NameToLayer("TileLayer"); 
                    movableTiles.Add(currentTile);
                }
                if (currentTile.tag == "Teleporter1")
                {
                    if (currentTile.GetComponent<TeleportTile_TeleportScript>().isTeleportActive && currentTile.GetComponent<TeleportTile_TeleportScript>().chooseTeleporter)
                    {
                        currentTile = GetTileFromCoords(new Vector2(i + 1, j));
                        if (currentTile)
                        {
                            if (currentTile.tag != "Hole")
                            {
                                currentTile.transform.GetChild(1).gameObject.SetActive(true);
                                currentTile.transform.gameObject.layer = LayerMask.NameToLayer("TileLayer");
                                movableTiles.Add(currentTile); // Highlight the adjacent tile if teleporter is linked
                                
                                break;
                            }
                        }
                    }
                }
            }
            i += (int) directionToHighlight.x;
            j += (int) directionToHighlight.y;
        }
    }

Teleportation

The game teleports the player when the player reaches the center of the teleporter tile (if active and linked). Additionally, the player moves one step when exiting the teleporter in the direction that the player entered. The script also manages necessary flags during the teleport to ensure that the player doesn’t cycle between the locations repeatedly.

 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
IEnumerator TeleportPlayer()
    {
        GameObject player = GameObject.Find("Player");
        PlayerCharacter_IsometricMove scriptRef = player.GetComponent<PlayerCharacter_IsometricMove>();
        
        int indexOfNext = int.Parse(chooseTeleporter.name.Substring(0,2));
        yield return new WaitForSeconds(0);

        scriptRef.targetPos = chooseTeleporter.transform.position;
        player.transform.position = chooseTeleporter.transform.position;
        AudioManager_AudioController.GetInstance().PlaySFX(eAudioName.PortalUse, audioSource);
        
        chooseTeleporter.GetComponent<TeleportTile_TeleportScript>().isTeleportUsed = true;

        string temp = null;
        if (scriptRef.MoveDir.x >= 0)
        {
            if(scriptRef.MoveDir.y >= 0)
            {
                indexOfNext++;
                temp = indexOfNext.ToString();
                if (temp.Length == 1)
                    temp = "0" + indexOfNext;
                
            }
            else
            {
                indexOfNext -= 10;
                temp = indexOfNext + "";
                if (temp.Length == 1)
                    temp = "0" + indexOfNext;
            }
        }

        if (scriptRef.MoveDir.x < 0)
        {
            if (scriptRef.MoveDir.y >= 0)
            {
                indexOfNext += 10;
                temp = indexOfNext + "";
                if (temp.Length == 1)
                    temp = "0" + indexOfNext;
            }
            else
            {
                indexOfNext--;
                temp = indexOfNext.ToString();
                if (temp.Length == 1)
                    temp = "0" + indexOfNext;
                
            }
        }

        GameObject target = GameObject.Find(temp);
        if (target && !target.GetComponent<Tile_TileBehavior>().isWall)
        {
            scriptRef.targetPos = GameObject.Find(temp).transform.position;
            scriptRef.isMoving = true;
        }
        else
        {
            scriptRef.isMoving = true ;
            scriptRef.currentTile = chooseTeleporter;
        }

        DeactivatePortal(this.gameObject);
        scriptRef.isTeleporting = false;
        StopCoroutine(TeleportPlayer());
    }

Personal Postmortem

Team Postmortem