Solo Projects

Networking Project FG

10 Week long game project in 2D using Unity.
This project was a challenge to handle networking in Unity with the use of Mirror.
Project Source Code.

Enemy AI

public class AI : NetworkBehaviour
{
    [Header("AI Settings")]
    public float maxHealth = 10f;
    [SyncVar] public float currentHealth;
    [SerializeField] private float speed = 2f;
    public float damage = 1f;
    public Transform attackPointRight;
    public Transform attackPointLeft;
    public float attackRange = 1f;
    public float attackRate = 0.25f;

    public Vector2 dir { get; private set; }
    private bool hurtCd = false;

    [Header("Detection Settings")]
    [SerializeField] private float maxDetectionDistance = 8f;
    public float detectionRadius = 5f;
    public float sqrCurrDistance { get; set; }
    public float sqrMaxDistance => maxDetectionDistance * maxDetectionDistance;

    [Header("AI Components")]
    public Health healthBar;

    // AI components
    public Collider2D[] detectionCircle { get; set; }
    public GameObject playerObject { get; set; }
    public StateMachine stateMachine { get; private set; }
    public FollowState followState { get; private set; }
    public PatrolState patrolState { get; private set; }
    public AttackState attackState { get; private set; }
    public Animator animator { get; private set; }
    public SpriteRenderer spriteRenderer { get; private set; }
    public LayerMask enemyMask { get; private set; }
    public LayerMask groundMask { get; private set; }

    private string[] ground = { StringData.groundLayer };
    private string[] enemy = { StringData.enemyLayer };

    public enum AnimState
    {
        idle,
        combat,
        run,
    }

    private void Start()
    {
        followState = new FollowState();
        patrolState = new PatrolState();
        attackState = new AttackState();

        animator = GetComponent();
        spriteRenderer = GetComponent();

        stateMachine = new StateMachine(this);
        stateMachine.ChangeState(patrolState);

        enemyMask = LayerMask.GetMask(enemy);
        groundMask = LayerMask.GetMask(ground);

        currentHealth = maxHealth;
        healthBar.SetMaxHealth(maxHealth);
    }

    private void Update()
    {
        UpdateDistanceToPlayer();

        stateMachine.UpdateState();

        FlipSpriteX();
    }

    private void UpdateDistanceToPlayer()
    {
        if (playerObject != null)
        {
            sqrCurrDistance = (playerObject.transform.position - transform.position).sqrMagnitude;
        }
    }

    public void PatrolMove(Vector2 direction)
    {
        RpcAIPatrol(direction);
        RpcAnimState(AnimState.run);
    }

    public void FollowMove()
    {
        RpcAIFollow();
        RpcAnimState(AnimState.run);
    }

    public void TakeDamage(float damage)
    {
        currentHealth -= damage;
        healthBar.SetHealth(currentHealth);

        if (currentHealth <= 0f)
        {
            RpcDieAnimation();
            return;
        }

        if (currentHealth > 0f & !hurtCd)
        {
            RpcHurtAnimation();
            hurtCd = true;
            StartCoroutine(HurtAnimCd(1.5f));
        }
    }

    private IEnumerator HurtAnimCd(float seconds)
    {
        yield return new WaitForSeconds(seconds);
        hurtCd = false;
    }

    private IEnumerator Die(float delay)
    {
        if (gameObject == null)
            yield break;

        animator.StopPlayback();
        animator.SetTrigger(StringData.death);
        yield return new WaitForSeconds(delay);
        Spawner.SpawnWhenDied(1);
        RestoreHealth(gameObject);
        ObjectPool.Despawn(gameObject);
    }

    private void HurtAnimation()
    {
        animator.SetTrigger(StringData.hurt);
    }

    public bool AnimationIsPlaying(string state)
    {
        return animator.GetCurrentAnimatorStateInfo(0).IsName(state);
    }

    public void AttackAnimation()
    {
        RpcAttackAnimation();
    }

    public void SetDirection(Vector2 direction)
    {
        dir = direction;
    }

    private void FlipSpriteX()
    {
        if (playerObject != null)
        {
            var playerDir = playerObject.GetComponent().GetDirInput;

            if (playerDir.x < 0f)
            {
                SetDirection((-1) * playerDir);
            }
            else if (playerDir.x > 0f)
            {
                SetDirection((-1) * playerDir);
            }
        }

        if (Mathf.Abs(dir.x) > Mathf.Epsilon)
        {
            spriteRenderer.flipX = dir.x > 0f;
        }
    }

    // When de-spawning a killed enemy, restore its health so next time it wont spawn with 0 health
    private void RestoreHealth(GameObject enemy)
    {
        var oldAI = enemy.GetComponent();

        if (oldAI != null)
        {
            if (oldAI.currentHealth <= 0f)
            {
                oldAI.currentHealth = oldAI.maxHealth;
                healthBar.SetHealth(oldAI.currentHealth);
            }
        }
    }

    [ClientRpc]
    private void RpcAIFollow()
    {
        // Move towards playerObjects location (x axis)
        transform.position = Vector2.MoveTowards(transform.position,
            new Vector2(playerObject.transform.position.x, transform.position.y), speed * Time.deltaTime);
    }

    [ClientRpc]
    private void RpcAIPatrol(Vector2 direction)
    {
        transform.Translate(direction * speed * Time.deltaTime);
    }

    [ClientRpc]
    private void RpcHurtAnimation()
    {
        HurtAnimation();
    }

    [ClientRpc]
    private void RpcDieAnimation()
    {
        StartCoroutine(Die(1f));
    }

    [ClientRpc]
    public void RpcAttackAnimation()
    {
        animator.SetTrigger(StringData.attack);
    }

