SHIELD RUSH

What Is it?


Shield Rush is a story driven single player game, based in a city, where robots rule. You play as Katelyn, who is a human girl, on her way home, but just as you are going to board the train, your ticket gets stolen. The thief is a robot who has control of all other robots, and he is feared throughout the city. Luckily, Katelyn is not alone; A small robot stands up and helps her, as she chases after the robots that stole her ticket.


My role during the project was mainly scripter and I scripted the character controls, AI and world events in the game. I also implemented animations, UI, particles and sound.

Platform: PC

Tools Used: Unity, SVN

Duration: 7 weeks

Team Size: 9

Role: Designer & Scripter

Responsibilities: Gameplay Design, Gameplay Scripting, AI Scripting

Combat

Overview

The combat in Shield Rush consist of basic abilities and modified abilities. The basic abilities are Attack, Dash and Shield. The shield can be either used for defense against enemies, or as a modifier to change how the attack or dash works. If an ability is modified, it will swap from offensive to defensive and vice versa.

Basic Attack & Dash

The player can perform a basic attack combo of three consecutive attacks. Each attack deal one damage and can be used quickly after each other. After the third attack, the player will have a slight delay before another attack is available, making it a good timing to use the dash.


The dash can be used to move a short distance quickly, however it will leave the player vulnerable for a short time after using it. The dash cost 15% of the player shield, so the player has to be careful of when it is used.

Shield Push

