cc3k

CS246 Final Project

As this project contains course assignment material, due to the University of Waterloo Policy 71 academic freedom rules, the source code will be available by request (please send me an email).

CC3k+ Final Design Document

1. Overview

The project is divided into several parts by directory:

  • src/

    • Files at root deal with terminal-specific rendering and input handling.

    • base/

      • Contains code that deal with scene management and generation, collision management, input management. Also contains the default scene layout.

      • Contains no game logic.

    • engine/

      • Contains the entity-component-system (ECS)-based game engine. This is completely independent of the game logic and base code.

    • game/

      • Contain game logic code. Files prefixed with Behaviour deal with in-game behaviour (i.e., BehaviourPlayer is responsible for player movement/attack/interactions). In-game items are implemented by Item*. Specialized non-player characters (NPCs) are in Npc*. Finally, player character (PC) race specializations are implemented in Pc*.

  • tests/

    • Contain unit and integration tests. lib/ contain the test fixtures.

The project is further split between 4 main implementation responsibilities: game logic, scene management/generation, input handling and rendering. Moreover, these 4 main responsibilities remain intact from the original plan (and its implementation is split between base/ and game/).

We will delve more into this in Section 3.

2. Updated UML

3. Design

The design of this project is based off the entity-component system (ECS). This is a ubiquitous design pattern in the game development industry. We will break this down:

  • Entity: Implemented by the GameObject class. Entities can be instantiated inside a scene. Refer to the UML diagram to see how scene management, GameObjects, Components will relate.

  • Component: Implemented by the Component class. In our design, these serve "decorate" entities -- providing both data and behaviour to the GameObjects.

  • System: This is a process that acts over all entities. In this case, we consider the scene to be the only system here. Since CC3k is very simple, most of this is controlled by the specialized CC3kScene class.

Moreover, we allow components to communicate with each other. This pattern allows us to bypass the observer pattern's limitation of needing to manually register observers. Here, we consider every component an observer of whatever system it's in.

Every component, by default, observes the update method call (issued by the scene). However, this pattern can also account for other method calls (called "messages").

Let's consider an example implementation. BehaviourPlayer is a component (see UML, it first derives BehaviourAttackable which derives Engine::Component) and we want to detect collisions when they happen. So,

class BehaviourPlayer : public BehaviourAttackable, public OnCollision {
    ...
protected:
    void onCollision(CollisionInfo info) override;
}

we can make the class inherit OnCollision. This forces us to implement a message handler function. The sender of the message (in this case, the ComponentCollider class) will then invoke:

// Insider ComponentCollider
if (didCollide(...)) {
    broadcastMessage(&OnCollision::onCollision, CollisionInfo{...});
}

The broadcastMessage method is at the heart of our program architecture. You can see the implementation in engine/GameObject.h. Moreover, the caller/handler relationship (see last page of UML.pdf) can be described naturally. Consider another example:

If the player attacks the enemy, we want the player to send a message (say, onAttacked) because the player entity "wants" to affect the enemy entity. Thus, the player must be the caller and the enemy, the receiver.

This pattern therefore allows us to very naturally decompose the game design into many (many) pairwise interactions in the form of message sending/receiving. This message passing scheme replaces the event handler pattern proposed in the plan because of the reasons outlined.

However, one severe limitation in the implementation of this design. Scene management and world generation does not integrate effortlessly into our ECS implementation. Actions such as restarting the game, ending the game and switching scenes are less elegant when implemented. We will discuss these limitations in the next section.

4. Resilience to Change

We've mostly established the ECS as a highly modular system. Unrelated game behaviours are implemented as separate, modular, components. Moreover, this message passing implementation does not force compilation dependencies between the sender and recipient (they only rely on the common On... classes).

This means changing some aspect of game behaviour is relatively effortless.

However the current design is limited by its ability to accommodate other changes. For instance, if you want to add more scene states (i.e., conditionally switch levels or something similar) then you would have to change quite a lot of code (probably rewrite a good chunk of CC3kScene).

Moreover, world generation can also be difficult to extend. ECS relies on some other method of assembling entities and components. That means, all our entity instantiating code lives alongside the world generation code. For this game, that's fine, but this quickly degenerates if there were more entities/items to spawn.

One solution would be to implement some sort of item registry or factory pattern. However, this is may be too complex for CC3k+. Game engine/frameworks such as Unity handle this by offering prefabs (prefabricated GameObjects) that may be created within the editor UI and instantiated in the code.

Aside from the lack of fancy game editor UI, we also run into the issue of cloning GameObjects. Since Component is an abstract class, we cannot clone it without employing a polymorphic clone solution. Our codebase does not support this (there's unfortunately a half-completed implementation which require changing some references to pointers to fix; this is not ideal).

5. Specification Q&A

Question: How could your design your system so that each race could be easily generated? Additionally, how difficult does such a solution make adding additional classes?

Answer: Races are simply new components which inherit from BehaviourPlayer. All aspects of the player can be override and extended. This is not too different than the original proposal.

Question: How does your system handle generating different enemies? Is it different from how you generate the player character? Why or why not?

Answer: Same as above. Enemies are first described by an NpcDescriptor structure which contain basic information (such as hp, attack, defense). If that's not enough (i.e., if they need specialized behaviours), enemies can specialize into derived classes, such as NpcMerchant that inherit from BehaviourNpc.

Question: How could you implement special abilities for different enemies. For example, gold stealing for goblins, health regeneration for trolls, health stealing for vampires, etc.?

Answer: See above. If enemies cannot be described by NpcDescriptor, then new derived classes can be made. These two answers are also very similar to the original proposal.

Question: What design pattern could you use to model the effects of temporary potions (Wound/Boost Atk/Def) so that you do not need to explicitly track which potions the player character has consumed on any particular floor?

Answer: We ended up using the ECS to implement every aspect of the game. So, potion effects are implemented by the player sending a "use" message to the potion along with some state, then the potion modifies that state and the player applies the effect. This chain of events is also similar to how Minecraft handles potion effects. This "state" that's passed around can be reset to 0 to clear all effects.

Question: How could you generate items so that the generation of Treasure, Potions, and major items reuses as much code as possible? That is for example, how would you structure your system so that the generation of a potion and then generation of treasure does not duplicate code? How could you reuse the code used to protect both dragon hordes and the Barrier Suit?

Answer: All generation code is inside the static SceneBuilder class. A lot of common code is refactored into helper methods. World generation is best done in one place, as it makes modification quite easy (though, see the limitations outlined in the previous section).

6. Final Questions

What lessons did this project teach you about developing software in teams? If you worked alone, what lessons did you learn about writing large programs?

Answer: Working alone, I learned learned how difficult (and stressful) it is to write large programs. While designing support for the ECS, I realized how optimized ECS was for a team project (as each component can be developed separately and independently).

I also learned about how important testing is. I used a testing framework (GTest) which generated coverage data and was able to find a good deal of quite nasty bugs.

I also saw, on the way, some "common OOP" problems such as polymorphic cloning, covariant typing and the dreaded diamond of death (w.r.t. multiple inheritance). Moreover, I realized the motivation behind "what causes these problems" and the solutions presented. If anything, I learned how difficult writing large programs (correctly) can be.

What would you have done differently if you had the chance to start over?

Answer: I would not choose such a hefty abstraction such as ECS. I don't know what alternative pattern I would use but bringing in ECS felt like cutting apples with an axe. I would also put more effort into finding a team to do this project with.

Last updated