Location: Samples/06 State Machines/04 Weapons
Recommended After: Brains
Learning Outcomes: in this sample you will learn:
How to allow a character to equip different weapons.
How have each weapon define its own animation details for the character to use.
How to use Input Buffers.
Summary
This sample expands upon the Brains sample to allow a character hold various weapons which each have their own set of animations. It only uses this system for Attack, Equip, and UnEquip animations, but the concept could be extended to other animations such as having a different Idle or Walk animation to hold the weapon correctly.
- Animancer doesn't know or care where your animations are defined, so there's no need for all of them to be defined on the character.
- In this case, a weapon prefab holds the animations that a character will use when they equip it.
- Input Buffering can help make a game feel more responsive to the player by accepting button presses that were slightly too early for the corresponding actions to be performed.
Controls
- WASD = Walk
- WASD + Left Shift = Run
- Left Click = Attack
- Right Click = Change Weapon
Licensing
The top video was made using the scripts from this sample on a character called Paladin J Nordstrom from Mixamo along with various animations. Unfortunately, those assets can't be legally redistributed in their raw source form, meaning that they can't be included in Animancer. So instead, the actual sample scene reuses the props and animations from earlier samples. This also avoids needing a large number of animations which would only be used in this one sample.
Overview
The general code structure is similar to the Brains sample, with a few changes:
MovingCharacterBrain
has been replaced byWeaponsCharacterBrain
.ActionState
has been replaced byEquipState
andAttackState
which don't reference their animations directly. Instead, they get their animations from the currently equippedWeapon
.
Weapon
The main purpose of this sample is to demonstrate how animations can be defined separately from a character. That's what the Weapon
class is for, it holds the animations relating to a particular weapon so that when a character equips it they can play appropriate animations for it:
using Animancer;
using UnityEngine;
public class Weapon : MonoBehaviour
{
It has an array of animations for the Attack State to use in sequence if the character attacks repeatedly:
[SerializeField]
private TransitionAsset[] _AttackAnimations;
public TransitionAsset[] AttackAnimations => _AttackAnimations;
And animations for the Equip State to use when changing to this weapon and before changing to another weapon:
[SerializeField]
private TransitionAsset _EquipAnimation;
public TransitionAsset EquipAnimation => _EquipAnimation;
[SerializeField]
private TransitionAsset _UnequipAnimation;
public TransitionAsset UnequipAnimation => _UnequipAnimation;
}
In a real game, this class might have other details like damage, damage type, weapon category, etc. It could also Inherit from a base Item class for things like weight, cost, and description.
Weapon Prefabs
Implementing Weapon
as a component means that each one can be set up along with its model as a prefab. This offers great flexibility in how they can be used, such as to have a character pick up or drop a weapon or to dynamically load individual weapons when they are needed instead of directly referencing all available weapons at once (which would mean they are all loaded at all times).
But all that is beyond the scope of this sample. Here we're just showing how animations can be defined outside of the logical state that uses them so the weapons are just objects in the scene and the Weapons Character Brain references them all in an array.
Animancer Weapons
The weapons in the sample scene are a bit limited because they're only reusing assets from other samples. They all use Humanoid-Flinch as their Equip Animation
and have no Unequip Animation
.
Unarmed doesn't have any attacks:
Pistol has one attack using Humanoid-PistolShoot:
Golf Club has an attack using Humanoid-GolfSwing then Humanoid-PistolShoot (it looks silly, but it at least shows how one attack animation chains into the next if you attack repeatedly):
Mixamo Weapons
The Mixamo weapons used for the first video are a bit more detailed.
Unarmed has two attacks but no equip or unequip animation:
Club has three attacks and one animation for both equip and unequip:
Great Sword has three attacks and separate animations for equip and unequip:
Equipment
The Character
class has a reference to an Equipment
component:
class Character
{
...
[SerializeField]
private Equipment _Equipment;
public Equipment Equipment => _Equipment;
}
The Equipment
script has a reference to the Transform
of the RightHand bone so it can re-parent weapons to that bone when they are equipped:
public class Equipment : MonoBehaviour
{
[SerializeField] private Transform _WeaponHolder;
It also has a reference to the currently equipped Weapon
with a public
wrapper property:
[SerializeField] private Weapon _Weapon;
public Weapon Weapon
{
get => _Weapon;
When the Weapon
is changed, it detaches the old weapon from the _WeaponHolder
and attaches the new one to it:
set
{
DetachWeapon();
_Weapon = value;
AttachWeapon();
}
}
And it also needs to attach the current weapon on startup:
protected virtual void Awake()
{
AttachWeapon();
}
Attaching a weapon involves setting the _WeaponHolder
as its parent
, clearing its local Transform
values, and activating its GameObject
:
private void AttachWeapon()
{
if (_Weapon == null)
return;
Transform transform = _Weapon.transform;
transform.parent = _WeaponHolder;
transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
transform.localScale = Vector3.one;
_Weapon.gameObject.SetActive(true);
}
And detaching a weapon is even simpler, it just sets this Equipment
script's Transform
as its parent
and deactivates its GameObject
:
private void DetachWeapon()
{
if (_Weapon == null)
return;
_Weapon.transform.parent = transform;
_Weapon.gameObject.SetActive(false);
}
}
In a real game, attaching and detaching a weapon might involve instantiating and destroying a prefab or leaving it active in the scene.
Equip State
The EquipState
script is a bit more complex than the states in earlier samples because it handles both the unequipping and equipping animations:
using Animancer;
using System;
using UnityEngine;
public class EquipState : CharacterState
{
public Weapon NextWeapon { get; set; }
public Weapon CurrentWeapon
=> Character.Equipment.Weapon;
public override CharacterStatePriority Priority
=> CharacterStatePriority.Medium;
protected virtual void Awake()
{
NextWeapon = CurrentWeapon;
}
public override bool CanEnterState
=> !enabled
&& NextWeapon != CurrentWeapon;
protected virtual void OnEnable()
{
if (CurrentWeapon.UnequipAnimation.IsValid())
{
AnimancerState state = Character.Animancer.Play(CurrentWeapon.UnequipAnimation);
state.Events(this).OnEnd ??= OnUnequipEnd;
}
else
{
OnUnequipEnd();
}
}
private void OnUnequipEnd()
{
Character.Equipment.Weapon = NextWeapon;
if (CurrentWeapon.EquipAnimation.IsValid())
{
AnimancerState state = Character.Animancer.Play(CurrentWeapon.EquipAnimation);
state.Events(this).OnEnd = Character.StateMachine.ForceSetDefaultState;
}
else
{
Character.StateMachine.ForceSetDefaultState();
}
}
}
It has two Weapon
properties:
- The
NextWeapon
needs to be set by whatever script tries to enter this state (in this sample, that's the Brain). - The
CurrentWeapon
is just a shortcut to get the current weapon from theEquipment
script since it will be used several times throughout this script.
public Weapon NextWeapon { get; set; }
public Weapon CurrentWeapon
=> Character.Equipment.Weapon;
protected virtual void Awake()
{
NextWeapon = CurrentWeapon;
}
This state can only be entered if it isn't already in progress (it can't interrupt itself) and if the NextWeapon
has been set to something different from the CurrentWeapon
:
public override bool CanEnterState
=> !enabled
&& NextWeapon != CurrentWeapon;
When entering this state, if the CurrentWeapon
(the previously equipped weapon) has a valid UnequipAnimation
, play it and tell it to use the cached event, but if there is no animation just pretend it ended immediately:
protected virtual void OnEnable()
{
if (CurrentWeapon.UnequipAnimation.IsValid())
{
AnimancerState state = Character.Animancer.Play(CurrentWeapon.UnequipAnimation);
state.Events(this).OnEnd ??= OnUnequipEnd;
}
else
{
OnUnequipEnd();
}
}
When the UnequipAnimation
ends (or is skipped), change the actual equipped weapon:
private void OnUnequipEnd()
{
Character.Equipment.Weapon = NextWeapon;
Then do basically the same as before with the new weapon's EquipAnimation
. This time when the animation it uses ForceSetDefaultState
to return the character to Idle:
if (CurrentWeapon.EquipAnimation.IsValid())
{
AnimancerState state = Character.Animancer.Play(CurrentWeapon.EquipAnimation);
state.Events(this).OnEnd = Character.StateMachine.ForceSetDefaultState;
}
else
{
Character.StateMachine.ForceSetDefaultState();
}
}
And finally, this state has Medium
priority to control what can Interrupt it:
public override CharacterStatePriority Priority
=> CharacterStatePriority.Medium;
Attack State
The AttackState
script plays animations from current weapon's AttackAnimations
array. When entered it starts with the first attack, but if re-entered while the previous attack is still fading out it plays the next attack in the array:
using Animancer;
using UnityEngine;
public class AttackState : CharacterState
{
private int _AttackIndex = int.MaxValue;
private AnimancerState _CurrentState;
public Weapon Weapon
=> Character.Equipment.Weapon;
public override bool CanEnterState
=> Weapon != null
&& Weapon.AttackAnimations.Length > 0;
public override CharacterStatePriority Priority
=> CharacterStatePriority.Medium;
protected virtual void OnEnable()
{
if (ShouldRestartCombo)
_AttackIndex = 0;
else
_AttackIndex++;
TransitionAsset animation = Weapon.AttackAnimations[_AttackIndex];
_CurrentState = Character.Animancer.Play(animation);
_CurrentState.Events(this).OnEnd ??= Character.StateMachine.ForceSetDefaultState;
}
private bool ShouldRestartCombo
=> _AttackIndex >= Weapon.AttackAnimations.Length - 1
|| _CurrentState == null
|| _CurrentState.Weight == 0;
}
This state uses a simple int
to keep track of which attack it's up to in the array.
- Each time the state is entered it either restarts from
0
or increments to the index. - It also remembers the
AnimancerState
it played so the next attempt to enter this state can check if that animation is still active.
private int _AttackIndex = int.MaxValue;
private AnimancerState _CurrentState;
protected virtual void OnEnable()
{
if (ShouldRestartCombo)
_AttackIndex = 0;
else
_AttackIndex++;
TransitionAsset animation = Weapon.AttackAnimations[_AttackIndex];
_CurrentState = Character.Animancer.Play(animation);
_CurrentState.Events(this).OnEnd ??= Character.StateMachine.ForceSetDefaultState;
}
There are two things that will cause it to restart:
- If the index is already at the last animation.
- Or if the previous animation has already faded out fully (or hasn't been initialized yet).
private bool ShouldRestartCombo
=> _AttackIndex >= Weapon.AttackAnimations.Length - 1
|| _CurrentState == null
|| _CurrentState.Weight == 0;
And just like with the EquipState
, this state has Medium
priority to control what can Interrupt it:
public override CharacterStatePriority Priority
=> CharacterStatePriority.Medium;
Weapons Character Brain
The WeaponsCharacterBrain
script has the same movement code as MovingCharacterBrain
, but has a few other additions:
- It uses an
InputBuffer
so that if a button is pressed before the corresponding state change is allowed, it will continue attempting the same state change for a short time.
- It references the
EquipState
to change the equipped weapon.
- It has an array of
Weapon
s to cycle through. This would normally be managed by an inventory system.
InputBuffer
so that if a button is pressed before the corresponding state change is allowed, it will continue attempting the same state change for a short time.EquipState
to change the equipped weapon.Weapon
s to cycle through. This would normally be managed by an inventory system.
using Animancer.FSM;
using Animancer.Units;
using System;
using UnityEngine;
public class WeaponsCharacterBrain : MonoBehaviour
{
[SerializeField] private Character _Character;
[SerializeField] private CharacterState _Move;
[SerializeField] private CharacterState _Attack;
[SerializeField, Seconds] private float _InputTimeOut = 0.5f;
[SerializeField] private EquipState _Equip;
[SerializeField] private Weapon[] _Weapons;
private StateMachine<CharacterState>.InputBuffer _InputBuffer;
protected virtual void Awake()
{
_InputBuffer = new StateMachine<CharacterState>.InputBuffer(_Character.StateMachine);
}
protected virtual void Update()
{
UpdateMovement();
UpdateEquip();
UpdateAction();
_InputBuffer.Update();
}
private void UpdateMovement()
{
Vector2 input = SampleInput.WASD;
if (input != Vector2.zero)
{
Vector3 movementDirection = new Vector3(input.x, 0, input.y);
Transform camera = Camera.main.transform;
movementDirection = camera.TransformDirection(movementDirection);
_Character.Parameters.MovementDirection = movementDirection;
_Character.StateMachine.TrySetState(_Move);
}
else
{
_Character.Parameters.MovementDirection = Vector3.zero;
_Character.StateMachine.TrySetDefaultState();
}
_Character.Parameters.WantsToRun = SampleInput.LeftShiftHold;
}
private void UpdateEquip()
{
if (SampleInput.RightMouseDown)
{
int equippedWeaponIndex = Array.IndexOf(_Weapons, _Character.Equipment.Weapon);
equippedWeaponIndex++;
if (equippedWeaponIndex >= _Weapons.Length)
equippedWeaponIndex = 0;
_Equip.NextWeapon = _Weapons[equippedWeaponIndex];
_InputBuffer.Buffer(_Equip, _InputTimeOut);
}
}
private void UpdateAction()
{
if (SampleInput.LeftMouseDown)
{
_InputBuffer.Buffer(_Attack, _InputTimeOut);
}
}
}
Input Buffering
Input Buffering is a technique where a failed state change is re-attempted each frame after the initial input until it succeeds or a short timer elapses. This makes things easier for the player to control by giving them a larger window of time during which they can perform actions.
To use it in Animancer's Finite State Machine system, initialize an InputBuffer
with the StateMachine
it will control:
private StateMachine<CharacterState>.InputBuffer _InputBuffer;
protected virtual void Awake()
{
_InputBuffer = new StateMachine<CharacterState>.InputBuffer(_Character.StateMachine);
}
To perform an action, call Buffer
with the desired state and a "Time Out" value to determine how long the buffer will last (it could be hard coded, but being exposed in the Inspector makes it easy to tweak and test different values):
[SerializeField, Seconds] private float _InputTimeOut = 0.5f;
private void UpdateAction()
{
if (SampleInput.LeftMouseDown)
{
_InputBuffer.Buffer(_Attack, _InputTimeOut);
}
}
And finally, call Update
on the buffer to make another attempt to enter the state:
protected virtual void Update()
{
UpdateMovement();
UpdateEquip();
UpdateAction();
_InputBuffer.Update();
}
Update Equip
To equip the next weapon, it needs to first figure out which one is equipped:
[SerializeField] private EquipState _Equip;
[SerializeField] private Weapon[] _Weapons;
private void UpdateEquip()
{
if (SampleInput.RightMouseDown)
{
int equippedWeaponIndex = Array.IndexOf(_Weapons, _Character.Equipment.Weapon);
This script could have a field to store the index, but the equipped weapon isn't actually changing here (only if the EquipState
is entered and finishes playing the UnequipAnimation
) so that could cause a mismatch between the stored value and the weapon that's actually equipped.
The index is incremented and loops back to 0
if it goes past the last weapon in the array:
equippedWeaponIndex++;
if (equippedWeaponIndex >= _Weapons.Length)
equippedWeaponIndex = 0;
Then it sets the NextWeapon
and buffers the EquipState
to try to equip that weapon:
_Equip.NextWeapon = _Weapons[equippedWeaponIndex];
_InputBuffer.Buffer(_Equip, _InputTimeOut);
}
}
If it fails to enter the EquipState
or it gets interrupted, that NextWeapon
will simply not get used and anything else that tries to enter the state should set its own value first.
What Next?
Sample | Topic |
---|---|
Platformer Game Kit | A separate (free) package which demonstrates a much more complete character implementation for a 2D platformer game. |
3D Game Kit | A more complex character framework based on Unity's 3D Game Kit Lite. |