The shield push is available when holding down the shield and pressing the attack button. The push can push away enemies, objects in the world and reflect projectiles. The player can force melee enemies away to create space, or throw them off a cliff so that they fall to their demise.

  1. using UnityEngine;
  2. [System.Serializable]
  3. public class AbilityProperties {
  4. [HideInInspector]
  5. public bool modified;
  6. public bool hasLearnt = false;
  7. public bool upgraded = false;
  8. public float modifiedMaxCooldown = 1;
  9. public float modifiedCurrentCooldown = 0;
  10. public float unmodifiedMaxCooldown = 1;
  11. public float unmodifiedCurrentCooldown = 0;
  12. public bool canUse = true;
  13. public float UnmodifiedCooldownPercentage { get { return unmodifiedCurrentCooldown / unmodifiedMaxCooldown; } }
  14. public float ModifiedCooldownPercentage { get { return modifiedCurrentCooldown / modifiedMaxCooldown; } }
  15. public bool ModifiedCooldownReady {
  16. get { return modifiedCurrentCooldown == 0; }
  17. }
  18. public bool UnmodifiedCooldownReady {
  19. get { return unmodifiedCurrentCooldown == 0; }
  20. }
  21. public Sprite abilityIcon;
  22. }
  23. public abstract class Ability : MonoBehaviour {
  24. public AbilityProperties abilityProperties;
  25. public abstract void UseAbility(bool modified);
  26. void Update() {
  27. if (abilityProperties.modifiedCurrentCooldown > 0) {
  28. abilityProperties.modifiedCurrentCooldown -= Time.deltaTime;
  29. abilityProperties.modifiedCurrentCooldown = Mathf.Clamp(abilityProperties.modifiedCurrentCooldown, 0, abilityProperties.modifiedMaxCooldown);
  30. }
  31. if (abilityProperties.unmodifiedCurrentCooldown > 0) {
  32. abilityProperties.unmodifiedCurrentCooldown -= Time.deltaTime;
  33. abilityProperties.unmodifiedCurrentCooldown = Mathf.Clamp(abilityProperties.unmodifiedCurrentCooldown, 0, abilityProperties.unmodifiedMaxCooldown);
  34. }
  35. }
  36. }
  1. public override void UseAbility(bool modified) {
  2.     if (abilityProperties.hasLearnt) {
  3.         if (modified && !playerProperties.shieldProperties.isAlive)
  4.             return;
  5.         if (!modified && !abilityProperties.UnmodifiedCooldownReady)
  6.             return;
  7.         if (modified && !abilityProperties.ModifiedCooldownReady) {
  8.             return;
  9.         }
  10.         abilityProperties.modified = modified;
  11.         if (canUse) {
  12.             if (modified && abilityProperties.upgraded)
  13.                 StartCoroutine(WaitForKeyRelease(modified));
  14.             else
  15.                 StartCoroutine(WaitForAbilityToFinish(modified, 1));
  16.         }
  17.     }
  18. }
  19. private IEnumerator ToggleReflect() {
  20.     Player p = GetComponent<Player>();
  21.     p.shieldProperties.canReflect = true;
  22.     yield return new WaitForSeconds(p.attackProperties.attackDuration * 3f);
  23.     p.shieldProperties.canReflect = false;
  24. }
  25. public IEnumerator WaitForKeyRelease(bool modified) {
  26.     power = 0;
  27.     isCharging = true;
  28.     playerProperties.ShieldAnimator.SetBool("Bash", true);
  29.     while (Input.GetButton("MeleeAttack")) {
  30.         playerProperties.attackProperties.canAttack = false;
  31.         if (power < 2)
  32.             power += 0.5f;
  33.         yield return new WaitForSeconds(0.1f);
  34.     } // Here is when we release the key
  35.     StartCoroutine(ToggleReflect());
  36.     anim.SetTrigger("Reflect"); // play the animations
  37.     playerProperties.ShieldAnimator.SetBool("Bash", false);
  38.     
  39.     // Call the method that actually runs the ability
  40.     StartCoroutine(WaitForAbilityToFinish(modified, power));
  41.     power = 0;
  42.     yield return new WaitForSeconds(0.2f);
  43.     isCharging = false;
  44. }
  45. IEnumerator CheckForCombo() {
  46.     yield return new WaitForSeconds(attackProperties.attackDuration);
  47.     currentCombo = 0;
  48.     playerProperties.movementProperties.slowed = false;
  49.     playerProperties.companion.side = 1;
  50. }
  51. IEnumerator WaitForAbilityToFinish(bool modified, float power) {
  52.     if (!modified)
  53.         currentCombo++;
  54.     bool waitAfter = true;
  55.     if (currentCombo <= combo) {
  56.         if (!Options.UseController)
  57.             GetComponent<PlayerRotation>().LookAtMouse();
  58.         if (coroutine != null)
  59.             StopCoroutine(coroutine);
  60.         // save the coroutine so that we can cancel it later if needed.
  61.         coroutine = CheckForCombo();
  62.         StartCoroutine(coroutine);
  63.         playerProperties.movementProperties.slowed = true;
  64.         if (modified && abilityProperties.upgraded) { // if the ability is modified.
  65.             UseAbilityModified();
  66.             abilityProperties.modifiedCurrentCooldown = abilityProperties.modifiedMaxCooldown;
  67.         } else if (!playerProperties.shieldProperties.shieldActive) {
  68.             UseAbilityUnModified(); // deal damage at the location in front of the player.
  69.             MoveCompanion();
  70.             anim.SetTrigger("Attack");
  71.             abilityProperties.unmodifiedCurrentCooldown = abilityProperties.unmodifiedMaxCooldown;
  72.             Vector3 rot = playerProperties.transform.eulerAngles + new Vector3(0, 0, 0);
  73.             if (attackProperties.attackSound.Count > 0) {
  74.                 var randomIndex = UnityEngine.Random.Range(0, attackProperties.attackSound.Count);
  75.                 var sound = attackProperties.attackSound[randomIndex];
  76.                 SoundManager.PlaySoundOneshot(sound);
  77.             }
  78.             //spawn effects for the ability.
  79.             var location = playerProperties.companion.transform.position;
  80.             Destroy(Instantiate(bashEffect, location, Quaternion.Euler(rot)), 3);
  81.         } else {
  82.             waitAfter = false;
  83.         }
  84.         if (waitAfter) {
  85.             if (currentCombo == combo) { // if the combo counter is maxed.
  86.                 StopCoroutine(coroutine);
  87.                 yield return new WaitForSeconds(attackProperties.attackDuration * 1.5f);
  88.                 currentCombo = 0;
  89.             } else {
  90.                 yield return new WaitForSeconds(attackProperties.attackDuration);
  91.             }
  92.         }
  93.         playerProperties.movementProperties.slowed = false;
  94.     }
  95. }

Charge

The player will unlock a charge, which can be used to move forwards. The distance of the charge depends on how long the player holds the button.


