collin.town
projects.

/

contact.
Year 2 Final Project

Second Year Final Project

Game Design

Objectives

The player takes on the role of a mercenary hired by one of the factions to explore the ruins of the old world and retrieve valuable artifacts and Arctium crystals. These ruins are filled with danger, including dungeon crawlers, cave spiders, and automated defenses, all seeking to eliminate anyone who dares to enter their territory. To survive, the player must use their starting weapon to hunt their way through enemies and collect the Arctium treasures hidden amongst the treacherous dungeon ruins.

As players delve deeper into the ruins, they will gather valuable items—such as hearts and Arctium crystals—that will give them an advantage over their enemies. But they must take caution to not dive too deep, lest they awaken the ultimate Arctbeast, creator of the Arctium.

Ultimately, the fate of this world rests on the shoulders of the player. Will they fight for their own gain by upgrading their weapons with the Arctium crystals they collect, or will they work to save the crystals in hopes of bring peace to a shattered world?

Game Mechanics

In this dungeon-themed top-down shooter game, the player is tasked with making their way through the dungeon ruins with a pistol to fight through enemies and bring the Arctium crystals to the chest at the end of each level. The player starts the game with 100 health and an inventory of 6 slots. If the player loses all their health, they die and restart at the beginning of the current level with their inventory retained.

World Levels

Ruin Dungeon

  • Infested with dungeon crawlers
  • The player navigates through a maze-like room containing Arctium crystals, stronger weapons, and a chest at the end of the room
  • The player can either store the crystals in their inventory to increase their collection, or use the crystals to create valuable ammo to fend off the horde

Arctium Cavern Dungeon

  • Infested with dungeon crawlers, mutant cave spiders and Arctium turrets
  • The player navigates through the constant barrage of Arctium turret blasts whilst fending off crawlers and spiders
  • As before, the player must bring their collected Arctium to the end chest

Game Development

Structural Design

The game engine is built using a component-based design, where each game object is a collection of components that define its behavior. The game engine is built using the following components:

  • Transform: Defines the position, rotation, and scale of the game object
  • Sprite: Defines the sprite of the game object
  • Collider: Defines the collision box of the game object
  • Light: Defines the light source of the game object

Pathfinding

class Node(Entity):
    """A node is a point in the grid that can be connected to other nodes."""
    def __init__(self, rect: pygame.Rect, active: bool = True):
        """Create a new node."""
        super().__init__(rect)
        self.x = rect.x
        self.y = rect.y
        self.active = active
        self.neighbors: list[Node] = []
        self.connection: Node = None
        self.__g: float = 0.0
        self.__h: float = 0.0

This code snippet shows the Node class, which is a subclass of the Entity class. The Node class contains the x and y coordinates of the node, a list of the node’s neighbors, a reference to the node’s connection, and the node’s g and h values. The Node class also contains a method to get the node’s f value, which is the sum of the node’s g and h values.

# Loop through the open nodes
while len(toSearch) > 0:
    current_node = min(toSearch, key=lambda node: node.get_f())

    if current_node == target_node:
        path = [current_node]

        # Loop through the connections
        while current_node.connection is not None:
            # Add the connection to the path
            path.append(current_node.connection)

            # Set the current node to the connection
            current_node = current_node.connection

        # Reverse the path and return it
        return path[::-1]

    # Remove the current node from the open nodes list and add it to the closed nodes list
    toSearch.remove(current_node)
    searched.add(current_node)

    for neighbor in current_node.neighbors:
        in_search = neighbor in toSearch

        # If the neighbor is not active or the neighbor is in the closed nodes list, skip it
        if not neighbor.active or neighbor in searched:
            continue

        cost_dist = np.sqrt((neighbor.x - current_node.x) ** 2 + (neighbor.y - current_node.y) ** 2)
        cost_to_neighbor = current_node.get_g() + cost_dist

        # If the neighbor is not in the open nodes list, set the neighbor's g value to the cost to the neighbor and add it to the open nodes list
        if not in_search or cost_to_neighbor < neighbor.get_g():
            neighbor.set_g(cost_to_neighbor)
            neighbor.connection = current_node

            if not in_search:
                h_dist = np.sqrt((neighbor.x - target_node.x) ** 2 + (neighbor.y - target_node.y) ** 2)
                neighbor.set_h(h_dist)
                toSearch.add(neighbor)

And this code snippet shows the A* algorithm. The algorithm loops through the open nodes list and finds the node with the lowest f value. If the current node is the target node, the algorithm loops through the current node’s connections and adds them to the path list. The algorithm then reverses the path list and returns it. If the current node is not the target node, the algorithm removes the current node from the open nodes list and adds it to the closed nodes list. The algorithm then loops through the current node’s neighbors and calculates the cost to get to each neighbor from the current node. If the neighbor is not in the open nodes list or the cost to get to the neighbor from the current node is less than the neighbor’s g value, the neighbor’s g value is set to the cost to get to the neighbor from the current node and the neighbor is added to the open nodes list. The algorithm then loops through the open nodes list again and repeats the process until the target node is found or the open nodes list is empty.

Lighting

For the lighting we used a modified version of this lighting engine. We modified it to work dynamically with the Tiled library and our component system to work nicely with our threading system.

Key Takeaways

What Went Well

  • We were able to implement a component-based game engine that allowed us to easily add new features to the game
  • We were able to implement a pathfinding algorithm that allowed us to create a dynamic enemy AI
  • We were able to implement a lighting engine that allowed us to create a dynamic lighting system
  • We were able to implement a threading system that allowed us to run the compute intensive pathfinding on a separate threads
  • We were able to implement a save system that allowed us to save the player’s progress
  • We were able to implement a menu system that allowed us to create a main menu and pause menu

What Could Have Gone Better

  • We could have implemented a better lighting engine that allowed us to create a more dynamic lighting system
  • We could have implemented a more efficient pathfinding algorithm that allowed us to create a more dynamic enemy AI
  • We could have created more levels that allowed us to create a more dynamic gameplay experience
  • We could have utilized our save system to create more states that the user could have been in throughout the game