The ExtendoArm

Intro

In this short devlog we'll go over the core design principles and development process of the ExtendoArm ability from our game WheelSteal. Designed for both mobility and utility, the ExtendoArm should provide the player with a quick means of both traversing terrain and an easy way to collect the precious letters!

WheelSteal Screenshot
In-game screenshot of WheelSteal.

Early Design

Programmatically, the ExtendoArm means dynamic behavior based on the player’s tactical decision making. Early in development our team decided to visualize this behavior with a flowchart to clearly define the ExtendoArm’s conditional mechanics.

This diagram lays out our intended behavior from start to finish.

ExtendoArm Flowchart
Flowchart dictating the ExtendoArm's conditional behaviour.

Development

When the ability is first activated, we start by disabling player movement and rotation to prevent them from interfering with their own intended trajectory. We also prevent them from interacting with letters to prevent unwanted interactions between the ExtendoArm and any currently held letters.

[PunRPC]
public override void Activate() {
    if (Active || Deactivating || !PickupComplete) return;

    Active = true;

    AbilityManager.movementEnabled = false;
    AbilityManager.grabEnabled = false;
    AbilityManager.rotationEnabled = false;

    StartCoroutine(Cast());
}

Once the ability has been activated, we cast the arm by enabling its extended animation state.

private IEnumerator Cast() {
    AbilityManager.clawCollider.enabled = true;
    AbilityManager.clawAnimator.SetBool("GrabberActive", true);

    // wait until we've entered the appropriate animstate
    AnimatorStateInfo state = AbilityManager.clawAnimator.GetCurrentAnimatorStateInfo(0);
    while (!state.IsName("GrabberArmExtend")) {
        state = AbilityManager.clawAnimator.GetCurrentAnimatorStateInfo(0);
        yield return new WaitForEndOfFrame();
    }
}

After casting, we loop until a collision is detected on the head of the arm, or the ability reaches its full length.

// let the animation play until time elapses or it hits a target
float timeRemaining = state.length / state.speed;
float dt = 0.01f;

while (timeRemaining > 0f) {
    if (collidedGameObject != null) break;

    yield return new WaitForSeconds(dt);
    timeRemaining -= dt;
}

Case 1: No Collision

If no collision is detected, the ability simply retracts and deactivates, allowing the player to move on and continue playing the game.

// if we did not hit anything during the cast, then the extendo arm breaks!
if (collidedGameObject == null) {
    AbilityManager.clawCollider.enabled = false;
    AbilityManager.clawAnimator.SetBool("GrabberActive", false);

    StartCoroutine(DeActivate());

    yield break;
}
ExtendoArm Failure
ExtendoArm being used unsuccessfully.

Case 2: Hit Collectible

However, if a collision is detected on the end of the arm, we collect the collided game object.

public void OnTriggerEnter(Collider collider) {
    player.GetComponent<Script_AbilitiesExtendoArm_Wyatt>().Grab(collider.gameObject);
}

If the collidedGameObject is found to be a Collectible object such as a letter, we’ll set it as a child of the ExtendoArm, causing it to retract along with the claw. Once the claw is fully retracted, the letter is unparented and allowed to fall to the ground at the player’s feet.

// handle claw collision
if (collidedGameObject.CompareTag("Collectible")) { // we hit a letter, pull it in
    collidedGameObject.transform.parent = AbilityManager.clawCollider.gameObject.transform;
    collidedGameObject.transform.localPosition = Vector3.zero;

    yield return new WaitForSeconds(0.5f);

    collidedGameObject.transform.parent = null;

Case 3: Hit Terrain

Alternatively, when hitting an obstacle in the game world, the ExtendoArm acts as a grappling hook, drawing the player toward the target.

First we set the starting and target positions and disable the player’s built in CharacterController as well as the effect of gravity on the player.

} else { // otherwise, pull ourselves in!
    Vector3 startPos = transform.position;
    Vector3 targetPos = collidedGameObject.transform.position;

    AbilityManager.playerMovement.controllerReference.enabled = false;
    AbilityManager.SetGravity(false);
}

Once the positions are set, we interpolate the player’s position between them.

float t = 0f;
while (t < 1f) {
    t += Time.deltaTime / 0.5f; // duration of 0.5 seconds
    Vector3 newPos = Vector3.Lerp(startPos, targetPos, t);
    AbilityManager.playerMovement.controllerReference.position = newPos;
    yield return new WaitForEndOfFrame();
}
Grappling with the ExtendoArm
Grappling with the ExtendoArm.

Cleanup

With all of the core usage for the ExtendoArm implemented, we restore the player’s CharacterController and gravity, before initiating the final DeActivation sequence.

    AbilityManager.playerMovement.controllerReference.enabled = true;
    AbilityManager.SetGravity(true);
}

StartCoroutine(DeActivate());

Now, the ExtendoArm’s deactivation function will be called, restoring standard movement to the player and resetting their ability slot.

// Restore movement
AbilityManager.movementEnabled = true;
AbilityManager.grabEnabled = true;
AbilityManager.rotationEnabled = true;

// Ensure all animations are complete, and then remove this component from the GameObject
yield return new WaitForSeconds(0.1f);

// Remove the ability from the player
AbilityManager.ability = null;
Destroy(GetComponent<Script_AbilitiesExtendoArm_Wyatt>());

With that, we wrap up our discussion on the first iteration of the ExtendoArm ability. Lots of revision will be required to make the ability more intuitive and less punishing when missed, but for a first phase prototype the ability is fun and functional!

Grammar-naut Posing
Grammar-naut posing.
WheelSteal Logo
The WheelSteal Logo.