The charge can also be used to kill enemies and destroy objects in the world.


The charge is used by holding the shield and pressing the dash button.

  1. private Vector3 FindRollTarget(float rollDistance, Vector3 direction, Vector3 startLocation, bool modified) {
  2. // modified = true -> charge
  3. // modified = false -> normal roll
  4. RaycastHit hit;
  5. Vector3 targetPos = direction;
  6. /*
  7.     // Look for ground each "checkDistance", if there is ground, set the targetPos to that location,
  8. // otherwise, return the position we are at.
  9.     */
  10. for (float i = 0f; i < rollDistance; i += checkDistance)
  11. var fwdDirection = transform.position + direction * i;
  12. var hitlayer = rollProperties.rollDetectionLayers;
  13. var canFindGround = Physics.Raycast(fwdDirection, -Vector3.up, rollDistance, hitlayer);
  14. var checkForward = Physics.Raycast(fwdDirection, direction, checkDistance, hitlayer);
  15. if (canFindGround && !checkForward) {
  16. targetPos = transform.position + direction * i;
  17. } else {
  18. if (modified) {
  19. // If we are charging into a worldobject that canbe destroyed
  20. var origin = transform.position + direction * i;
  21. var layers = rollProperties.rollDetectionLayers;
  22. if (Physics.Raycast(origin, direction, out hit, checkDistance * 2, layers)) {
  23. // Keep charging. Dont stop.
  24. var wo = hit.collider.gameObject.GetComponent<WorldObject>();
  25. if (wo && wo.destructable) {
  26. targetPos = transform.position + direction * i;
  27. continue;
  28. } else {
  29. return targetPos - (direction);
  30. }
  31. } else {
  32. return targetPos - (direction);
  33. }
  34. } else { // if there is no ground
  35. return targetPos;
  36. }
  37. }
  38. return targetPos - (direction);
  39. }
  40. public IEnumerator waitForRoll(float force, Vector3 direction, bool modified, float extraRollpower) {
  41. Vector3 targetPos = direction;
  42. targetPos = FindRollTarget(rollDistance, direction, transform.position, modified);
  43. float counter = 0;
  44. int tries = 0;
  45. if (targetPos != direction) {
  46. // Tries to charge until player is at target location or too many tries (saftety for if we get stuck)
  47. while (transform.position != targetPos && tries < maxChargeTries) {
  48. shieldProperties.shieldActive = true;
  49. // If the game is paused, just wait until we unpause the game, then continue the charge.
  50. if (GameController.isPaused) {
  51. yield return new WaitForSeconds(0.01f);
  52. } else {
  53. if (modified) { // If we are charging and not rolling: Deal damage.
  54. var fwd = playerProperties.unitMesh.forward * checkDistance;
  55. var fLocation = playerProperties.unitMesh.position + fwd;
  56. var location = fLocation * 2 + playerProperties.unitMesh.up * 2;
  57. var damage = playerProperties.attackProperties.damage * extraRollpower / 2;
  58. dealDamageAtLocation(location, damage);
  59. }
  60. tries++;
  61. counter += Time.deltaTime;
  62. var location = transform.position + direction;
  63. var layer = rollProperties.rollDetectionLayers;
  64. var checkDown = Physics.Raycast(location, -Vector3.up, rollDistance, layer);
  65. var checkForward = Physics.Raycast(location, direction, 0.1f, layer);
  66. if (checkDown && !checkForward)
  67. var speed =chargeSpeed* Time.deltaTime* rollProperties.rollSpeed.Evaluate(counter)
  68. transform.position = Vector3.MoveTowards(transform.position, targetPos, speed);
  69. // rollProperties.rollSpeed is a curve that can be modified in the editor.
  70. else
  71. break;
  72. yield return 0; // Wait one frame.
  73. }
  74. }
  75. }
  76. }

EMP

