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.
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.
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:
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.
publicenum Decision
{
NotDecided,
Stay,
UpMove,
RightMove,
DownMove,
LeftMove
}
public interface ICmpMovementEntity // [1]
{
int Initiative { get; set; } // [2]
Decision RequestDecision (); // [3]
}
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]
}
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);
}
}
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 ();
}
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);
}
private void NextTurn ()
{
entitiesMovedInTurn.Clear ();
}
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 ();
}
}
}
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;
}
}
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.
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.