    [ClientRpc]
    public void RpcAnimState(AnimState animation)
    {
        animator.SetInteger(StringData.animState, (int)animation);
    }



State Machine

[System.Serializable]
public class StateMachine
{
    public iState currentState { get; private set; }
    public T owner;

    public StateMachine(T owner)
    {
        this.owner = owner;
        currentState = null;
    }

    public void ChangeState(iState newState)
    {
        if (currentState != null)
        {
            currentState.ExitState(owner); 
        }

        currentState = newState;
        currentState.EnterState(owner);
    }

    public void UpdateState()
    {
        if (currentState != null)
        {
            currentState.UpdateState(owner);
        }
    }
}

public interface iState
{
    void EnterState(T owner);
    void ExitState(T owner);
    void UpdateState(T owner);
}

/////////////////////////////////////////////////////////////////////////////////////////////

// One of the states the AI uses.
public class PatrolState : iState
{
    private bool rightWall;
    private bool leftWall;
    private Vector2 direction;
    private Vector2 boxSize = new Vector2(0.15f, 1.15f);
    private float distance = 0.15f;

    public void EnterState(AI owner)
    {
        // When spawning a AI in an open Area it will not know if it
        // should start walking to the left or right
        if (direction.x == 0f)
        {
            System.Random rand = new System.Random();
            direction = rand.Next(2) == 1 ? Vector2.right : Vector2.left;
        }

        Debug.Log("Entering PatrolState.");
    }

    public void ExitState(AI owner)
    {
        Debug.Log("Exiting PatrolState.");
    }

    public void UpdateState(AI owner)
    {
        if (owner.playerObject)
        {
            owner.stateMachine.ChangeState(owner.followState);
            return;
        }

        if (!owner.playerObject)
        {
            DetectNearbyPlayer(owner);
        }

        Patrol(owner);
    }

    private void DetectNearbyPlayer(AI owner)
    {
        owner.detectionCircle = Physics2D.OverlapCircleAll(owner.transform.position, owner.detectionRadius);

        foreach (var col in owner.detectionCircle)
        {
            var player = col.GetComponent();

            if (player != null && player.GetHP > 0f)
            {
                owner.playerObject = player.gameObject;
                return;
            }
        }
    }

    private void Patrol(AI owner)
    {
        HorizontalCollision(owner);

        if (rightWall)
        {
            direction = Vector2.left;
        }
        else if (leftWall)
        {
            direction = Vector2.right;
        }

        owner.SetDirection(direction);
        owner.PatrolMove(direction);
    }

    private void HorizontalCollision(AI owner)
    {
        Vector2 originRight = new Vector2(owner.transform.position.x + 0.5f, owner.transform.position.y + 0.65f);
        Vector2 originLeft = new Vector2(owner.transform.position.x - 0.5f, owner.transform.position.y + 0.65f);
        Vector2 origin = direction.x == 1 ? originRight : originLeft;

        RayAndBoxCast(owner, origin, direction, boxSize, distance);
    }

    private void RayAndBoxCast(AI owner, Vector2 origin, Vector2 direction, Vector2 boxSize, float distance)
    {
        // Don't know why but the 'right' ray-cast distance needs to be longer.
        if (direction.x == 1)
            distance = 0.3f;

        RaycastHit2D wallHit = Physics2D.BoxCast(origin, boxSize, 0f, direction, distance, owner.groundMask);
        RaycastHit2D enemyHit = Physics2D.Raycast(origin, direction, distance, owner.enemyMask);

        if (wallHit)
        {
            rightWall = direction.x == 1;
            leftWall = direction.x == -1;
        }

        if (enemyHit.collider != null)
        {
            var enemyID = enemyHit.collider.gameObject.GetInstanceID();
            var thisID = owner.gameObject.GetInstanceID();

            rightWall = enemyID != thisID && direction.x == 1;
            leftWall = enemyID != thisID && direction.x == -1;
        }
    }
}

Communication for connected players

public class ChatBehaviour : NetworkBehaviour
{
    [SerializeField] private GameObject chatUI = null;
    [SerializeField] private TMP_Text chatText = null;
    [SerializeField] private TMP_InputField inputField = null;
    private PlayerController controller = null;

    private static event Action OnMessage;

    public bool isActive { get; private set; }

    public override void OnStartAuthority()
    {
        base.OnStartAuthority();
        chatUI.SetActive(true);

        controller = GetComponent();

        isActive = false;
        inputField.gameObject.SetActive(isActive);

        OnMessage += HandleNewMessage;
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.T) && !isActive)
        {
            ToggleChat();
        }

        if (IsExitingChat())
        { 
            ToggleChat(); 
        }
    }

    private bool IsExitingChat()
    {
        if (Input.GetKeyDown(KeyCode.Escape) && isActive)
        {
            return true;
        }

        return false;
    }

    private void ToggleChat()
    {
        isActive = !isActive;

        inputField.gameObject.SetActive(isActive);

        // while we are in "chat mode" disable movement code
        controller.DeactivateScript(!isActive);
    }

    [ClientCallback]
    private void OnDestroy()
    {
        if (!hasAuthority)
            return;

        OnMessage -= HandleNewMessage;
    }

    private void HandleNewMessage(string message)
    {
        chatText.text += message;
    }

    [Client]
    public void Send(string message)
    {
        if (!Input.GetKeyDown(KeyCode.Return))
            return;

        CmdSendMessage(message);
        inputField.text = string.Empty;
    }

    [Command]
    private void CmdSendMessage(string message)
    {
        RpcHandleMessage($"[{connectionToClient.connectionId}]: {message}");
    }

    [ClientRpc]
    private void RpcHandleMessage(string message)
    {
        OnMessage?.Invoke($"\n{message}");
    }
}