The player will, in the later stage of the game, gain access to an EMP. The emp shuts off all nearby enemy robots, making them incapable of doing anything for three seconds. This allows the player to get in close to the enemies without risking taking damage.

  1. public override void UseAbility(bool modified) {
  2. if (abilityProperties.ModifiedCooldownReady && abilityProperties.upgraded && playerProperties.HasShield(shieldCost * playerProperties.shieldProperties.maxShieldHealth)) {
  3. // Removes a percentage of the players shield when used.
  4. playerProperties.TakeShieldDamage(shieldCost * playerProperties.shieldProperties.maxShieldHealth);
  5. // Shakes the camera.
  6. Camera.main.GetComponent<CameraMovement>().ShakeCamera(camShakeForce, camShakeTime);
  7. // Play the sound of the ability
  8. PlayEMPSound();
  9. abilityProperties.modifiedCurrentCooldown = abilityProperties.modifiedMaxCooldown;
  10. // Spawn the ground effect and destroy it in 4 seconds.
  11. Destroy(Instantiate(slamGroundEffect, transform.position + Vector3.up * 0.05f, Quaternion.identity), 4);
  12. // Play the EMP animation of the player.
  13. playerProperties.unitMesh.GetComponent<Animator>().SetTrigger("EMP");
  14. StartCoroutine(FlickerLights());
  15. // Find all enemies in range.
  16. Collider[] hits = Physics.OverlapSphere(transform.position, AOE, playerProperties.attackProperties.hitLayers);
  17. foreach (Collider hit in hits) {
  18. UnitProperties up = hit.gameObject.GetComponent<UnitProperties>();
  19. if (up) {
  20. up.TakeDamage(0, playerProperties);
  21. // If the emp hit a shield, play its death animation and destroy it.
  22. if (hit.GetComponent<WorldObject>() && hit.GetComponent<WorldObject>().destroyFromEMP) {
  23. if (hit.GetComponent<Animator>()) {
  24. hit.GetComponent<Animator>().SetTrigger("DestroyShield");
  25. Destroy(hit.gameObject, 1f);
  26. } else
  27. // Apply int.MaxValue damage to the object if it can take damage from emp and has no shield.
  28. up.TakeDamage(int.MaxValue, playerProperties);
  29. // This ensures that the object is killed.
  30. }
  31. up.AddStunDuration(stunDuration);
  32. // If its a world object, like houses, where lights can be turned off.
  33. if (hit.gameObject.GetComponent<Animator>() && hit.GetComponent<WorldObject>().canTriggerAnimation) {
  34. hit.GetComponent<WorldObject>().canTriggerAnimation = false;
  35. hit.gameObject.GetComponent<Animator>().SetTrigger("Trigger");
  36. }
  37. }
  38. }
  39. }
  40. }

AI

Overview

The AI has three different states they can be in: Idle, Chase and Roam.


When in Idle state, the enemy stands still and waits for the player to get in range and once the player is in range, it will go to the Chase state, where the robot will chase until the player is dead, or outside range.


When the AI is in Roam state, it will run around looking for the player and if the player is found, it will enter the Chase state, just like the Idle state.

Small Melee Robot

The Small melee robot looks for the player, and if the player is found, then the AI will start to chase. Once in range of the player, the robot will stop and attack. This robot has three HP and deal one damage every time it hits.


These robots are weak by themselves, but in group they can be very dangerous. They usually hang out in groups and try to surround the player if they are close by.

Medium Range Robot

The range enemy follows the player and throws projectiles if in range. If the player comes close to the robot, it will try to back away and keep distance. This forces the player to chase after, or use charge to kill the robot. This robot has two HP and deal one damage for every projectile it hits. The projectile can be reflected by the players Shield Push, if it is reflected, then the projectile flies back towards the robot.

Big Boss Robot

