Entity Variants Using Inheritance

Introduction

Most games include multiple groups of entities (which we'll refer to as variants) such as enemies, bullets, and pickups. Variants usually share common characteristics such as:

  • Reactions to collision, such as always dealing damage to the player

  • Creation logic, such as being created by the player when shooting or by an enemy spawner

  • Interfaces and code such as implementing ICollidable or being a top-down entity

Of course, variants can also differ. Common examples of how variants may differ include:

  • Visuals, such as different .achx files (Animation Chains)

  • Coefficients and variables such as movement speed or max HP

  • Logic, such as movement patterns or navigation behavior

  • Collision shapes and sizes

Conceptually variants can differ in any number of ways - the lists above are just common examples.

The current recommended approach for creating entity variants (as of the time of this writing in February 2024) is to use entity inheritance. Conceptually speaking you can conceptually achieve variants through States, loading custom data files, or any other approach. However, inheritance is very well supported in FlatRedBall. When using inheritance, the FRB Editor provides code generation which simplifies common operations including creating new instances, accessing derived type files, and reading coefficient values.

This and other Tutorials refer to the "Variants" (capitalized) as a class generated by the FRB. The term veriant (lower-case) refers to the concept of creating multiple types of entities which differ as explained above.

Screen Variants

This documentation discusses Entity variants primarily; however, the concepts and syntax discussed here also apply to Screens. In other words, the FRB Editor generates Variant classes in both Entity and Screen generated code, so variants can be used to access information about derived Screens such as Level screens.

Entity Inheritance in the FlatRedBall Editor

FlatRedBall entities provide support for inheritance. By default all entities inherit from the PositionedObject class, but inheritance can be changed to that entities can inherit from other entities. When working with entities with multiple variants, it's best to create a base entity such as Enemy, then create derived entities from the base.

For example, a game project may include an entity called Enemy which serves as the base for all other enemies.

Derived entities might include the different types of enemies that exist in the game. Note that the Enemy entity may never be directly instantiated in the game. It only serves to include all common code, files, and objects for all other enemies. This is similar to the GameScreen screen and the derived Level screens.

When creating a variant of the Enemy entity, the Enemy entity type should be selected as the base type.

It may be helpful to organize your derived variants in a subfolder. For example, the following enemy variants are all stored in an Enemies folder.

Inheritance can also be set on an Entity after it has been created by selecting the entity and changing its BaseEntity type in the Properties tab.

Notice that when a derived Entity is created, FlatRedBall does not create a list for that derived type. In most cases you will not need a separate list for each variant - you can use the list for the base type for collision and game logic. In this case, the GameScreen includes an EnemyList, but does not include lists for the specific enemy types.

Customizing Variant Visuals

As mentioned earlier, entity variants can differ in many ways. One of the most common is through the visuals. The most common way to vary the visuals for each type is to create a Sprite in the base entity, but to assign different animation chains on the derived types.

For example, the Enemy entity includes a Sprite instance, but the Sprite does not have its AnimationChains set.

Each derived entity would typically include its own AnimationChains. For simplicity, the file can be named the same in all cases (AnimationChainListFile). While this may seem less expressive compared to having different names for each file, keeping the name the same can make your code much simpler if you need to access the file. To learn more about creating .achx files, see the Animation Editor documentation.

Each derived entity needs to access the SpriteInstance defined in the base entity to modify its appearance. To od this, we can expose SpriteInstance in the base entity. This can be done by selecting the Sprite in the base entity and setting its ExposedInDerived property to true.

By setting this value to true, the Sprite is accessible in all of the derived entities.

Now we can assign the animation chain on each of the deirived SpriteInstances. You can do this by selecting the Sprite and changing its AnimationChains dropdown.

Alternatively you can drag+drop the .achx file onto the Sprite. This is a shortcut to setting the Animation Chains through the dropdown.

Regardless of which approach you use, be sure to also set the Current Chain Name to a valid animation.

If you do not have any animations in the Current Chain Name drop-down, you need to first add new animations to your .achx.

Typically each AnimationChainListFile would have animations which are named the same. This allows code to set the animation for every enemy variant using the same logic. For example, you might have animations for Idle, Walk, Attack, and Die. These may also include different directions (such as Left, Right, Up, and Down).

Adding Derived Instances to Your Level

Once you have created derived entities, you can add them to your levels. The easiest way is to use the Live Edit system to drag+drop them into your game.

Notice that every instance is added to EnemyList. FlatRedBall understands that the derived entities should be organized in the base entity list. This makes handling collisions much easier than if each type had its own list.

Entity instances can also be created in code by using their respective factories. When a derived entity's factory is used, the newly-created instance is automatically added to the base list. Derived entities end up in their base list whether created through the FRB Editor or in custom code.

The following code shows how to create an entity instance whenever the user clicks the cursor (mouse) in GameScreen.cs.

void CustomActivity(bool firstTimeCalled)
{
    var cursor = GuiManager.Cursor;

    if(cursor.PrimaryClick)
    {
        Factories.SnakeFactory.CreateNew(cursor.WorldPosition);
    }
}

Using the Variant Type

Games often need to refer variants in code or in the FRB Editor. For example, an EnemySpawner entity may need to define which Enemy variant to spawn. FlatRedBall provides a special type for all derived entities which can be used in the FRB Editor and code which is called a Variant.

For this example, consider an Entity called EnemySpawner. Each EnemySpawner instance may need to indicate which Enemy variant it should spawn. To support this, we can add a new variable of type EnemyVariant to the EnemySpawner.

In this case, the Entities.EnemyVariant appears as a possible variable type because the Enemy has at least one derived entity. If you create more entities which have derived types, then they will also appear in this list.

After adding this variable, instances of the EnemySpawner can be assigned an enemy type.

This variable can be used in code to create instances of your variant. For example, the following code could be added to EnemySpawner.cs to create its enemy variant whenever the space bar is pressed.

private void CustomActivity()
{
    if(InputManager.Keyboard.KeyPushed(Microsoft.Xna.Framework.Input.Keys.Space))
    {
        this.EnemyVariant.CreateNew(this.Position);
    }
}

This code uses the space bar for the sake of simplicity, but a real implementation may spawn enemies based on a timer, player proximity, or other game logic.

Accessing Derived Entity Variables

Variants, such as enemies, may have different variables which may need to be accessed without the creation of an instance. For example, your game may include a bestiary of enemies that the player has encountered. This may be shown to the player using a Gum UI which dislays information about the enemy such as its MaxHealth.

Variables such as MaxHealth can be defined per enemy type. For example, the following image shows a Skeleton which has 80 MaxHealth.

This can be accessed in a type-safe way in code using the EnemyVariant class as shown in the following snippet:

var maxHealth = EnemyVariant.Skeleton.MaxHealth;

Similarly, the information is accessible through an EnemyVariant variable which might be assigned in the FRB Editor. For example, the EnemySpawner would also be able to access the MaxHealth of the enemy variant that it spawns.

var maxHealth = this.EnemyVariant.MaxHealth;

EnemyVariant can also be accessed through their name. For example, the following code would get the MaxHealth of a Skeleton:

var maxHealth = EnemyVariant.FromName("Skeleton").MaxHealth;

Accessing Derived Files

At times your game may need to access files from a derived entity. For example, if we consider the bestiary example from above, you may want to display an image of your enemy in Gum UI. To do this, you would need to access the AnimationChain from the entity. Variants provides access to file loading. For example, we can access a Skeleton's AnimationChainListFile using the following code:

var animationChains = EnemyVariant.Skeleton.GetFile("AnimationChainListFile");
// use animationChains to display the enemy, such as in a separate sprite, or in Gum

Using the Name to Serialize Data

Your game may need to serialize data about derived types. For example, the player may equip different weapon variants which should be saved in the profile. The Name property on a derived Variant can be used to uniquely identify the type as shown in the following code.

// This assumes that Weapon is a base Entity for the different types
// of weapons that the player can equip
var equippedWeapon = this.WeaponVariant;
var name = equippedWeapon.Name;
// use the name to save to disk
// Later, the weapon can be obtained using the name
// ...
this.WeaponVariant = WeaponVariant.FromName(name);

All List

The Variant class includes a static list named All which provides access to all Entity variants. The All list can be useful in a number of situations including:

  • Listing all Level screens (Screens inheriting from GameScreen) in a LevelSelect screen or in a debug menu

  • Displaying all enemies in a bestiary

  • Selecting a random entity for instantiation - either the entire set or subset

The following code shows how to fill a ListBox with available Screen names which could be used in level selection UI:

var listBox = Forms.ListBoxInstance;
foreach(var level in GameScreenVariant.All)
{
   listBox.Items.Add(level.Name);
}

Creating New Variants

Variants are created automatically by FlatRedBall in the base entity's codegen. These entities are hard-coded and will be compiled into your game, but you are not limited to using just these variant types. Instead, you can create new variants in custom code.

For example, consider an entity type called Enemy. A new variant can be created in code using the following code:

var newVariant = new EnemyVariant();
newVariant.PointValue = 300;
newVariant.MaxHealth = 1000;

This variant can be used to instantiate enemies with the assigned values as shown in the following code:

var enemyInstance = newVariant.CreateNew(x:100, y:50);

Since this new enemy type is not created in the FlatRedBall Editor, its C# type is Enemy (the base Entity type). Note that custom variants can only be created if the base class can be instantiated through a factory. That means that the base class must have the following:

Last updated