[Ex]change Game Project

Project Status

Project Finished.

Concept

Exchange was an international project between the Hochschule Harz Wernigerode, the Tokyo University of Technology and the Universidad Nacional Autónoma de México. It had a duration of 1 year and focused on game development in a multi-cultural and international team.

Logline

Take control of a little cleaning robot in a futuristic,post-apocalyptic world which used to be inhabited by humans. When your peaceful live of cleaning the old house suddenly is interrupted by the key to total adaptability it is your opportunity and burden to save all of robot kind from the AI which already got rid of all humans.

Gameplay

Clear levels with your little robot and exchange parts with other robots with the power of total adaptability.

Contribution

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 Saw_Robot
The SawEnemy
The Saw_Robot FOV
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 Drone Enemy
The Enemy Drone
The FOV of the 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 Endboss
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;
        }

Conclusion

The exchange project was unfortunately a pretty rough time. We had to fight with 3 different time zones almost close to 14 hours apart from another. The language barrier was pretty high, which made working difficult. Some time later people especially from the UNAM started leaving the project. That way we were whittled down from in the beginning close to 30 people to about 10 to 13. Therefore we needed to adjust the scope accordingly and unfortunately frequently. The obstacles did not stop there unfortunately, the overall working environment in the german team soured pretty quickly due to a multitude of reasons which ultimately boil down to a lack of communication. I would improve here by being more open about my thoughts and talk more about how others currently feel.
I contributed towards the Gameplay Aspect of the Project. I was in charge of the enemy AI, I rewrote the movement script for the player and wrote a functional Grappling Hook. Also some of my scripts were the Basis for things like the Health or On-Screen Enemy Health Bars.
In the project I made the logic behind 3 different enemies: A sawdrone, a flying drone and the final boss.
The basic structure is the same for all: A simple state machine with: Attack, Chase and Idle. In Idle they patrol between a set of Waypoints, in Chase they investigate the player and in attack they try to kill the player in their own unique way. The sawdrone tries to drive the player to death, the flydrone shoots at the player and the boss tries both with a giant sawarm and a cannon in his eye.
I am pretty happy with the first two considering it was my first time writing enemies for a 3d environment. The Boss however, is not my best work. Unfortunately I only had 4 weeks to code him and that is not enough to work out all the kinks and focus on other assignments as well. Apart from that I am very happy about my contributions to the project.
Would I do such a project again? Yes, this is one of these „once in a lifetime“ things and I am a bit sad about how it turned out in the end. While the game turned into something I am proud in being a part of, I can not say the same thing about the team's working atmosphere. If I were to do it again I would try to obtain a management position and try to work around some problems such as the language barrier and low morale towards the shared goal of a great game.
In conclusion, it was an interesting project with a very big load of issues, however I would jump at such an opportunity again. Since I know what lacked here, I have a good idea on how to improve these things from here onwards. I also still have contact with my japanese colleagues, who now help me learn Japanese. So it was overall an okay experience, just very tense and very exhausting.