Over the course of the project my workload and
responsibilities kept increasing. At first only
a programmer I soon became the project manager for
the technical parts of the project.
I setup the git, reviewed the contributions and made
sure everything got implemented smoothly, with some
pride I must say that we had only 2 Merge Conflicts
out of close to 30+ merges. I programmed much of the
gameplay like the walking and some of the gameplay
obstacles like the hover pads,magnetic objects and
the broader logic for interactable objects like
switches and pressure plates. I also became the UI
Designer and programmer some time into the project
and the saving mechanic was also written by me.
Movement
The Movement divides the Screen in the middle with a little safe zone of about 5% to the left and right. Then wherever the player taps the magnet moves towards.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
[SerializeField]
public AudioSource sound;
[Header("Movement")] public Vector2 velocity;
public float Speed;
public float WalkAcceleration;
public float GroundDeccaleration;
[Header("Spawn")] [SerializeField] Transform SpawnPoint;
[Header("Layermask")] [SerializeField] private LayerMask whatIsNotPlayer;
private int _percentile;
private Animator _animator;
private SpriteRenderer _renderer;
private bool _isGrounded;
public float distToGround;
private Rigidbody2D rb2D;
// Start is called before the first frame update
void Start()
{
_percentile = Screen.width * 10 / 100;
_animator = GetComponent<Animator>();
_renderer = GetComponent<SpriteRenderer>();
distToGround = GetComponent<BoxCollider2D>().bounds.extents.y + 0.2f; //height
Debug.Log(distToGround);
Physics2D.queriesHitTriggers = false;//ignore triggers for raycast
_isGrounded = GroundedCheck();
rb2D = GetComponent<Rigidbody2D>();
}
private void Update()
{
_isGrounded = GroundedCheck();
}
// Update is called once per frame
void FixedUpdate()
{
float moveInput = Input.GetAxisRaw("Horizontal"); //move right = 1, move left = -1
if (Input.touchCount > 0)
{
Touch touch = Input.GetTouch(0);
switch (touch.phase)//on touching the screen on the left or right begin walking there
{
case TouchPhase.Began:
if (touch.position.x < (Screen.width / 2) - _percentile)//left
{
moveInput = -1;
Debug.Log("began touching left");
_renderer.flipX = true;
}
if (touch.position.x > (Screen.width / 2) + _percentile)//right
{
moveInput = 1;
Debug.Log("began touching right");
_renderer.flipX = false;
}
//_animator.SetBool("isWalking", true);
break;
case TouchPhase.Stationary:
if (touch.position.x < (Screen.width / 2) - _percentile)//left
{
moveInput = -1;
Debug.Log("began touching left");
}
if (touch.position.x > (Screen.width / 2) - _percentile)//right
{
moveInput = 1;
Debug.Log("began touching right");
}
//_animator.SetBool("isWalking", true);
break;
case TouchPhase.Ended://stop Moving
//_animator.SetBool("isWalking", false);
Debug.Log("Stopped touching");
moveInput = 0;
break;
}
}
if (moveInput != 0)
{
if (_isGrounded)
{
_animator.SetBool("isWalking", true);
_animator.SetBool("isFloating", false);
_animator.SetBool("isJumping", false);
}
velocity.x = Mathf.MoveTowards(velocity.x, Speed * moveInput, WalkAcceleration * Time.fixedDeltaTime);
transform.Translate(velocity * Time.fixedDeltaTime);
}
//moveinput == 0
{
_animator.SetBool("isWalking", false);
velocity.x = Mathf.MoveTowards(velocity.x, 0, GroundDeccaleration * Time.fixedDeltaTime);
}
Debug.LogError(rb2D.velocity);
if (rb2D.velocity.y < 0 && !_isGrounded)//Animatior Values
{
_animator.SetBool("isJumping", true);
_animator.SetBool("isFloating", false);
_animator.SetBool("isWalking", false);
}
else if (rb2D.velocity.y > 0 && !_isGrounded)
{
_animator.SetBool("isJumping", false);
_animator.SetBool("isFloating", true);
_animator.SetBool("isWalking", false);
}
else if (rb2D.velocity.y != 0 && _isGrounded)
{
_animator.SetBool("isJumping", false);
_animator.SetBool("isFloating", false);
_animator.SetBool("isWalking", false);
}
}
/// <summary>
/// Resets the Player if he walks into an obstacle
/// </summary>
/// <param name="col"></param>
private void OnCollisionEnter2D(Collision2D col)
{
if (col.gameObject.CompareTag("Obstacles"))
{
sound.Play();
transform.position = new Vector3(SpawnPoint.position.x, SpawnPoint.position.y);
}
else if (col.gameObject.CompareTag("Collectables"))
{
col.gameObject.GetComponent<IInteractables>().OnInteraction();
}
}
/// <summary>
/// Interacts with an Interactable when touched
/// </summary>
/// <param name="col"></param>
private void OnTriggerEnter2D(Collider2D col)
{
if (col.gameObject.CompareTag("Interactable"))
{
col.gameObject.GetComponent<IInteractables>().OnInteraction();
}
}
/// <summary>
/// Checks whether or not the player is grounded by casting a ray down from the lower half of the sprite, if the ray
/// hits a gameobject with a non-player layer the player is grounded
/// </summary>
/// <returns></returns>
private bool GroundedCheck()
{
RaycastHit2D raycastHit2D = Physics2D.Raycast(transform.position, Vector3.down, distToGround, whatIsNotPlayer);
Color raycolor;
if (raycastHit2D.collider != null)//Debug Values
{
raycolor = Color.green;
}
else
{
raycolor = Color.red;
}
Debug.DrawRay(transform.position, Vector3.down * distToGround, raycolor);
Debug.Log(raycastHit2D.collider);
return raycastHit2D.collider != null;
}
}
Timed Jumping Plates
The Timed Jumping Plates are a level object in the game, that activates and deactivates after a short time. They use the IInteractable Interface
to make it easier to engage and disengage them on interaction. Depending on which sister stands on the Plate they are either repulsed or not. To istigate
the impression, the player is constantly repulsed I used the velocity attribute of the Rigidbody2D component to work around Unitys Gravity without
creating an unrealistic repulsion effect.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimedJumpPlate : MonoBehaviour
{
[Header("References")]
public GameObject ObjectToBeAttracted;
public GameObject ObjectToBeRepulsed;
public GameObject MagneticSpaces;
[Header("Gameplay Values")]
public bool isActive = true;
[Tooltip("Going above 1.0 only reccomended, when you want a really strong effect. In normal setting use fractions of one.")]
public float magnetStrength = 1.0f;
[SerializeField] private float Timer = 3.0f;
private float time;
private void Start()
{
if(ObjectToBeAttracted == null && CompareTag("Positive")) ObjectToBeAttracted = GameObject.FindGameObjectWithTag("Lucy");
else if(ObjectToBeAttracted == null && CompareTag("Negative")) ObjectToBeAttracted = GameObject.FindGameObjectWithTag("Joy");
if(ObjectToBeRepulsed == null && CompareTag("Positive")) ObjectToBeRepulsed = GameObject.FindGameObjectWithTag("Joy");
else if(ObjectToBeRepulsed == null && CompareTag("Negative")) ObjectToBeRepulsed = GameObject.FindGameObjectWithTag("Lucy");
if(!isActive) MagneticSpaces.SetActive(false);
time = Timer;
}
private void Update()
{
time -= Time.deltaTime;
if (time <= 0)
{
OnInteraction();
time = Timer;
}
}
private void OnTriggerStay2D(Collider2D other)
{
if (isActive)
{
if ((other.CompareTag("Lucy") && CompareTag("Positive")) || (other.CompareTag("Joy") && CompareTag("Negative")))
{
other.GetComponent<Rigidbody2D>().velocity = Vector2.up * magnetStrength;
}
}
}
public void OnInteraction()
{
isActive = !isActive;
if (isActive == true)
{
MagneticSpaces.SetActive(true);
}
else
{
MagneticSpaces.SetActive(false);
}
}
}
StartupFrameRate
In the Beginning of the Project we chose the Unity Version 2021.3.22f1. This Version unfortunately had some problems with a consistent framerate on mobile.
But we already had made some progress on the game, when we noticed the bug and the next version where this problem would be fixed was not released yet.
Therefore I researeched into the problem and discovered a fix on the Untiy Subreddit. This Code is my version of this fix. It basically sets the maximum
framerate manually, waits for the frames to pass and tells the programm to forgo Vsync, since this was where the problem was originating.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
public class StartUpFrameRate : MonoBehaviour
{
[Header("Frame Settings")]
public int MaxRate = 9999;
public float TargetFrameRate = 60.0f;
private float _currentFrameTime;
private void Awake()
{
QualitySettings.vSyncCount = 0;//schalte vsync ab
Application.targetFrameRate = MaxRate;
_currentFrameTime = Time.realtimeSinceStartup;
StartCoroutine("WaitForNextFrame");
}
IEnumerator WaitForNextFrame()
{
while (true)
{
yield return new WaitForEndOfFrame();
_currentFrameTime += 1.0f / TargetFrameRate;
var t = Time.realtimeSinceStartup;
var sleepTime = _currentFrameTime - t - 0.01f;
if (sleepTime > 0)
{
Thread.Sleep((int) (sleepTime * 1000));
while (t < _currentFrameTime)
{
t = Time.realtimeSinceStartup;
}
}
}
}
}