The big boss robot has the same behaviour as the range enemy, but instead of throwing projectiles, he charges forward, just like the player can. The boss has a shield which need to be removed by using the EMP, but once it is gone, it will regenerate after 10 seconds. This robot has 25 HP and deal one damage per second in front of him while it is charging.

  1. using UnityEngine;
  2. using System.Collections;
  3. [RequireComponent(typeof(EnemyAI))]
  4. public abstract class AIBehaviour : MonoBehaviour {
  5. protected EnemyAI ai;
  6. protected Enemy enemy;
  7. protected Animator anim;
  8. private Attack attackComponent;
  9. public AnimationEnum startAnimation = AnimationEnum.None;
  10. public virtual void Setup(EnemyAI ai, Enemy enemy) {
  11. this.ai = ai;
  12. this.enemy = enemy;
  13. attackComponent = GetComponent<Attack>();
  14. anim = enemy.unitMesh.GetComponent<Animator>();
  15. SetStartAnimation();
  16. }
  17. public abstract void TriggerBehaviour();
  18. void SetStartAnimation() {
  19. if (anim != null)
  20. switch (startAnimation) {
  21. case AnimationEnum.Idle:
  22. anim.SetFloat("MoveSpeed", 0);
  23. break;
  24. case AnimationEnum.Run:
  25. anim.SetFloat("MoveSpeed", 1);
  26. break;
  27. }
  28. }
  29. protected virtual void CheckForAttack() {
  30. RaycastHit hit;
  31. Debug.DrawLine(transform.position, transform.position + transform.forward * enemy.attackProperties.range);
  32. Physics.BoxCast(transform.position, new Vector3(0.3f, 0.3f, 0.3f), transform.forward, out hit, transform.rotation, enemy.attackProperties.range, enemy.attackProperties.hitLayers);
  33. if (attackComponent && ai.target && enemy.attackProperties.canAttack && hit.collider != null) {
  34. if (attackComponent.canAttack) {
  35. attackComponent.currentCooldown = enemy.attackProperties.attackCooldown;
  36. StartCoroutine(StopAndAttack(enemy.attackProperties.attackDuration, hit.collider.transform.position));
  37. }
  38. }
  39. }
  40. protected void LookForTarget() {
  41. Collider[] hits = Physics.OverlapSphere(transform.position, ai.AiProperties.detectionRange, ai.AiProperties.detectLayers);
  42. if (hits.Length > 0) {
  43. foreach (Collider hit in hits) {
  44. if (hit.CompareTag("Player")) {
  45. ai.target = hit.gameObject.transform;
  46. GetComponent<UnitSpeech>().Speak(Mood.Aggressive);
  47. return;
  48. }
  49. }
  50. }
  51. }
  52. protected virtual IEnumerator StopAndAttack(float waitTime, Vector3 target) {
  53. ai.AiProperties.active = false;
  54. yield return new WaitForSeconds(waitTime / 2);
  55. if (enemy.healthProperty.isAlive)
  56. attackComponent.UseAttack(target);
  57. yield return new WaitForSeconds(waitTime / 2);
  58. if (ai.navMeshAgent.isOnNavMesh && enemy.healthProperty.isAlive)
  59. ai.AiProperties.active = true;
  60. }
  61. protected virtual void UpdateTargets() {
  62. if (ai.target) {
  63. float distance = Vector3.Distance(ai.target.position, transform.position);
  64. if (distance > ai.AiProperties.leash) {
  65. ai.target = null;
  66. }
  67. } else {
  68. ai.navMeshAgent.Stop();
  69. ai.navMeshAgent.ResetPath();
  70. }
  71. }
  72. }
  1. using UnityEngine;
  2. using System.Collections;
  3. using System;
  4. public class AIBehaviorStandStill : AIBehaviour {
  5. private const float unitHeightOffset = 0.44f;
  6. private Vector3 startLoc;
  7. public override void Setup(EnemyAI ai, Enemy enemy) {
  8. base.Setup(ai, enemy);
  9. startLoc = transform.position;
  10. }
  11. public override void TriggerBehaviour() { }
  12. void Update() {
  13. if (anim)
  14. anim.SetFloat("MoveSpeed", ai.navMeshAgent.desiredVelocity.normalized.magnitude);
  15. if (ai.AiProperties.active) {
  16. if (ai.target) {
  17. Vector3 direction = (ai.target.transform.position - transform.position).normalized;
  18. Vector3 targetLoc = new Vector3(ai.target.position.x, ai.target.position.y + unitHeightOffset, ai.target.position.z) - (direction * ai.AiProperties.stopRange);
  19. if (ai.navMeshAgent.isOnNavMesh)
  20. ai.navMeshAgent.destination = targetLoc;
  21. if (Vector3.Distance(transform.position, targetLoc) <= enemy.attackProperties.range && ai.CanMove) {
  22. if (enemy.movementProperties.canRotate) {
  23. transform.LookAt(ai.target);
  24. }
  25. }
  26. } else
  27. LookForTarget();
  28. if (!ai.CanMove) {
  29. ai.navMeshAgent.Stop();
  30. } else {
  31. UpdateTargets();
  32. CheckForAttack();
  33. if (ai.navMeshAgent.isOnNavMesh)
  34. ai.navMeshAgent.Resume();
  35. }
  36. }
  37. }
  38. protected override void UpdateTargets() {
  39. if (ai.target) {
  40. float distance = Vector3.Distance(ai.target.position, transform.position);
  41. if (distance > ai.AiProperties.leash) {
  42. ai.target = null;
  43. }
  44. } else {
  45. if (Vector3.Distance(transform.position, startLoc) > 0.5f) {
  46. if (ai.navMeshAgent.isOnNavMesh)
  47. ai.navMeshAgent.SetDestination(startLoc);
  48. } else {
  49. ai.navMeshAgent.Stop();
  50. ai.navMeshAgent.ResetPath();
  51. }
  52. }
  53. }
  54. }
  1. using UnityEngine;
  2. using System.Collections;
  3. using System;
  4. public class AIBehaviourRoam : AIBehaviour {
  5. private Vector3 patrolLocation;
  6. public override void TriggerBehaviour() {
  7. StartCoroutine(PatrolRandomPoints());
  8. }
  9. public override void Setup(EnemyAI ai, Enemy enemy) {
  10. base.Setup(ai, enemy);
  11. StartCoroutine(PatrolRandomPoints());
  12. }
  13. void Update() {
  14. if (anim)
  15. anim.SetFloat("MoveSpeed", ai.navMeshAgent.desiredVelocity.normalized.magnitude);
  16. if (ai.AiProperties.active) {
  17. if (ai.target) {
  18. Vector3 direction = ai.target.transform.position - transform.position;
  19. direction.Normalize();
  20. Vector3 targetLoc = new Vector3(ai.target.position.x, ai.target.position.y + 0.44f, ai.target.position.z) - (direction * ai.AiProperties.stopRange);
  21. ai.navMeshAgent.destination = targetLoc;
  22. if (Vector3.Distance(transform.position, ai.target.transform.position) < ai.AiProperties.stopRange && ai.CanMove) {
  23. if (enemy.movementProperties.canRotate)
  24. transform.LookAt(ai.target);
  25. } else {
  26. if (!ai.CanMove) {
  27. ai.navMeshAgent.Stop();
  28. } else {
  29. ai.navMeshAgent.Resume();
  30. }
  31. }
  32. } else
  33. LookForTarget();
  34. if (ai.CanMove && ai.AiProperties.active) {
  35. UpdateTargets();
  36. CheckForAttack();
  37. }
  38. }
  39. }
  40. public IEnumerator PatrolRandomPoints() {
  41. bool arrived = true;
  42. float timeStandingStill = 0;
  43. Vector3 lastPosition = transform.position;
  44. if (ai.navMeshAgent.isOnNavMesh)
  45. while (true) {
  46. if (enemy.stunnedTime <= 0)
  47. if (ai.navMeshAgent.isOnNavMesh)
  48. ai.navMeshAgent.Resume();
  49. if (!ai.target && ai.AiProperties.active && enemy.stunnedTime <= 0) {
  50. if (Vector3.Distance(lastPosition, transform.position) <= 0.01f)
  51. timeStandingStill += Time.deltaTime;
  52. lastPosition = transform.position;
  53. if (Vector3.Distance(transform.position, patrolLocation) < 1 && !arrived) {
  54. arrived = true;
  55. yield return new WaitForSeconds(1f);
  56. }
  57. if (arrived || timeStandingStill >= 3) {
  58. arrived = false;
  59. do {
  60. patrolLocation = transform.position + UnityEngine.Random.insideUnitSphere * ai.AiProperties.detectionRange;
  61. patrolLocation.y = transform.position.y;
  62. } while (!NavMesh.CalculatePath(transform.position, patrolLocation, ai.navMeshAgent.areaMask, new NavMeshPath()));
  63. timeStandingStill = 0;
  64. } else
  65. ai.navMeshAgent.destination = patrolLocation;
  66. }
  67. yield return 0;
  68. }
  69. }
  70. }

fredrik.tumlin@Gmail.com

+46 76 191 96 57