A game programming pattern for creating and managing complex behaviors with parallelism at its core.
1. act-unity
2. act-godot
3. act-unreal
Need a port for a different game engine? Open a feature request to let the developer know.
The entire pattern revolves around Acts. Each act is the smallest self-contained unit of behaviour that can be performed in the game e.g. Walk Act, Run Act, Jump Act, Reload Act, Shoot Act, etc.
Since every Act is self-contained they can all perform in parallel.
However, when acts conflict (e.g. Walk Act & Run Act) or depend on other acts (e.g. Reload Act & Shoot Act) there are 2 mechanisms to resolve these: Blocking & Prologuing
Each act can block certain other acts when it performs. An act that is blocked cannot perform nor block any other act until it is unblocked. There are 2 types of blocks:
-
Persistent Block: This type of block remains persistent on the blockee till the blocker act has completed its perform.
e.g. IfSword Slash Actpersistent blocksMove Actthen the player cannot move till the sword slash has completed. -
Interrupt Block: This type only interrupts the blockee act's perform when the blocker act starts its perform. The blockee act can then be performed again even if the blocker act is still performing.
e.g. IfReload Actinterrupt blocksAim Actthen aiming will stop when reloading starts but the player can press aim again mid reloading to cancel and start aiming again.
Each act can have prologue acts i.e. acts that need to be performed before the main act can perform.
In example 1, the arrow pointing from an act denotes "who is my prologue" therefore:
Bis a prologue ofACis a prologue ofBDis a prologue ofC
and as such, first D performs then C then B then finally A.

Also keep in mind that no acts within the same prologue chain can block each other.
Terminology Note:
- Acts that come before an act are called prologue acts (e.g.
Bis a prologue ofA)- Acts that come after an act are called epilogue acts (e.g.
Cis an epilogue ofD)- Acts that are at the top of the chain are called top epilogue acts (e.g.
Ais a top epilogue ofB,C,D)
An act can have more than one prologue
In such a case both prologues work in parallel and only when both have finished does the act perform.
So in example 2, first C & D perform together in parallel then B then A.

An act can have more than one epilogue
In such a case both epilogues will wait for the prologue to complete first before performing themselves.
So in example 3, first D will perform then C will perform then A & B will perform together in parallel.

Cyclic prologues are not allowed
An act cannot be a prologue of itself or of any descendants. Therefore example 4 is invalid.

