Builder

Overview

You will have to show what you have learned, by reusing multiple patters we have covered along the course. There is no base code, but you are free to reuse any assets and code from your previous solutions. Especially useful are the Switch, Movement and Factory solutions. You may implement your own version as well.

In this task you will have to create a procedural dungeon. The floors and walls of the map should be endlessly generated, loaded and unloaded based on the positions of player characters. New chunks of the map should be generated lazily - a new chunk is generated when player goes near it.

There will exist two different representations of the world map. The first is the generated version held in a class responsible for the map. In this version the tiles are represented only by their type, location and prefab name. It is recommended to make these objects plain old C# classes. This version of the map is permanent until the game is closed. The second version of the map consists of Unity GameObjects with graphics and all other relevant components. Tiles for the second version are loaded into the game by instantiating prefabs and unloaded when the player is more than 1 chunk away. When revisiting a previous location the GameObject version of the map has to be reloaded based on the map data. You can reuse the map and tile classes from factory tasks base package or create your own solution.

An example of what loading and unloading chunks near the player could look like:

Requirements

Your solution to the task is graded based on the following criteria:

  • (2p) Add a png image with the domain model or class diagram of the solution to the project. You are not required to strictly follow the UML specifications. If done on paper, then include a scan or a picture of the diagram. You may also use www.draw.io. It is recommended to start from this task to plan out the classes you will have to create.
  • (1p) The map consist of chunks. Each chunk consists of 10x10 tiles.
  • (1p) The map chunks are built using the builder pattern.
  • (2p) The map data is built by the builder.
    • Map data uses plain C# classes.
    • The builder exposes methods for a director to create walls and floors.
    • If you create only a single builder, then there is no need for an interface.
    • The builder exposes a GetResult method, which returns a map chunk.
    • All tiles of the same type have to use the same tile object(i.e flyweight pattern). This applies across tiles as well, so you will need to access same builder each time new chunk has to be generated.
  • (2p) The layout of the tiles inside the map chunks is decided by the builder directors.
    • A room builder director creates a chunk that is mostly filled with floor tiles. The border of the chunk should be mostly filled by walls, but the chunk has to have an exit with floor tile.
    • A tunnel builder director creates a chunk that is mostly filled with wall tiles. All exits must be connected by a tunnel of floor tiles to at least one other exit.
    • When a new chunk is being generated a director is chosen at random.
  • (1p) The game has two or more characters, which can be controlled one at a time.
  • (3p) Only chunks that a character stands on and 8 surrounding chunks are loaded into the game. 
    • Make use of the observer pattern to decide when to load or unload a chunk. Loading operation instantiates GameObjects for the tiles and unloading destroys the tiles.
    • Load all chunks that the player characters are in and all chunks around the player character at distance of 1 chunk. (I.e area of 3x3 chunks around the player)
    • The limitations of observer pattern task do not apply in this time. You are free to make use of the built in features of C#.

Questions

How to use C# language level features to implement the observer pattern?

It is actually possible to implement observer pattern in C# with only a few lines of code due to some more advanced language features. Here is example how you would hook up an observer to a player movement script. The example sends Vector3 data to the observer, but you might want to consider sending a reference to the player object instead.

In the class that handles player movement create an Action. Defining an observable event(i.e. subject):

public event Action PlayerMoved; //Methods with a single Vector3 parameter can be attached to this action

In the class observing player create the method that handles any new information from the subject.

public void HandlePlayerMoved(Vector3 position) { /* Here we could handle loading and unloading the map chunks */ }

In the class observing player attach a method to the action. This would be the class responsible for deciding when to load or unload chunks. Attaching methods to the observed event(i.e observers):

player.PlayerMoved += HandlePlayerMoved; //Attaches the method with name HandlePlayerMoved to the action.

In the class responsible for player movement notify all listeners when a movement occurs.

PlayerMoved?.Notify(position); //Avoids sending when there are no listeners

How to keep track of generated terrain data and make the terrain infinite*?

First thing you will need is a class that will take on this responsibility. I've decided to name this as World. Your designs may wary.

using UnityEngine;

public class World : MonoBehaviour { }

This class should be a monobehaviour, as it will need to listen on messages from the game engine and take action. Other responsibilities of this class may be:

  • Requesting new map data from the directors and builders
  • Observing the player movement
  • Initiating the loading and unloading of gameobjects based on player movement and map data.

With the general idea in mind of where we are going to keep track of terrain data, we can now decide on how to do it. Again your designs may wary, but one option is to use a Dictionary. The data to be kept there would be the map chunks. Each tile in turn can be within the mapchunk, so we can rest assured that all tiles will be kept track of. The obvious choice for keys of the dictionary would be the positions of chunks.

Benefits of dictionary:

  • We only assign memory for the areas of map that we actually have data for. A two dimensional array would need to be as large as the largest coordinate and assign memory for all the locations in between.
  • We have constant time access to any chunk. If we were to use a  linked list where we only keep the chunks that exist, then we would need to search for the locations.
  • Dictionary gives us the access speed of an array and the memory requirement of a linked list. Well that is true at least in theory, in practice it has some amount of insignificant overhead.

This now only leaves us with the question of keys. As a dictionary has a single key and we have coordinates on two axis, then we need to encapsulate them somehow. Here again we have many different options and your designs can wary. For example we could go with a Unity specific approach and used the Vector2Int as a key.

private Dictionary<Vector2Int, Chunk> chunks = new Dictionary<Vector2Int, Chunk>();

Other notable options would be:

  • Define your own struct that has two non floating point numbers for coordinates
  • Use the generic Tuple class from the C# System.Collections.Generic library. I.e Tuple<int, int>.
  • Use a string that contains two numbers and a separator. This is strongly not recommended though.

Other hints

The instructions in this part are not mandatory to follow. You can make up your own solution to the requirements. This section will be expanded based on your questions.

How to stop characters from walking into the walls?

  • Add the class representing the world map to the service locator. Before each step the movement script can ask from world if the target tile is passable.
  • Do a raycast from the character to the target tile. If the tile is not passable, do not walk there.
  • Use a physics based character controller and add colliders to the walls. 

 

;