Skip to content

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

🎭 Act

Status: Alpha License

A game programming pattern for creating and managing complex behaviors with parallelism at its core.

⚙️ Game Engine Specific

Need a port for a different game engine? Open a feature request to let the developer know.

💡 Principle

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

1. Blocking

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. If Sword Slash Act persistent blocks Move Act then 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. If Reload Act interrupt blocks Aim Act then aiming will stop when reloading starts but the player can press aim again mid reloading to cancel and start aiming again.

2. Prologuing

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:

  • B is a prologue of A
  • C is a prologue of B
  • D is a prologue of C

and as such, first D performs then C then B then finally A.
prologue chain example

Also keep in mind that no acts within the same prologue chain can block each other.

Terminology Note:

  1. Acts that come before an act are called prologue acts (e.g. B is a prologue of A)
  2. Acts that come after an act are called epilogue acts (e.g. C is an epilogue of D)
  3. Acts that are at the top of the chain are called top epilogue acts (e.g. A is a top epilogue of B, 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.
prologue branched example

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.
prologue merging example

Cyclic prologues are not allowed

An act cannot be a prologue of itself or of any descendants. Therefore example 4 is invalid.
prologue cyclic incorrect example

🧭 Usage

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 Implementations

It 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:

act lifecycle

You can implement the desired behaviour by overriding these methods:

  1. Setup(): Where your one time initialization setup logic lives.
  2. CanPerform(): Define conditions that allow/disallow performing
  3. Enter(): 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.
  4. Tick()/PhysicsTick()/LateTick(): Incase the core logic needs continious ticking updates while performing
  5. Exit(): Used for cleanup after core logic on each perform
  6. Cleanup(): Where your deinitialization teardown logic lives.

You can also assign these properties in Setup() to further finetune your behaviour:

  1. _canReperform: Set true if act is allowed to perform again while already ongoing.
  2. _tickFlags: The type of ticking (if any) while performing
Ticking an act

To make an act tick you need to do 3 things:

  1. Make sure the act has been assigned a theater
  2. Assign _tickFlags in setup
  3. Return Outcome.Pending in Enter()
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();
}

🗺️ Example

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;
    }
}

❤️ Sponsors

If this has been useful in your projects consider supporting its development.
Any support motivates to keep the project well maintained, documented and growing.

🔑 License

MIT © Manas Ravindra Makde

About

A game programming pattern for creating and managing complex behaviors with parallelism at its core.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Sponsor this project

Contributors