Note:
All examples are in Unity but the same concepts applies to other engines as well.
For examples in other engines take a look at Game Engine Specific ImplementationsIt is also recommended that you go through the documentation first for your desired engine specific implementation.
Creating your first act
Create a class inheriting from Act class and override the Enter() method like this:
public class MyFirstAct : Act
{
protected override Outcome Enter()
{
Debug.Log("Hello World!");
return Outcome.Success;
}
}Then initialize it and use it by invoking Perform() from wherever you'd like:
void Awake()
{
// Initialize
MyFirstAct myFirstAct = new();
myFirstAct.Init();
// Use
myFirstAct.Perform();
}That's it! Congrats you've just created & performed your first act.
How to use an act
The act follows this perform lifecycle:
You can implement the desired behaviour by overriding these methods:
Setup(): Where your one time initialization setup logic lives.CanPerform(): Define conditions that allow/disallow performingEnter(): Where the actual core logic lives on each perform. You have the following return options:Outcome.Success: Return this if core behaviour was successfully completed.Outcome.Pending: Return this if you don't want to immediately exit.Outcome.Failure: Return this if core behaviour failed to complete.Outcome.Retry: Return this if you want the act to perform again.
Tick()/PhysicsTick()/LateTick(): Incase the core logic needs continious ticking updates while performingExit(): Used for cleanup after core logic on each performCleanup(): Where your deinitialization teardown logic lives.
You can also assign these properties in Setup() to further finetune your behaviour:
_canReperform: Set true if act is allowed to perform again while already ongoing._tickFlags: The type of ticking (if any) while performing
Ticking an act
To make an act tick you need to do 3 things:
- Make sure the act has been assigned a theater
- Assign
_tickFlagsin setup - Return
Outcome.PendinginEnter()
public class MyTickAct : Act
{
// Private
private int _tickCounter = 0;
protected override void Setup()
{
_tickFlags = TickFlags.Tick;
}
protected override Outcome Enter(){
return Outcome.Pending;
}
protected override Outcome Tick()
{
// Tick 5 times then exit
_tickCounter++;
if (5 <= _tickCounter)
{
return Outcome.Success;
}
return Outcome.Pending;
}
}Delaying exit in act
To delay exit without using tick for something like a timer simply return Outcome.Pending and make sure no tick flag is assigned.
public class MyTimerAct : Act
{
private Coroutine waitCoroutine;
private IEnumerator WaitRoutine()
{
yield return new WaitForSeconds(5.0f); // Wait for 5 seconds then exit
Finish(Outcome.Success);
}
protected override Outcome Enter()
{
waitCoroutine = GetTheater().StartCoroutine(WaitRoutine());
return Outcome.Pending;
}
protected override void Exit()
{
if (waitCoroutine != null)
{
GetTheater().StopCoroutine(waitCoroutine);
waitCoroutine = null;
}
}
}Blocking in act
You can assign which acts to block as such:
void Awake()
{
// Initialize
MyAct myAct = new();
myAct.Init();
MyBlockingAct myBlockingAct = new();
myBlockingAct.AddToBlock(new() { myAct });
myBlockingAct.Init();
// Use
myBlockingAct.Perform();
myAct.Perform(); // Will fail till myBlockingAct perform has completed
}You can also directly disable an act:
void Awake()
{
// Initialize
MyAct myAct = new();
myAct.Init();
// Use
myAct.SetEnabled(false);
myAct.Perform(); // Will fail since act is disabled
myAct.SetEnabled(true);
myAct.Perform(); // Will work since act has been re-enabled
}Prologuing in act
You can assign which acts to prologue as such:
void Awake()
{
// Initialize
MyAct myAct = new();
myAct.Init();
MyMainAct myMainAct = new();
myMainAct.prologue = (Act act) => new() { myAct };
myMainAct.Init();
// Use
myMainAct.Perform(); // This will perform myAct first then myMainAct
}If you have multiple acts you want to chain in prologue sequence you can also use Seq() as such:
void Awake()
{
// Initialize
Act actA = new();
actA.Init();
Act actB = new();
actB.Init();
Act actC = new();
actC.Init();
Act actD = new();
actD.Init();
MyMainAct myMainAct = new();
myMainAct.prologue = (Act act) => Act.Seq(new() {
new() { actA },
new() { actB, actC },
new() { actD }
});
myMainAct.Init();
// Use
myMainAct.Perform(); // Order of perform: actA -> actB & actC (in parallel) -> actD
}Using a theater
A Theater can be used if you want to organize & manage your acts together.
[SerializeField] Theater theater;
void Awake()
{
// Initialize
Act actA = new();
actA.Init("My Act A", theater);
Act actB = new();
actB.Init("My Act B", theater);
// Uses
theater.SetEnabled(false); // If a theater is disabled all it's acts will be disabled as well
theater.IsEnabled();
theater.AbortAll();
theater.AreAnyOngoing();
theater.GetAllActs();
}Here's what a simple top down game player looks like in unity:
using System;
using UnityEngine;
public class Player : MonoBehaviour
{
// Act Properties
[SerializeField] Theater theater;
[SerializeField] MoveAct moveAct = new();
[SerializeField] ShootAct shootAct = new();
[SerializeField] AimAct aimAct = new();
// Override Methods
void Update()
{
// Move
float horizontalInput = Input.GetAxisRaw("Horizontal");
float verticalInput = Input.GetAxisRaw("Vertical");
moveAct.direction = new Vector2(horizontalInput, verticalInput).normalized;
moveAct.Perform();
// Aim towards mouse pointer
Vector3 mouseWorldPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
aimAct.targetRotation = aimAct.RotationTowardsPosition(mouseWorldPosition);
aimAct.Perform();
// Shoot
if (Input.GetMouseButtonDown(0))
{
Vector2 mouseWorldPos2D = mouseWorldPosition;
Vector2 playerPos2D = transform.position;
shootAct.direction = (mouseWorldPos2D - playerPos2D).normalized;
shootAct.Perform();
}
}
void Awake()
{
// Setup acts
theater = GetComponent<Theater>();
aimAct.turnSpeed = 5.0f;
aimAct.Init("Aim Act", theater);
moveAct.Init("Move Act", theater);
shootAct.Init("Shoot Act", theater);
}
}Move Act Class
[Serializable]
public class MoveAct : Act
{
// Public Properties
[SerializeField] public float speed = 5f;
[SerializeField] public bool useBorder = false;
[SerializeField] public Rect border = new Rect(-10f, -10f, 20f, 20f);
[HideInInspector] public Vector2 direction = new();
[HideInInspector] public Rigidbody2D rb;
// Override Methods
protected override void Setup()
{
if (rb == null)
{
rb = GetOwner().GetComponent<Rigidbody2D>();
}
rb.gravityScale = 0f; // No gravity for top down
}
protected override bool CanPerform()
{
return rb != null;
}
protected override Outcome Enter()
{
Vector2 nextPosition = rb.position + direction * speed * GetDelta();
if (useBorder)
{
nextPosition.x = Mathf.Clamp(nextPosition.x, border.xMin, border.xMax);
nextPosition.y = Mathf.Clamp(nextPosition.y, border.yMin, border.yMax);
}
rb.MovePosition(nextPosition);
return Outcome.Success;
}
protected override void Exit()
{
direction = Vector2.zero;
}
}Shoot Act Class
[Serializable]
public class ShootAct : Act
{
// Public Properties
[SerializeField] public GameObject projectilePrefab;
[HideInInspector] public Vector2 spawnLocation = new();
[HideInInspector] public bool spawnAtOwner = true;
[HideInInspector] public Vector2 direction = new();
// Private Methods
private void SpawnBullet()
{
// Spawn Bullet
var spawnPosition = spawnAtOwner ? GetOwner().transform.position : (Vector3)spawnLocation;
GameObject bullet = MonoBehaviour.Instantiate(projectilePrefab, spawnPosition, Quaternion.identity);
// Set bullet direction and owner
ProjectileBase bulletScript = bullet.GetComponent<ProjectileBase>();
bulletScript.direction = direction;
bulletScript.SetOwner(GetOwner());
}
// Override Methods
protected override bool CanPerform()
{
return projectilePrefab != null;
}
protected override Outcome Enter()
{
SpawnBullet();
return Outcome.Success;
}
protected override void Exit()
{
// Reset state
direction = Vector2.zero;
}
}Aim Act Class
[Serializable]
public class AimAct : Act
{
// Public Properties
[SerializeField] public float targetRotation = 0f;
[SerializeField] public float turnSpeed = 150f;
[HideInInspector] public Rigidbody2D rb;
// Public Method
public float RotationTowardsPosition(Vector2 position)
{
Vector2 direction = (Vector2)position - rb.position;
return Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
}
// Private Methods
private float CalcRotationLerp(float goalRotation, float deltaTime)
{
return Mathf.MoveTowardsAngle(rb.rotation, goalRotation, turnSpeed * deltaTime);
}
// Override Methods
protected override void Setup()
{
_canReperform = true;
// Auto get rigidBody if not provided
if (rb == null)
{
rb = GetOwner().GetComponent<Rigidbody2D>();
}
// Enable ticking
_tickFlags = TickFlags.PhysicsTick;
}
protected override bool CanPerform()
{
// Fail if no rigidbody assigned
if (rb == null)
{
WriteLog("Failed to perform, No rigidbody found!");
return false;
}
return true;
}
protected override Outcome PhysicsTick()
{
rb.MoveRotation(CalcRotationLerp(targetRotation, GetPhysicsDelta()));
return Outcome.Pending;
}
}If this has been useful in your projects consider supporting its development.
Any support motivates to keep the project well maintained, documented and growing.
MIT © Manas Ravindra Makde