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!
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.
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;
}
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();
}
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!