How I Created Abstract Classes in Godot

I love real-time strategy games. I was first introduced to the genre by the 2004 version of Axis and Allies made by Timegate Studios. Since then, I have tried to build custom maps, and now I want to build my own game. I chose to work in Godot as an excuse to continue learning the engine after trying it out for smaller projects.

To start, I need to have a “Unit” class. This class represents basic properties a moveable game piece needs to function. That includes things like how to move around the map, if the piece is selected, what team the unit is on, etc. I then extended this class to make a variety of sub-units. Each class needs to know its own unit type so game events can interact with it properly. Eventually Unit will be an abstract class and all sub-units will extend Unit.

Base Class

Let’s start with the initial implementation of the unit class looked like this:


extends RigidBody3D
class_name Unit
static func get_type() -> Types.Node_Types:
   return Types.Node_Types.UNIT

In the actual game, there is pathfinding logic, variables that hold what team the unit is on, and more. This information is not relevant to the structure of the base class or its derived classes, so they are omitted. Notice that the Unit class is not an abstract class yet. It will need to be made abstract after it is extended.

Extending the Unit Base Class

When I added units with custom behavior, I needed game events to know what type of unit they were dealing with. To achieve this, I attempted to override the “getType” function to instead return the type of unit, for example: “Carrier” or “Infantry.” A class might look like this:


extends Unit
class_name Carrier

static func get_type() -> Types.Unit_Types:
    return Types.Unit_Types.CARRIER

With this implementation, I saw an error, though.

The function signature doesn't match the parent. Parent signature is "get_type() -> types.gd.Node_Types".gdscript(-1)
enum Unit_Types{CARRIER = 0, FIGHTER = 1, GUNSHIP = 2, UNKNOWN = 3}
Defined in res://utils/types.gd

The error is thrown because I had statically typed the return of “getType” in the Unit base class. The Carrier class cannot change the return type of getType when overriding it. This error led me to the realization that I needed two different ways of getting an object’s type. I needed to know the type of the base class, for example “Building” or “Unit.” I also needed to know its subtype within that examples of sub-units is “Carrier” or “Infantry”. That means I would need two different functions. I settled on using “get_unit_type”. With the new function, the carrier class implementation looked like this:


extends Unit
class_name Carrier

static func get_unit_type() -> Types.Unit_Types:
     return Types.Unit_Types.CARRIER

This new implementation resolves the error. It also allows other parts of the game to figure out exactly what type of unit this game object is when collisions and events happen. Other classes derived from Unit will also need this function. So I don’t forget to implement get_unit_type on classes derived from the unit I would like to use an abstract class. That way all classes that extend Unit must also declare a get_unit_type function.

Unfortunately, Godot does not support abstract functions because each script can theoretically be instantiated on its own which would not be allowed for an abstract class. The good news is, there are ways to work around this and fake abstract classes.

Faking Abstract Classes

As a workaround, an error can be thrown in the base class using assert.


extends RigidBody3D
class_name Unit
static func get_type() -> Types.Node_Types:
     return Types.Node_Types.UNIT
static func get_unit_type() -> Types.Unit_Types:
     assert(false, "This class is abstract")
     return Types.Unit_Types.UNKNOWN

This will raise an error and help remember to define this function in all derived classes. This grants the ability to build abstract classes and can form an extensible base for a game.

If you build a good base, it’s easier to see the project through to completion.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *