Extending a Character

Custom Character (Inheritance)

One of the notable features of the Character class is its role as a highly robust foundation for all your game avatars. It enables users to expand its functionality by adding specific features and mechanics tailored to the requirements of their game.

In this example, we will demonstrate how to extend a Character using inheritance. In this particular case, we will add a Sprint ability.

One of the advantages of inheritance over composition is the ability to extend methods from the base class, allowing us to modify them to suit our purpose. In particular, we will use the GetMaxSpeed method. As the name suggests, this method returns the maximum speed at which the character can move, depending on its state or movement mode, making it perfect for Sprint.

Furthermore, we will leverage a newly introduced method in version 1.4, namely OnBeforeSimulationUpdate. This method simplifies the process of modifying the character's state within the character's simulation loop, enhancing the overall flexibility and control over the character controller's functionality.

Firstly, we create our custom character named SprintableCharacter:

public class SprintableCharacter : Character
{
    // TODO
}

When extending a Character, it is advisable to adhere to the established conventions of the Character class by creating a set of methods to control the execution of newly added abilities. Ensure these methods follow the pattern of the character's built-in functions, such as Jump, StopJumping, Crouch, UnCrouch, and so forth. In this specific scenario, these methods are designed to manage the Sprint ability.

For example:

public class SprintableCharacter : Character
{
    [Space(15.0f)]
    public float maxSprintSpeed = 10.0f;
        
    private bool _isSprinting;
    private bool _sprintInputPressed;
    
    public void Sprint()
    {
        _sprintInputPressed = true;
    }

    public void StopSprinting()
    {
        _sprintInputPressed = false;
    }

    public bool IsSprinting()
    {
        return _isSprinting;
    }

    private bool CanSprint()
    {
        // A character can only sprint if:
        // A character is in its walking movement mode and not crouched
            
        return IsWalking() && !IsCrouched();
    }

    private void CheckSprintInput()
    {
        if (!_isSprinting && _sprintInputPressed && CanSprint())
        {
            _isSprinting = true;
        }
        else if (_isSprinting && (!_sprintInputPressed || !CanSprint()))
        {
            _isSprinting = false;
        }
    }
}

The SprintableCharacter class serves as an illustration of this methodology. The variable maxSprintSpeed defines the character's top speed during sprinting, while the _isSprinting variable indicates the character's ongoing state. Moreover, the _sprintInputPressed variable oversees the activation of the ability, commonly triggered by input events such as key-down or key-up actions.

The CheckSprintInput method adheres to the previously mentioned pattern. Action execution methods, like Sprint and StopSprinting, prompt the character to carry out a specific action. This request undergoes processing in the CheckSprintInput method, where the CanSprint method determines whether the character is capable of initiating or should cease sprinting.

While not obligatory, it is advisable to embrace a comparable approach when extending a Character. This methodology is actively utilized by the Character class to implement its inherent jump and crouch functionalities.

The final step involves modifying the character's maximum speed while sprinting. To achieve this, we extend the character's GetMaxSpeed method. This extension enables us to return maxSprintSpeed when the character is in a sprinting state.

public override float GetMaxSpeed()
{
    return _isSprinting ? maxSprintSpeed : base.GetMaxSpeed();
}

Finally, we will expand upon the previously mentioned OnBeforeSimulationUpdate method to incorporate the management of our sprint ability.

protected override void OnBeforeSimulationUpdate(float deltaTime)
{
    // Call base method implementation
    
    base.OnBeforeSimulationUpdate(deltaTime);
    
    // Handle sprint
    
    CheckSprintInput();
}

To initiate sprinting, we utilize the Sprint method, while the StopSprinting method is employed to conclude a sprint in response to input events:

private void Update()
{
    ..

    if (Input.GetKeyDown(KeyCode.LeftShift))
        Sprint();
    else if (Input.GetKeyUp(KeyCode.LeftShift))
        StopSprinting();
}

Here is the entire script:

/// <summary>
/// This example shows how to extend a Character (through inheritance) to perform a sprint ability.
/// This uses one of the new methods (introduced in v1.4) OnBeforeSimulationUpdate,
/// to easily modify the character's state within Character's simulation loop. 
/// </summary>

public class SprintableCharacter : Character
{
    [Space(15.0f)]
    public float maxSprintSpeed = 10.0f;
    
    private bool _isSprinting;
    private bool _sprintInputPressed;
    
    /// <summary>
    /// Request the character to start to sprint. 
    /// </summary>

    public void Sprint()
    {
        _sprintInputPressed = true;
    }
    
    /// <summary>
    /// Request the character to stop sprinting. 
    /// </summary>

    public void StopSprinting()
    {
        _sprintInputPressed = false;
    }

    public bool IsSprinting()
    {
        return _isSprinting;
    }
    
    /// <summary>
    /// Determines if the character is able to sprint in its current state.
    /// </summary>

    private bool CanSprint()
    {
        // A character can only sprint if:
        // A character is in its walking movement mode and not crouched
        
        return IsWalking() && !IsCrouched();
    }
    
    /// <summary>
    /// Start / stops a requested sprint.
    /// </summary>

    private void CheckSprintInput()
    {
        if (!_isSprinting && _sprintInputPressed && CanSprint())
        {
            _isSprinting = true;
        }
        else if (_isSprinting && (!_sprintInputPressed || !CanSprint()))
        {
            _isSprinting = false;
        }
    }
    
    /// <summary>
    /// Override GetMaxSpeed method to return maxSprintSpeed while sprinting.
    /// </summary>
    
    public override float GetMaxSpeed()
    {
        return _isSprinting ? maxSprintSpeed : base.GetMaxSpeed();
    }

    protected override void OnBeforeSimulationUpdate(float deltaTime)
    {
        // Call base method implementation
        
        base.OnBeforeSimulationUpdate(deltaTime);
        
        // Handle sprint
        
        CheckSprintInput();
    }
    
    private void Update()
    {
        // Movement input
        
        Vector2 inputMove = new Vector2()
        {
            x = Input.GetAxisRaw("Horizontal"),
            y = Input.GetAxisRaw("Vertical")
        };
        
        Vector3 movementDirection =  Vector3.zero;

        movementDirection += Vector3.right * inputMove.x;
        movementDirection += Vector3.forward * inputMove.y;
        
        // If character has a camera assigned...
        
        if (camera)
        {
            // Make movement direction relative to its camera view direction
            
            movementDirection = movementDirection.relativeTo(cameraTransform);
        }

        SetMovementDirection(movementDirection);
        
        // Crouch input
        
        if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.C))
            Crouch();
        else if (Input.GetKeyUp(KeyCode.LeftControl) || Input.GetKeyUp(KeyCode.C))
            UnCrouch();
        
        // Jump input
        
        if (Input.GetButtonDown("Jump"))
            Jump();
        else if (Input.GetButtonUp("Jump"))
            StopJumping();
        
        // SPRINT input
        
        if (Input.GetKeyDown(KeyCode.LeftShift))
            Sprint();
        else if (Input.GetKeyUp(KeyCode.LeftShift))
            StopSprinting();
    }
}

Character Extension (Composition)

In the previous section, we illustrated the process of extending a Character through inheritance. Now, we will replicate the same example of adding sprint functionality to a character. However, unlike the previous approach, we will utilize composition.

Composition, in object-oriented programming, involves constructing a class using instances of other classes to achieve the desired functionality. It's worth noting that composition offers certain advantages over inheritance. With composition, components can be dynamically combined to create more flexible and modular designs. This contrasts with the more rigid structure of inheritance, allowing for easier modification and extension of code.

In essence, composition enables the building of complex structures by assembling simpler, independent parts. This approach often leads to more maintainable and scalable code, fostering a design that prioritizes flexibility and reusability.

To achieve this, we'll make use of the recently introduced 'hooks' (events) in version 1.4.0. These hooks empower us to influence the character's state without the need for inheritance from the Character base class. In particular, the BeforeSimulationUpdated event essentially serves the same purpose as the previously utilized OnBeforeSimulationUpdate method.

/// <summary>
/// Event called before character simulation updates.
/// This 'hook' lets you externally update the character 'state'.
/// </summary>

public event BeforeSimulationUpdateEventHandler BeforeSimulationUpdated;

In essence, the code remains quite similar with some minor modifications. Specifically, the CheckSprintInput method now directly modifies the Character's maxWalkSpeed property. This adjustment is necessary because, unlike the previous approach (inheritance), extending the GetMaxSpeed method is not feasible in this context.

Below is the complete script:

/// <summary>
/// This example shows how to extend a Character (through composition) to perform a sprint ability.
/// This one use the new simulation OnBeforeSimulationUpdate event (introduced in v1.4),
/// to easily modify the character's state within Character's simulation loop.
/// </summary>

public class SprintAbility : MonoBehaviour
{
    [Space(15.0f)]
    public float maxSprintSpeed = 10.0f;
    
    private Character _character;

    private bool _isSprinting;
    private bool _sprintInputPressed;

    private float _cachedMaxWalkSpeed;
    
    /// <summary>
    /// Request the character to start to sprint. 
    /// </summary>

    public void Sprint()
    {
        _sprintInputPressed = true;
    }
    
    /// <summary>
    /// Request the character to stop sprinting. 
    /// </summary>

    public void StopSprinting()
    {
        _sprintInputPressed = false;
    }

    public bool IsSprinting()
    {
        return _isSprinting;
    }

    private bool CanSprint()
    {
        return _character.IsWalking() && !_character.IsCrouched();
    }

