TwoConnect

Project Status

Game Released: Take a look

Concept

Two Connect is a game for Android mobile phones developed in the Unity Game Engine. It is a puzzle game in which you solve puzzles with the magnetic powers of two sentient monopoles.

Logline

Harnass the power of magnets and help the two monopole sisters Joy and Lucy reach the end of puzzling levels

Gameplay

Tap and hold the screen to move the character. Switch between the two sisters to solve puzzles

Contribution

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;
                    }
                }
            }
        }
    }

Conclusion

This project was by far one of the most difficult and out of comfort zone experience I had until now. This is due to the sheer amount of work that goes into making a full fledged game, with such a small team, and not only a prototype. I learned about a lot of things I was not familier with at first like UI Designing and Programming and therefore it was really fun. I like challenging myself and this project really challenged me in unique ways. I loved the team I had the chance to work with and would do it again if I could. Of course not everything was perfect. One of the artists was really slaggy and slow so his stuff arrived pretty delayed which slowed down the whole team. I could have helped there by being more strict with the time limit for him and communicating more about everyones task status. But I learned my lesson and will be more on point with everyones task should I ever be in a leading position again. I am really proud about this project.