For the exchange project I was a gameplay programmer. It was my job to rewrite the movement script into a more familiar form for everyone. I also was
in charge of the logic and the behavior of the enemies and the final boss. I also wrote the grappling hook item, with which the player can swing
through the levels. Aside from that I helped with worldbuilding and leveldesign
Enemies
General Approach
The enemies use a basic FOV Coroutine combined with a state machine to first detect the player then follow him until he is killed or the player vanishes from
their sight successfully.
FOV Logic
private IEnumerator FOVRoutine()
{
float delay = 0.2f;
WaitForSeconds wait = new WaitForSeconds(delay);
while (true)
{
yield return wait;
FieldOfViewCheck();
}
}
/// <summary>
/// This Coroutine checks whether our player is inside the FOV of our enemy or not
/// </summary>
private void FieldOfViewCheck()
{
if (_isPlayerDead)
{
canSeePlayer = false;
return;
}
Collider[] rangeChecks = Physics.OverlapSphere(transform.position, CheckRadius, whatIsPlayer);
if (rangeChecks.Length != 0) //we are only looking for one object, if this does not work look that only the player has the Player Layer
{
Transform target = rangeChecks[0].transform;
Vector3 directionTarget = (target.position - transform.position).normalized;
if (Vector3.Angle(transform.forward, directionTarget) < angle / 2)
{
float distanceToTarget = Vector3.Distance(transform.position, target.position);
//is nothing obscuring the player
if (!Physics.Raycast(transform.position, directionTarget, distanceToTarget, obstructionMask))
{
canSeePlayer = true;
}
else //something is obscuring the player
{
canSeePlayer = false;
}
}
}
else if (canSeePlayer == true) //when the player is not within the radius anymore unsee him
{
canSeePlayer = false;
}
}
The Sawrobot
The Sawrobot is an enemy, that follows the player once detected. His aim is to kill the player by constantly driving into him with his saw.
public override void Attack()
{
RotateToPoint(Player.transform.position);
if (Vector3.Distance(transform.position, Player.position) >= AttackDist)
{
if (_agent.isStopped) _agent.isStopped = false;
Debug.Log("Attacking Player");
_agent.destination = Player.position;
_agent.transform.position = Vector3.SmoothDamp(transform.position, new Vector3(_agent.nextPosition.x, 0, _agent.nextPosition.z), ref velocity, 0.3f );
}
}
The SawEnemy
The FOV of the SawEnemy and other checking values and their range
The Drone
The Drone is an enemy, that chases the player. When reaching a certain rage he halts and shoots at the player. He has a large FOV of 90
public override void Attack()
{
_agent.destination = transform.position;// lock the robot in place while Shooting at player
transform.LookAt(Player);
ShootAtPlayer();
}
private void ShootAtPlayer()
{
_bulletTime -= Time.deltaTime;
if (_bulletTime > 0) return;//do not shoot until "charging" complete
_bulletTime = timer;
shootSound.Play();
GameObject bulletObject =
Instantiate(EnemyBullet, SpawnPoint.transform.position, transform.rotation);
Rigidbody bulletRigidbody = bulletObject.GetComponent<Rigidbody>();
/*
*for some reason there is a negative offset between the playermodel and the playerentity,
* therefore it needs to shoot at the Cameraroot,
* since it is the only component right above our player model
*/
Vector3 direction = (Player.transform.position+offSet) - transform.position;
direction.Normalize();
bulletRigidbody.AddForce(direction * bulletSpeed, ForceMode.Impulse);
}
The Enemy Drone
The FOV of the Drone and other checking values and their range
The Endboss
The Endboss is an accumulation of the features introduced in the Sawrobot and the Drone. He has a big saw and a laser eye. Unfortunately I did not
have that much time to develop and test the script properly, since he was introduced as an idea in the last month of development. If I would have to pick
an enemy to rewrite it would be him
The mighty Endboss
Grappling Hook
The Grappling Hook is a Gameplay Element, that allows the player to quickly traverse a certain distance by shooting a rope to that point.
Programmwise I achived this by freezing the player for the jump, then shooting a Raycast to the target. If it was in Range I did a simple physics calculation
to get the force needed to reach the point. Then I applied the force. During the end of production another programmer reworked my script to work with the
input system choosen for the game. Unfortunately this person forgot to comment the code and the old script was overwritten instead of deprecated.
Therefore I could only comment where I knew what was happening. You could call this script a joint venture.
[Header("References")]
private MovementRigidbody pm;
public Transform camera;
public LayerMask whatIsGrappable;
[FormerlySerializedAs("lr")] public LineRenderer lr_Left;
public LineRenderer lr_Right;
public Rigidbody rb;
private BasicEnergy _playerEnergy;
[Header("GrappleValues")]
public float maxGrappleDistance;
public float grappleDelayTime;
public float overshootYAxis;
private Vector3 grapplePoint;
[Header("CooldownValues")]
public float grapplingCd;
private float grapplingTimer;
[Header("Input")]
public KeyCode grappleKey = KeyCode.Mouse1;
private bool grappling;
private bool _isGrappable;
private bool _freeze;
private float _speedStorage;
[SerializeField] private InputActionReference grappleAction;
[Header("Energy & Damage")] [SerializeField]
private float energyCost = 25f;
[SerializeField] private int damage = 1;
[Header("Audio")]
[SerializeField] private AudioSource startGrapple;
private void OnEnable()
{
grappleAction.action.Enable();
grappleAction.action.performed += StartGrapple;
}
private void Start()
{
pm = GetComponentInParent<MovementRigidbody>();
_speedStorage = pm.MoveSpeed;
rb = GetComponentInParent<Rigidbody>();
_playerEnergy = PlayerInstance.Instance.GetPlayerEnergy();
}
private void Update()
{
if (_freeze)
{
pm.MoveSpeed = 0f; //freeze the player for a short time
}
else
{
pm.MoveSpeed = _speedStorage;
}
if (grapplingTimer > 0)
{
grapplingTimer -= Time.deltaTime;
}
if (grappling && _isGrappable)
{
_freeze = false;
var position = transform.position;
Vector3 lowestPoint = new Vector3(position.x, position.y - 1f, position.z);
float grapplePointRelativeYPosition = grapplePoint.y - lowestPoint.y;
float highestPointOnArc = grapplePointRelativeYPosition + overshootYAxis;
if (grapplePointRelativeYPosition < 0) highestPointOnArc = overshootYAxis;
JumpToPosition(grapplePoint, highestPointOnArc);
Invoke(nameof(StopGrapple),1f);
}
if (grapplingTimer > 0) grapplingTimer -= Time.deltaTime;
}
private void LateUpdate()
{
if (grappling)
{
lr_Left.SetPosition(0,lr_Left.transform.position);
lr_Right.SetPosition(0,lr_Right.transform.position);
}
}
/// <summary>
/// Start the Grappling by Shooting a Raycast into the scene. If it hits something grappable continue with
/// calculations. Otherwise revert.
/// </summary>
/// <param name="obj"></param>
private void StartGrapple(InputAction.CallbackContext obj)
{
if(UIManager.Instance.HasActiveElements) return;
if (grapplingTimer > 0) return;
if (!_playerEnergy.Use(energyCost)) return;
grappling = true;
_freeze = true;
startGrapple.Play();
RaycastHit hit;
if (Physics.Raycast(camera.position, camera.forward, out hit, maxGrappleDistance, whatIsGrappable))
{
grapplePoint = hit.point;
_isGrappable = true;
Debug.Log("Collider hit" , hit.collider);
MakeDamage(hit.collider);
Invoke(nameof(ExecuteGrapple), grappleDelayTime);
}
else
{
grapplePoint = camera.position + camera.forward * maxGrappleDistance;
_isGrappable = false;
Debug.Log("No Collider hit");
Invoke(nameof(StopGrapple), grappleDelayTime);
}
lr_Left.enabled = true;
lr_Left.SetPosition(1, grapplePoint);
lr_Right.enabled = true;
lr_Right.SetPosition(1, grapplePoint);
}
private void MakeDamage(Collider raycastHit)
{
if (raycastHit.TryGetComponent(out BasicHealth health))
{
health.Damage(damage);
}
}
/// <summary>
/// disable everything.
/// </summary>
private void StopGrapple()
{
_freeze = false;
grappling = false;
_isGrappable = false;
grapplingTimer = grapplingCd;
lr_Left.enabled = false;
lr_Right.enabled = false;
}
/// <summary>
/// Jump to the calculated position
/// </summary>
/// <param name="targetPosition"></param>
/// <param name="trajectoryHeight"></param>
private void JumpToPosition(Vector3 targetPosition, float trajectoryHeight)
{
rb.velocity = CalculateJumpVelocity(transform.position, targetPosition, trajectoryHeight);
}
/// <summary>
/// Execute the grappling
/// </summary>
private void ExecuteGrapple()
{
_freeze = false;
rb.constraints = RigidbodyConstraints.FreezeRotation;
//rotationfreeze is needed, because otherwise when the player hits the wall with its edge uncontrollable spinning ensues
Vector3 lowestPoint = transform.position;
float grapplePointRelativeYPos = grapplePoint.y - lowestPoint.y;
float highestPointOnArc = grapplePointRelativeYPos + overshootYAxis;
if (grapplePointRelativeYPos < 0) highestPointOnArc = overshootYAxis;
JumpToPosition(grapplePoint, highestPointOnArc);
Invoke(nameof(StopGrapple), 1f);
}
/// <summary>
/// Calculate the jumpvelocity needed to reach the point.
/// </summary>
/// <param name="startpoint"></param>
/// <param name="endPoint"></param>
/// <param name="trajectoryHeight"></param>
/// <returns></returns>
private Vector3 CalculateJumpVelocity(Vector3 startpoint, Vector3 endPoint, float trajectoryHeight)
{
float gravity = Physics.gravity.y;
float displacementY = endPoint.y - startpoint.y;
Vector3 displacementXZ = new Vector3(endPoint.x - startpoint.x, 0f, endPoint.z - startpoint.z);
Vector3 velocityY = Vector3.up * Mathf.Sqrt(-2 * gravity * trajectoryHeight);
Vector3 velocityXZ = displacementXZ / (Mathf.Sqrt(-2 * trajectoryHeight / gravity)
+ Mathf.Sqrt(2 * (displacementY - trajectoryHeight) / gravity));
return velocityXZ + velocityY;
}
private void OnDisable()
{
grappleAction.action.Disable();
grappleAction.action.performed -= StartGrapple;
}