5 min read

This post presents a simple turn-based game framework using the Duality game engine. The engine is written in C#, and scripting it requires the same language. The word ‘scripting’ is a bit misleading here, because it is more of the process of extending the engine. In this text, the inner workings of Duality are not explained in detail, as it is better done by the official documentation on GitHub. However, if you are familiar with the vocabulary of game development, the tool is rather easy to pick up, and this guide can be also followed. In addition, these concepts can be tailored to fit any other game framework or engine.

Required tools

Duality can be downloaded from the official site. A C# compiler and a text editor are also needed. Visual Studio 2013 or higher is recommended, but other IDEs like MonoDevelop also work.

Overall model

Turn-based games were popular long before the computer era, and even these days they are among the most successful releases. These games often require analytical thinking instead of lightning-fast reflexes, and favor longer term strategy over instinctive decisions. The following list gathers the most typical attributes of the genre, which we need to consider while building a turn-based game:

  • The game process is divided into turns.
  • Usually every player takes action once per turn. Everyone has to wait for their opportunity.
  • The order of the players’ actions can be fixed, or based on some sort of mechanic of initiative. In the latter case, the order can change between turns.
  • During the game, some players drop out, some others can join. The system has to handle these changes.

Implementation

In the following paragraph, a prototype-quality system is described in order to achieve these goals. In addition to discrete time measurement, turn-based games often utilize discrete measurement of space, for example a grid based movement system. We are implementing that as well.

The solution contains two distinct building blocks: a manager object an entity object which has multiple instances. The manager object, as its name suggests,arranges the order of the entities taking action in the turn and asks them to decide their own action, a movement direction in our case. It should not distinguish between player-controlled entities and AI ones. Thus the actual logic behind the entities can be various, but they need to implement the same methods—it is clear that we need an interface language struct for that.

Defining the ICmpMovementEntity interface

publicenum Decision
{
NotDecided,
    Stay,
UpMove,
RightMove,
DownMove,
LeftMove
}

public interface ICmpMovementEntity // [1]
{
int Initiative { get; set; } // [2]
    Decision RequestDecision (); // [3]
}
  1. The interface’s name has the ICmp prefix. This follows a convention in Duality, and indicates that only Component objects should implement that.
  2. Initiative returns an integer, used by the manager object to determine the order of entities in the turn.
  3. The method RequestDecision returns the enum type Decision. Its value is NotDecided, when there is no decision yet. In that case, the same entity is asked at the next game loop update. If the returned value is Stay, the entity object remains at its place, otherwise it is moved to the returned direction.

Implementing the manager object

The skeleton of the manager object is the following:

internal class TurnMovementManager
{
    private const float GRID = 64; // [1]

    private readonlyHashSet<ICmpMovementEntity>entitiesMovedInTurn = new HashSet<ICmpMovementEntity> (); // [2]

    private ICmpMovementEntityonTurnEntity; // [3]

    public void Tick (); // [4]

    private ICmpMovementEntityGetNextNotMovedEntity (); // [5]

    private void MoveEntity(ICmpMovementEntity entity, Decision decision); // [6]

    private void NextTurn (); // [7]
}
  1. GRID is the discrete measurement step in the game world.
  2. entitiesMovedInTurn is the set of the entities already taken action in the current turn.
  3. onTurnEntity keeps track the entity that is asked to decide its move next.
  4. Tick is invoked every game loop update. The main processing happens here.
    public void Tick ()
    {
    onTurnEntity = GetNextNotMovedEntity ();
        if (onTurnEntity == null) {
    NextTurn ();
            return;
        }
    
    var decision = onTurnEntity.RequestDecision ();
        if (decision != Decision.NotDecided&& decision != Decision.Stay) {
    entitiesMovedInTurn.Add (onTurnEntity);
    MoveEntity (onTurnEntity, decision);
        }
    }
    
  5. GetNextNotMovedEntity collects the entities not moved in the current turn, sorts them by their initiative, and returns the first. If there are no unmoved entities left, it returns null.
    privateICmpMovementEntityGetNextNotMovedEntity ()
    {
    varentitiesInScene = Scene.Current.FindComponents<ICmpMovementEntity> ();
    varnotMovedEntities = entitiesInScene.Where (ent => !entitiesMovedInTurn.Contains (ent)).ToList ();
    
        Comparison<ICmpMovementEntity> compare = (ent1, ent2) =>ent2.Initiative.CompareTo (ent1.Initiative);
    notMovedEntities.Sort (compare);
    
        return notMovedEntities.FirstOrDefault ();
    }
  6. MoveEntity displaces the currently processed entity, according to its decision.
    private void MoveEntity (ICmpMovementEntity entity, Decision decision)
    {
    varentityComponent = onTurnEntity as Component;
    var transform = entityComponent.GameObj.Transform;
    Vector2 direction;
    
        switch (decision)
        {
            case Decision.UpMove:
                direction = -Vector2.UnitY;
                break;
            case Decision.RightMove:
                direction = Vector2.UnitX;
                break;
            case Decision.DownMove:
                direction = Vector2.UnitY;
                break;
            case Decision.LeftMove:
                direction = -Vector2.UnitX;
                break;
            case Decision.NotDecided:
            case Decision.Stay:
            default:
                throw new ArgumentOutOfRangeException(nameof(decision), decision, null);
        }
    
    transform.MoveByAbs(GRID * direction);
    }
    
  7. NextTurn clears the moved entity set.
    private void NextTurn ()
    {
    entitiesMovedInTurn.Clear ();
    }
    

Driving the manager object from the CorePlugin

Developing games in the Duality engine is usually done via Core Plugin development. Every assembly that extends the base engine functionality needs to implement a CorePlugin object. These objects can be used drive global logic, such as our manager class. The TurnbasedMovementCorePlugin class overrides the OnAfterUpdate method of its superclass, to update a TurnMovementManager instance every frame.

public class TurnbasedMovementCorePlugin : CorePlugin
{
    private readonlyTurnMovementManagerturnMovementManager = new TurnMovementManager();

    protected override void OnAfterUpdate ()
    {
base.OnAfterUpdate ();
        if (DualityApp.ExecContext == DualityApp.ExecutionContext.Game) {
turnMovementManager.Tick ();
        }
    }
}

Trivial test implementation of a ICmpMovementEntity

ICmpMovementEntity implementations can be complex, but for demonstration purposes, a simpler one is presented below. It is based on user input.

[RequiredComponent(typeof(Transform))]
public class TurnMovementTestCmp : Component, ICmpMovementEntity
{
    public int Initiative { get; set; } = 1;

    public Decision RequestDecision ()
    {
        if (DualityApp.Keyboard.KeyHit (Key.Space))
            return Decision.RightMove;
        return Decision.NotDecided;
    }
}

Summary

Of course more convoluted ICmpMovementEntity implementations are needed for game logic. I hope you enjoyed this post. In case you have any questions, feel free to post them below, or on the Duality forums.

About the author

LorincSerfozo is a software engineer at Graphisoft, the company behind the the BIM solution ArchiCAD. He is studying mechatronics engineering at Budapest University of Technology and Economics, an interdisciplinary field between the more traditional mechanical engineering, electrical engineering and informatics, and has quickly grown a passion towards software development. He is a supporter of opensource software and contributes to the C# and OpenGL-based Duality game engine, creating free plugins and tools for itsusers.

LEAVE A REPLY

Please enter your comment!
Please enter your name here