    private void CheckSprintInput()
    {
        if (!_isSprinting && _sprintInputPressed && CanSprint())
        {
            _isSprinting = true;

            _cachedMaxWalkSpeed = _character.maxWalkSpeed;
            _character.maxWalkSpeed = maxSprintSpeed;

        }
        else if (_isSprinting && (!_sprintInputPressed || !CanSprint()))
        {
            _isSprinting = false;
            
            _character.maxWalkSpeed = _cachedMaxWalkSpeed;
        }
    }
    
    private void OnBeforeSimulationUpdated(float deltaTime)
    {
        // Handle sprinting
        
        CheckSprintInput();
    }

    private void Awake()
    {
        // Cache character
        
        _character = GetComponent<Character>();
    }

    private void OnEnable()
    {
        // Subscribe to Character BeforeSimulationUpdated event
        
        _character.BeforeSimulationUpdated += OnBeforeSimulationUpdated;
    }
    
    private void OnDisable()
    {
        // Un-Subscribe from Character BeforeSimulationUpdated event
        
        _character.BeforeSimulationUpdated -= OnBeforeSimulationUpdated;
    }
}

Finally, we will employ the Sprint and StopSprinting methods to commence or halt sprinting in response to input events.

private SprintAbility _sprintAbility;

private void Update()
{
    ..
    
    if (Input.GetKeyDown(KeyCode.LeftShift))
        _sprintAbility.Sprint();
    else if (Input.GetKeyUp(KeyCode.LeftShift))
        _sprintAbility.StopSprinting();
}

As you can observe, the process is quite similar to what we've seen before, with the distinction that now it's the SprintAbility responsible for initiating or stopping the sprint.

Creating a Custom Movement Mode

The Character class incorporates various movement modes and allows for the creation of custom modes. Before creating a new movement mode, it is recommended to attempt implementing your desired mechanics on one of the available movement modes. For instance, the jump ability is built upon the falling movement mode, while the crouch mechanic is built upon the walking movement mode.

This approach is employed to leverage and extend the functionality of existing code, making it easier to maintain and expand. By using a base movement mode and adding or modifying specific mechanics, a more modular and flexible system is created.

For example, if you already have a walking movement mode, implementing the crouch mechanic on top of it allows you to reuse the walking logic while only adding the necessary adjustments for crouching.

Similarly, building the jump ability on top of the falling movement mode allows you to reuse the falling logic while introducing specific rules for initiating and controlling jumps. This modular approach not only streamlines development but also ensures consistent and predictable behavior across different movement modes.

To create a new movement mode, you first define an ID for it. For instance:

public enum ECustomMovementMode
{
    None,
    Dashing,
    Climbing
}

This identifier will be used to establish the new movement mode using the SetMovementMode method:

SetMovementMode(MovementMode.Custom, (int)ECustomMovementMode.Dashing);

In this example, you've created an enumeration called ECustomMovementMode, which includes values for different movement modes like None, Dashing, Climbing, etc. This enumeration helps you easily identify and switch between various movement modes.

Next, extend the CustomMovementMode method. Depending on whether you're developing a derived class from the Character class (using inheritance) or subscribing to the CustomMovementModeUpdated event (utilizing composition), this is where you'll implement the logic for the new movement mode.

Using Inheritance:

When inheriting from the Character class, it is advisable to extend its CustomMovementMode method:

public class CustomCharacter : Character
{
    protected override void CustomMovementMode(float deltaTime)
    {
        // Call base method implementation
            
        base.CustomMovementMode(deltaTime);
            
        // Update dashing movement mode

        if (customMovementMode == (int)ECustomMovementMode.Dashing)
            DashingMovementMode(deltaTime);
    }
}

Using Composition:

On the other hand, when using composition, you should subscribe to its CustomMovementModeUpdated event.

protected void OnEnable()
{
    // Subscribe to Character Event
    
    character.CustomMovementModeUpdated += OnCustomMovementModeUpdated;
}

protected void OnDisable()
{
    character.CustomMovementModeUpdated += OnCustomMovementModeUpdated;
}

private void OnCustomMovementModeUpdated(float deltaTime)
{
    // Update dashing movement mode

    if (customMovementMode == (int)ECustomMovementMode.Dashing)
        DashingMovementMode(deltaTime);
}

In both cases, customize the logic inside the CustomMovementMode method to define how your character behaves in the "Dashing," "Climbing," or other custom movement modes. This flexibility allows for a tailored and dynamic user experience based on different movement scenarios.

To exit a custom movement mode, it is suggested to use the Walking or Falling movement modes, as they are automatically managed based on the character's grounding status.

Additionally, when configuring 'enter' or 'exit' settings for movement modes, it is advisable to use the OnMovementModeChanged method in the case of inheritance, or its corresponding MovementModeChanged event when employing composition. These features enable you to execute specific setup actions upon entering or leaving a particular movement mode.

Classes derived from Character are encouraged to extend the OnCustomMovementModeUpdated method instead of subscribing to the event for enhanced customization and seamless integration.

Please refer to the included examples for a complete implementation of custom movement modes.

Last updated