LSCS: Custom Behaviour & API
The Large Scale Combat System is built on a highly modular, Scriptable Object-based architecture. This allows you to create entirely new unit behaviors and AI tactics without modifying the core manager scripts. This guide explains the key interfaces and classes you can inherit from to create your own custom logic.
1. CombatBehaviourSO
This Scriptable Object defines the individual state machine and actions of a single Unit. It answers the question: „What should I be doing right now?“
How to Create a Custom Behaviour:
- Create a new C# script.
- Have it inherit from
CombatBehaviourSO. - Add the
[CreateAssetMenu]attribute to make it creatable in the Project window. - Override the abstract methods to implement your custom logic.
Example:
C#
using UnityEngine;
namespace TopsonGames
{
[CreateAssetMenu(fileName = "MyCustomBehaviour", menuName = "Combat Behaviours/Custom")]
public class MyCustomBehaviour : CombatBehaviourSO
{
public override void TickCombat(Unit unit, float deltaTime, Coroutine movementRoutine)
{
// Custom combat logic here...
}
// ... implement other abstract methods
}
}
Key Methods to Override:
public abstract void InitializeUnit(Unit unit, Formation formation);- Purpose: Called once when a unit is spawned. Use this to set initial values, like randomizing the
attackTimer.
- Purpose: Called once when a unit is spawned. Use this to set initial values, like randomizing the
public abstract void TickIdle(Unit unit, NavMeshAgent agent, Formation formation, Coroutine movementRoutine);- Purpose: Called every frame a unit’s
currentStateisIdle. Use this to define what a unit does when it has no orders, such as moving back to its waypoint.
- Purpose: Called every frame a unit’s
public abstract void TickMovement(Unit unit, NavMeshAgent agent, Formation formation, Coroutine movementRoutine);- Purpose: Called every frame a unit’s
currentStateisMoving. This typically handles the logic for checking if the unit has arrived at its destination.
- Purpose: Called every frame a unit’s
public abstract void TickCombat(Unit unit, float deltaTime, Coroutine movementRoutine);- Purpose: The core of the combat logic. Called every frame a unit’s
currentStateisFighting. This is where you should check for attack range, manage attack cooldowns, and trigger attack animations via theanimatorLink.
- Purpose: The core of the combat logic. Called every frame a unit’s
public abstract void OnMovementTick(Unit unit);- Purpose: Called continuously by a coroutine while a unit is in the
Fightingstate. This is ideal for complex repositioning logic, such as cavalry circling a target or infantry pushing forward.
- Purpose: Called continuously by a coroutine while a unit is in the
All Methods to Override:
- public abstract void InitializeUnit(Unit unit, Formation formation);
- public abstract void TickCombat(Unit unit, float deltaTime, Coroutine movementRoutine);
- public abstract void TickIdle(Unit unit, NavMeshAgent agent, Formation formation, Coroutine movementRoutine);
- public abstract void TickMovement(Unit unit, NavMeshAgent agent, Formation formation, Coroutine movementRoutine);
- public abstract bool ShouldEngage(Unit self, Unit potentialTarget);
- public abstract void OnMovementTick(Unit unit);
- public abstract Unit OnFindClosestEnemy(Unit unit, Formation formation);
- public abstract void DrawGizmosOnBehaviour(Formation formation);
- public abstract void OnUpdateUnit(Unit unit, Formation formation);
- public abstract void OnUpdateFormation(Formation formation);
- public abstract void InitializeFormation(Formation formation);
- public abstract void OnReportArrows(Formation formation);
- public abstract void OnReportArrowsUnit(Unit unit, Coroutine arrowRoutine);
- public abstract bool IsFacingTarget(Unit unit, Transform target, float angleThreshold = 30f);
- public abstract void OnTakeDamage(float Damage, Unit unit, Unit attacker, bool shieldHit = false);
- public abstract void OnDeath(Unit unit, Unit attacker, Formation formation);
2. CombatPlacementSO
This Scriptable Object defines how an entire Formation of units behaves and repositions its waypoints while it is engaged in combat.
How to Create a Custom Placement Behaviour:
Create a new C# script that inherits from CombatPlacementSO.
Example:
C#
using UnityEngine;
namespace TopsonGames
{
[CreateAssetMenu(fileName = "MyPlacementBehaviour", menuName = "Combat Placement/Custom")]
public class MyCustomPlacementSO : CombatPlacementSO
{
public override void TickUpdateEngagement(Formation formation, Formation target)
{
// Logic to update the waypoints of all units in 'formation'
}
}
}
Key Method to Override:
public abstract void TickUpdateEngagement(Formation formation, Formation target);- Purpose: This is the main update loop for an engaged formation. It’s called every frame by the
Formation’sUpdateEngagementmethod. Use this to implement flocking (Boids), line-holding, or envelopment maneuvers by directly manipulating theunit.Waypoint.transform.positionof each unit within the formation.
- Purpose: This is the main update loop for an engaged formation. It’s called every frame by the
3. AITacticSO
This Scriptable Object defines a single, modular tactical option for the AICommander. It is the core of the AI’s decision-making process.
How to Create a Custom Tactic:
Create a new C# script that inherits from AITacticSO.
Example:
C#
using UnityEngine;
using System.Collections.Generic;
namespace TopsonGames.AI
{
[CreateAssetMenu(fileName = "Tactic_MyTactic", menuName = "AI/Tactic/My Custom Tactic")]
public class MyCustomTactic : AITacticSO
{
public override float Evaluate(AICommander commander, List<Formation> allEnemies, List<Formation> availableFormations)
{
// Return a score indicating how good this tactic is right now
return 50f;
}
public override void Execute(AICommander commander, List<Formation> allEnemies, List<Formation> availableFormations)
{
// Issue commands to formations here
}
}
}
Key Methods to Override:
public abstract float Evaluate(AICommander commander, List<Formation> allEnemies, List<Formation> availableFormations);- Purpose: The „thinking“ part of the tactic. This method should analyze the battlefield (using the provided lists of friendly and enemy formations) and return a score. The
AICommanderwill execute the tactic with the highest score.
- Purpose: The „thinking“ part of the tactic. This method should analyze the battlefield (using the provided lists of friendly and enemy formations) and return a score. The
public abstract void Execute(AICommander commander, List<Formation> allEnemies, List<Formation> availableFormations);- Purpose: The „acting“ part. If this tactic is chosen, this method is called. This is where you issue commands to your formations, typically by finding the right units from
availableFormationsand callingSetCustomTarget()on them. Remember to callcommander.CommitFormationsToAction()at the end to mark the units as busy.
- Purpose: The „acting“ part. If this tactic is chosen, this method is called. This is where you issue commands to your formations, typically by finding the right units from
1. RangedWeaponSO
This Scriptable Object defines the behavior of a specific type of ranged attack. It is responsible for calculating the trajectory and velocity of a projectile. By creating different RangedWeaponSO assets, you can have archers fire high-arcing volleys, crossbows fire flat bolts, or catapults hurl stones.
How to Create a Custom Ranged Weapon:
Create a new C# script that inherits from RangedWeaponSO.
Example:
C#
using UnityEngine;
namespace TopsonGames
{
[CreateAssetMenu(fileName = "MyCustomProjectileWeapon", menuName = "Ranged Weapon/Custom")]
public class MyCustomProjectileWeaponSO : RangedWeaponSO
{
public override void Attack(RangedWeapon rangedWeapon, Unit currentTarget, Formation formation, Formation parentFormation)
{
// Your custom trajectory calculation and projectile firing logic goes here.
}
}
}
Key Method to Override:
public abstract void Attack(RangedWeapon rangedWeapon, Unit currentTarget, Formation targetFormation, Formation parentFormation);- Purpose: This is the core method that is called when a ranged unit performs an attack. Your implementation should contain all the logic to calculate the projectile’s path and initial velocity.
- Parameters:
rangedWeapon: TheRangedWeaponMonoBehaviour component on the unit’s weapon model. Use this to access theShootPointtransform and the projectile pool viarangedWeapon.GetNextArrow().currentTarget: The specificUnitthe individual soldier is aiming at. Useful for predictive targeting based on the target’s velocity.targetFormation: TheFormationthat thecurrentTargetbelongs to. Useful for calculating area-of-effect or volley-style attacks.parentFormation: TheFormationthat is initiating the attack.
2. VisualizerSO
This Scriptable Object defines a custom debug visualization that can be drawn in the Scene View when a formation is highlighted or selected. It works by manipulating a LineRenderer component. You can use this to draw attack ranges, movement paths, or any other tactical information.
How to Create a Custom Visualizer:
Create a new C# script that inherits from VisualizerSO.
Example:
C#
using UnityEngine;
namespace TopsonGames
{
[CreateAssetMenu(fileName = "MyCustomVisualizer", menuName = "Visualizer/Custom")]
public class MyCustomVisualizerSO : VisualizerSO
{
public override void TickVisualize(CombatBehaviourSO combatBehaviourSO, Formation formation, LineRenderer lineRenderer)
{
// Your custom logic to set the points of the LineRenderer goes here.
}
}
}
Key Method to Override:
public abstract void TickVisualize(CombatBehaviourSO combatBehaviourSO, Formation formation, LineRenderer lineRenderer);- Purpose: This method is called by the
Visualizer.cscomponent every frame that it is active. Your implementation should calculate the desired points for the line and apply them to the providedlineRenderer. - Parameters:
combatBehaviourSO: TheCombatBehaviourSOof the formation. You can use this to get contextual data likeattackRange.formation: TheFormationbeing visualized. You can use this to get its center, its units, its target, etc.lineRenderer: TheLineRenderercomponent that you will use to draw your visualization. You can set its points vialineRenderer.SetPositions().
- Purpose: This method is called by the
Formation.cs API:
List<Unit> GetUnits(): Returns the list of allUnitcomponents in this formation.Vector3 CalculateUnitCenter(): Calculates the average world position of all units.Quaternion CalculateAverageRotation(): Calculates the average forward-facing direction of all units.void SetCustomTarget(Formation enemy): Issues a high-level command for this formation to attack a specific enemy formation.void SetMoveOrder(bool isCustomTargetMove): Initiates a movement state for the formation.void Disengage(): Resets the formation’s state fromEngagedback toIdle.- Properties like
CurrentState,CurrentCombatState, andcustomEnemycan be read to understand the formation’s status. public FormationState CurrentState;- Purpose: The current state of the formation’s high-level state machine (
Idle,MovingToWaypoint,Engaged). This is the primary driver of the formation’s behavior.
- Purpose: The current state of the formation’s high-level state machine (
public CombatState CurrentCombatState;- Purpose: Defines the current combat style of the formation (
MeleeorArcher). This is particularly useful for hybrid units like archers who can switch to melee.
- Purpose: Defines the current combat style of the formation (
public Formation customEnemy;- Purpose: The specific enemy
Formationthat this formation has been commanded to attack by the player or AI. This takes priority over targets of opportunity.
- Purpose: The specific enemy
public Formation ArcherTarget;- Purpose: A specific variable used by the
ArcherCombatSOto store its chosen target formation for the next volley.
- Purpose: A specific variable used by the
public int numberOfUnits;- Purpose: The current number of active units in the formation. This is updated automatically.
public Transform WaypointCenter;- Purpose: The central ‚brain‘ or anchor point of the formation. The formation grid is arranged around this transform, and it’s what moves along the NavMesh path during a move order.
public Transform WaypointIndicator;- Purpose: The parent transform for all the individual unit
WaypointIndicatorobjects. This is used by theFormationControllerto show the user where the formation will be when dragging a move order.
- Purpose: The parent transform for all the individual unit
public int currentArrows;- Purpose: For archer formations, this tracks the remaining ammunition. It is initialized by the
ArcherCombatSO.
- Purpose: For archer formations, this tracks the remaining ammunition. It is initialized by the
public float archerDetectionTimer;- Purpose: A timer used by the
ArcherCombatSOto control how often it scans for new targets.
- Purpose: A timer used by the
public float currentTimeBetweenShots;- Purpose: A timer used by the
ArcherCombatSOto manage the cooldown between volleys.
- Purpose: A timer used by the
public float engagementRecalculationTimer;- Purpose: A general-purpose timer, often used by
CombatPlacementSOs to control how frequently the formation’s positions are recalculated during combat.
- Purpose: A general-purpose timer, often used by
public ArmyData armyData;- Purpose: A reference to the
ArmyDatafrom theArmyScriptable Object that this formation was spawned from. It contains the original and current troop count (Troops).
- Purpose: A reference to the
public bool isFreeShooting = true;- Purpose: A flag that controls the archer AI’s behavior. If
true, archers will attack any valid target in range. Iffalse, they will only attack their assignedcustomEnemy. Note that it is also set byFormationUI
- Purpose: A flag that controls the archer AI’s behavior. If
public bool isDefender = false;
Unit.cs API:
Unit GetCurrentTarget(): Returns the specific enemyUnitthis unit is currently fighting.void SetCurrentTarget(Unit unit): Assigns a specific enemyUnitto fight.Unit GetClosestEnemy(): Returns the closest enemyUnit, as calculated by theGameManager.void MoveTo(Vector3 destination): Commands the unit’sNavMeshAgentto move to a position.void StopMovement(): Stops theNavMeshAgentand sets the unit’s state toIdle.public Vector3 lastTargetPosition;- Purpose: Stores the last known position of the unit’s
currentTarget. Useful inOnMovementTickto prevent issuing a newMoveTocommand every frame if the target has only moved a small amount.
- Purpose: Stores the last known position of the unit’s
public float attackTimer;- Purpose: The cooldown timer for attacks. The
TickCombatmethod in aCombatBehaviourSOis typically responsible for decrementing this and triggering an attack when it reaches zero.
- Purpose: The cooldown timer for attacks. The
public float findEnemyTimer;- Purpose: A general-purpose timer, often used by
CombatBehaviourSOs to control how frequently a unit scans for new enemies when it doesn’t have acurrentTarget.
- Purpose: A general-purpose timer, often used by
public float switchAnimationTimer;- Purpose: A timer used by
OnUpdateUnitinCombatBehaviourSOs to control how often a unit can switch to a different idle animation, adding visual variety.
- Purpose: A timer used by
public int currentIdleAnimation;- Purpose: The index of the idle animation that the
AnimatorLinkshould tell the Animator to play.
- Purpose: The index of the idle animation that the
public bool hasAppliedKnockback;- Purpose: A state flag to ensure that a single attack (like from a cavalry charge) only applies its knockback effect once per animation.
public int lineInformation;- Purpose: Stores which row (1 being the front row) the unit is in within its formation. This value is set automatically by
Formation.ArrangeFormationand can be used to create special rules (e.g., „only the first two rows of archers can fire“).
- Purpose: Stores which row (1 being the front row) the unit is in within its formation. This value is set automatically by
FormationController.cs API:
List<Formation> GetFormations(): Returns all in-scene FormationsList<Formation> GetSelectedFormations(): Returns all selected Player Formationsvoid ClearSelectedFormations(): Clears all selected Player Formationsvoid AddSelectedFormation(Formation formation): Adds formation to selected Player Formations