Undefeatables – 3D MOBA For Mobile [Unfinished Project]

Because of the project has commercial value, I cannot exhibit all properties of the project.

This is 3D moba game made for mobile. This unfinished project was created in a group of 3 people: 2 designer/modeler, 1 developer (me).

I used Photon Networking to develop online experience in Unity game engine.

My Role In The Team

As I mentioned, I worked as an only developer in the team. I was responsible for Unity Engine, gameplay programming, multiplayer programming, and Construct 2 programming. I also participated physical prototyping phase of the project.

Because of that we were small team and working collaboratively, all of us contributed with out ideas to the project.

This project also has commercial value. So that, I cannot exhibit all codes in the project. Here is some codes:

Some Codes From The Project

NPCMonsterController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using UnityEngine.UI;
using Photon.Realtime;

public class NPCMonsterController : MonoBehaviour
{
    #region Variables
    [Header("Monster Related Stuff")]
    [SerializeField]
    GameObject currentTarget;
    [SerializeField]
    Animator monsterAnimator;
    [SerializeField]
    Image healthBar;
    [SerializeField]
    new string name;
    [SerializeField]
    float hp;
    [SerializeField]
    float damage;
    [SerializeField]
    float attackRange;
    [SerializeField]
    float movementSpeed;
    [SerializeField]
    float attackSpeed;
    [SerializeField]
    bool isEnemyInDangerZone;
    [SerializeField]
    bool isAttacked;
    [SerializeField]
    bool isOverlappingMonster;
    float startHealth;

    [Header("Current Action of Monster")]
    [SerializeField]
    bool chasePlayer;
    [SerializeField]
    bool attackPlayer;
    [SerializeField]
    bool returnToDangerZone;

    [Header("Other Stuff")]
    [SerializeField]
    GameObject dangerZone;
    bool levelUpOnce;

    //Getters and Setters
    public static string WhoAmI { get; set; }
    public GameObject DangerZone { get => dangerZone; set => dangerZone = value; }
    #endregion

    #region Unity Functions
    // Start is called before the first frame update
    void Start()
    {
        isEnemyInDangerZone = false;
        isAttacked = false;
        isOverlappingMonster = false;
        levelUpOnce = false;

        WhoAmI = name;
        startHealth = hp;
        healthBar.fillAmount = hp / startHealth;
    }
    // Update is called once per frame
    void Update()
    {
        if (GameSceneManager.gameSceneManagerInstance.IsGameOver == false && dangerZone != null)
        {
            isEnemyInDangerZone = dangerZone.GetComponent<DangerZoneController>().IsEnemyThere;
            currentTarget = dangerZone.GetComponent<DangerZoneController>().EnemyPlayer;

            if ((chasePlayer == false || attackPlayer == false) && isEnemyInDangerZone && currentTarget != null && hp > 0)
            {
                //Chase Player up to attack range
                if (Vector3.Distance(transform.position, currentTarget.transform.position) >= attackRange && isOverlappingMonster == false)
                {
                    transform.LookAt(currentTarget.transform);
                    transform.Translate(Vector3.forward * Time.deltaTime * movementSpeed);
                    SetAnimatorState("running");
                }
                //if player enters attack range of monster
                else
                {
                    if (isAttacked == false)
                    {
                        StartCoroutine(AttackOnce());
                    }
                }
            }
            else if (hp > 0 && returnToDangerZone == true)
            {
                currentTarget = dangerZone;
                transform.LookAt(currentTarget.transform);
                transform.Translate(Vector3.forward * Time.deltaTime * movementSpeed);
                SetAnimatorState("running");
            }
            else if (hp > 0)
            {
                StopAllCoroutines();
                isAttacked = false;
                SetAnimatorState("idle");
            }
            else if (hp <= 0)
            {
                StopAllCoroutines();
                isAttacked = false;
            }
        }
    }
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("PitSpirit") || collision.gameObject.CompareTag("Gorg"))
        {
            isOverlappingMonster = true;
        }
        isOverlappingMonster = false;
    }
    private void OnTriggerStay(Collider other)
    {
        if (other.gameObject.CompareTag("DangerZone") == true)
        {
            returnToDangerZone = false;
        }
    }
    private void OnTriggerExit(Collider other)
    {
        if (other.gameObject.CompareTag("DangerZone") == true)
        {
            returnToDangerZone = true;
        }
    }
    #endregion

    #region Custom Functions
    void SetAnimatorState(string name)
    {
        if (name == "die")
        {
            monsterAnimator.SetBool("IsRunning", false);
            monsterAnimator.SetBool("IsIdle", false);
            monsterAnimator.SetBool("IsAttacking", false);
            monsterAnimator.SetBool("IsHit", false);
            monsterAnimator.SetBool("IsDead", true);
        }
        else if (name == "takeDamage")
        {
            monsterAnimator.SetBool("IsRunning", false);
            monsterAnimator.SetBool("IsIdle", false);
            monsterAnimator.SetBool("IsAttacking", false);
            if (monsterAnimator.GetBool("IsHit") == false)
            {
                monsterAnimator.SetBool("IsHit", true);
                StartCoroutine(WaitForHitAnim());
            }
            monsterAnimator.SetBool("IsDead", false);
        }
        else if (name == "running")
        {
            monsterAnimator.SetBool("IsRunning", true);
            monsterAnimator.SetBool("IsIdle", false);
            monsterAnimator.SetBool("IsAttacking", false);
            monsterAnimator.SetBool("IsHit", false);
            monsterAnimator.SetBool("IsDead", false);
        }
        else if (name == "idle")
        {
            monsterAnimator.SetBool("IsRunning", false);
            monsterAnimator.SetBool("IsIdle", true);
            monsterAnimator.SetBool("IsAttacking", false);
            monsterAnimator.SetBool("IsHit", false);
            monsterAnimator.SetBool("IsDead", false);
        }
        else if (name == "attack")
        {
            monsterAnimator.SetBool("IsRunning", false);
            monsterAnimator.SetBool("IsIdle", false);
            monsterAnimator.SetBool("IsAttacking", true);
            monsterAnimator.SetBool("IsHit", false);
            monsterAnimator.SetBool("IsDead", false);
        }
    }
    
    IEnumerator WaitForHitAnim()
    {
        yield return new WaitForSeconds(0.3f);
        if (hp > 0)
        {
            SetAnimatorState("idle");
        }
        else
        {
            SetAnimatorState("die");
        }
    }
    IEnumerator AttackOnce()
    {
        isAttacked = true;
        SetAnimatorState("attack");
        if (monsterAnimator.GetCurrentAnimatorStateInfo(0).IsName("hitwithsword") == true)
        {
            attackSpeed = monsterAnimator.GetCurrentAnimatorStateInfo(0).length;
        }   

        if (currentTarget.gameObject.GetComponent<PhotonView>() != null && currentTarget.gameObject.GetComponent<PhotonView>().Owner != null)
        {
            Debug.Log(gameObject.name + " has attacked to player " + currentTarget.gameObject.GetComponent<PhotonView>().Owner.NickName);
            //give damage
            GetComponent<MonsterAudioController>().SetSound("attacking");
            currentTarget.gameObject.GetComponent<PhotonView>().RPC("LooseHealth", RpcTarget.AllBuffered, damage);
        }
        else
        {
            //offline testing
            Debug.Log(gameObject.name + " has attacked to player " + currentTarget.gameObject.name);
        }

        yield return new WaitForSeconds(attackSpeed);
        isAttacked = false;
    }
    #endregion

    #region RPC Function
    [PunRPC]
    public void LooseHealth(float _playerDamage, PhotonMessageInfo info)
    {
        hp -= _playerDamage;
        Debug.Log(gameObject.name + "'s current health: " + hp);

        healthBar.fillAmount = hp / startHealth;
        if (hp > 0f)
        {
            SetAnimatorState("takeDamage");
        }
        else if (levelUpOnce == false)
        {
            levelUpOnce = true;
            //monster died
            GetComponent<CapsuleCollider>().direction = 0;
            SetAnimatorState("die");
            GetComponent<MonsterAudioController>().SetSound("die");
            Debug.LogWarning(info.Sender.NickName + " killed " + gameObject.name);
            //Player Level Up
            Debug.Log(info.Sender.NickName);

            GameObject[] _players = GameObject.FindGameObjectsWithTag("Player");

            foreach (GameObject p in _players)
            {
                p.GetComponent<PhotonView>().RPC("LevelUp", RpcTarget.AllBuffered, info.Sender.NickName);
            }
            Destroy(gameObject, 3.0f);
        }
    }
    #endregion
}
ConnectionManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;
using TMPro;

public class ConnectionManager : MonoBehaviourPunCallbacks
{
    #region Variables
    [Header("Panels")]
    [SerializeField]
    GameObject enterGamePanel;
    [SerializeField]
    GameObject connectionStatusPanel;
    [SerializeField]
    GameObject lobbyPanel;
    [SerializeField]
    GameObject createRoomPanel;
    [SerializeField]
    GameObject insideRoomPanel;
    [SerializeField]
    GameObject roomListPanel;
    [SerializeField]
    GameObject joinRandomRoomPanel;

    [Header("Character Menu's")]
    [SerializeField]
    GameObject yeloriyaMenu;
    [SerializeField]
    GameObject tulgaMenu;
    [SerializeField]
    GameObject keplerMenu;

    [Header("Character Prefabs")]
    [SerializeField]
    GameObject yeloriyaPrefab;
    [SerializeField]
    GameObject tulgaPrefab;
    [SerializeField]
    GameObject keplerPrefab;

    [Header("Selected Prefab")]
    [SerializeField]
    GameObject selectedCharacter;

    [Header("Network Status Panel Components")]
    [SerializeField]
    TextMeshProUGUI networkStatusText;

    [Header("Create Room Panel Components")]
    [SerializeField]
    InputField roomNameInput;
    [SerializeField]
    InputField maxPlayerNumberString;

    [Header("Room List Panel Components")]
    [SerializeField]
    GameObject newRoomPrefab;
    [SerializeField]
    GameObject roomListParent;

    [Header("Inside Room Panel Components")]
    [SerializeField]
    Text roomInfoText;
    [SerializeField]
    GameObject playerListPrefab;
    [SerializeField]
    GameObject playerListContent;
    [SerializeField]
    GameObject startGameButton;

    private Dictionary<string, RoomInfo> cachedRoomsList;
    private Dictionary<string, GameObject> currentRoomsList;
    private Dictionary<int, GameObject> playerListsGameObject;
    #endregion

    #region Unity Functions
    private void Start()
    {
        ShowPanel(enterGamePanel.name);
        cachedRoomsList = new Dictionary<string, RoomInfo>();
        currentRoomsList = new Dictionary<string, GameObject>();
        selectedCharacter = yeloriyaPrefab;

        //Loads scene according to master (host, room creator)
        //so, every player loads same screen
        PhotonNetwork.AutomaticallySyncScene = true;
    }

    private void Update()
    {
        networkStatusText.text = "Network Status: " + PhotonNetwork.NetworkClientState;
    }
    #endregion

    #region Custom Functions
    public void CreateRoomPanelActivate()
    {
        ShowPanel(createRoomPanel.name);
    }
    public void LobbyPanelActivate()
    {
        ShowPanel(lobbyPanel.name);
    }
    public void OnCreateRoomButtonClicked()
    {
        string _roomName = roomNameInput.text;

        if (string.IsNullOrEmpty(_roomName) || string.IsNullOrWhiteSpace(_roomName))
        {
            _roomName = "Room " + Random.Range(0, 100);
        }

        //customize room
        RoomOptions _roomOptions = new RoomOptions();
        _roomOptions.IsVisible = true;
        _roomOptions.MaxPlayers = (byte)int.Parse(maxPlayerNumberString.text);

        if (_roomOptions.MaxPlayers > 3)
        {
            _roomOptions.MaxPlayers = 3;
        }
        else if (_roomOptions.MaxPlayers <= 0)
        {
            _roomOptions.MaxPlayers = 3;
        }

        //create room
        PhotonNetwork.CreateRoom(_roomName, _roomOptions);
    }
    public void ConnectToPhotonServer()
    {
        if (PhotonNetwork.IsConnected == false && !string.IsNullOrWhiteSpace(PhotonNetwork.NickName))
        {
            PhotonNetwork.ConnectUsingSettings();
            ShowPanel(connectionStatusPanel.name);
        }
        else if (string.IsNullOrWhiteSpace(PhotonNetwork.NickName))
        {
            Debug.LogError("Player name is empty or contains only whitespace!");
        }
    }
    public void OnJoinRandomRoomButtonClick()
    {
        ShowPanel(joinRandomRoomPanel.name);
        PhotonNetwork.JoinRandomRoom();
    }
    public void RightCharacterButton()
    {
        if (yeloriyaMenu.activeInHierarchy == true)
        {
            keplerMenu.SetActive(true);
            selectedCharacter = keplerPrefab;

            yeloriyaMenu.SetActive(false);
        }
        else if (keplerMenu.activeInHierarchy == true)
        {
            tulgaMenu.SetActive(true);
            selectedCharacter = tulgaPrefab;

            keplerMenu.SetActive(false);
        }
        else if (tulgaMenu.activeInHierarchy == true)
        {
            yeloriyaMenu.SetActive(true);
            selectedCharacter = yeloriyaPrefab;

            tulgaMenu.SetActive(false);
        }
    }
    public void LeftCharacterButton()
    {
        if (yeloriyaMenu.activeInHierarchy == true)
        {
            tulgaMenu.SetActive(true);
            selectedCharacter = tulgaPrefab;

            yeloriyaMenu.SetActive(false);
        }
        else if (keplerMenu.activeInHierarchy == true)
        {
            yeloriyaMenu.SetActive(true);
            selectedCharacter = yeloriyaPrefab;

            keplerMenu.SetActive(false);
        }
        else if (tulgaMenu.activeInHierarchy == true)
        {
            keplerMenu.SetActive(true);
            selectedCharacter = keplerPrefab;

            tulgaMenu.SetActive(false);
        }
    }
    public void OnShowRoomButtonClicked()
    {
        if (!PhotonNetwork.InLobby)
        {
            PhotonNetwork.JoinLobby();
            Debug.Log("current lobby: " + PhotonNetwork.CurrentLobby);
        }

        ShowPanel(roomListPanel.name);
    }
    public void OnListRoomBackButtonClicked()
    {
        if (PhotonNetwork.InLobby)
        {
            PhotonNetwork.LeaveLobby();
        }
        ShowPanel(lobbyPanel.name);
    }
    public void OnLeaveRoomButtonClicked()
    {
        PhotonNetwork.LeaveRoom();
    }
    public void OnStartButtonClicked()
    {
        if (PhotonNetwork.IsMasterClient)
        {
            PhotonNetwork.LoadLevel("UDFGameScene");
        }
    }

    void ShowPanel(string panelName)
    {
        enterGamePanel.SetActive(panelName.Equals(enterGamePanel.name));
        connectionStatusPanel.SetActive(panelName.Equals(connectionStatusPanel.name));
        createRoomPanel.SetActive(panelName.Equals(createRoomPanel.name));
        lobbyPanel.SetActive(panelName.Equals(lobbyPanel.name));
        insideRoomPanel.SetActive(panelName.Equals(insideRoomPanel.name));
        roomListPanel.SetActive(panelName.Equals(roomListPanel.name));
        joinRandomRoomPanel.SetActive(panelName.Equals(joinRandomRoomPanel.name));
    }
    void OnJoinCreatedRoomInRoomList(string roomName)
    {
        if (PhotonNetwork.InLobby)
        {
            PhotonNetwork.LeaveLobby();
        }

        PhotonNetwork.JoinRoom(roomName);
    }
    void ClearCurrentRoomsList()
    {
        foreach (var roomListcurrentObjects in currentRoomsList.Values)
        {
            Destroy(roomListcurrentObjects);
        }

        currentRoomsList.Clear();
    }
    #endregion

    #region Photon Callbacks
    public override void OnConnected()
    {
        Debug.Log("Connected to Internet!");
    }
    public override void OnConnectedToMaster()
    {
        Debug.Log(PhotonNetwork.NickName + " is connected to Photon Server!");
        ShowPanel(lobbyPanel.name);

        Debug.Log("Current Room Number: " + PhotonNetwork.CountOfRooms);

        PhotonNetwork.JoinLobby();
    }
    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        Debug.Log(message);

        string _roomName = "Room " + Random.Range(10000, 100000);
        RoomOptions _roomOptions = new RoomOptions();
        _roomOptions.MaxPlayers = 3;

        PhotonNetwork.CreateRoom(_roomName, _roomOptions);
    }
    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        ClearCurrentRoomsList();

        foreach (RoomInfo room in roomList)
        {
            if (!room.IsOpen || !room.IsVisible || room.RemovedFromList)
            {
                if (cachedRoomsList.ContainsKey(room.Name))
                {
                    cachedRoomsList.Remove(room.Name);
                }
            }
            else
            {
                if (cachedRoomsList.ContainsKey(room.Name))
                {
                    cachedRoomsList[room.Name] = room;
                }
                else
                {
                    cachedRoomsList.Add(room.Name, room);
                }
            }
        }

        foreach (RoomInfo room in cachedRoomsList.Values)
        {
            GameObject _newRoomPrefab = Instantiate(newRoomPrefab);
            _newRoomPrefab.transform.SetParent(roomListParent.transform);
            _newRoomPrefab.transform.localScale = Vector3.one;

            _newRoomPrefab.transform.GetChild(0).GetComponent<Text>().text = room.Name;
            _newRoomPrefab.transform.GetChild(1).GetComponent<Text>().text = room.PlayerCount + " / " + room.MaxPlayers;
            _newRoomPrefab.transform.GetChild(2).GetComponent<Button>().onClick.AddListener(() => OnJoinCreatedRoomInRoomList(room.Name));

            currentRoomsList.Add(room.Name, _newRoomPrefab);
        }
    }
    public override void OnLeftLobby()
    {
        ClearCurrentRoomsList();
        cachedRoomsList.Clear();
    }
    public override void OnJoinedRoom()
    {
        Debug.Log(PhotonNetwork.NickName + " joined to " + PhotonNetwork.CurrentRoom.Name);
        ShowPanel(insideRoomPanel.name);

        //if we are admin in this room
        if (PhotonNetwork.LocalPlayer.IsMasterClient)
        {
            startGameButton.SetActive(true);
        }
        else
        {
            startGameButton.SetActive(false);
        }

        roomInfoText.text = "Room Name: " + PhotonNetwork.CurrentRoom.Name + " " + "Players/Max Players: " + PhotonNetwork.CurrentRoom.PlayerCount + "/" + PhotonNetwork.CurrentRoom.MaxPlayers;

        if (playerListsGameObject == null)
        {
            playerListsGameObject = new Dictionary<int, GameObject>();
        }

        //create player list
        foreach (Player player in PhotonNetwork.PlayerList)
        {
            //Debug.Log("list worked");
            GameObject _playerListObject = Instantiate(playerListPrefab);
            _playerListObject.transform.SetParent(playerListContent.transform);
            _playerListObject.transform.localScale = Vector3.one;

            _playerListObject.transform.GetChild(0).GetComponent<Text>().text = player.NickName;

            if (player.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber)
            {
                _playerListObject.transform.GetChild(1).gameObject.SetActive(true);
            }
            else
            {
                _playerListObject.transform.GetChild(1).gameObject.SetActive(false);
            }

            playerListsGameObject.Add(player.ActorNumber, _playerListObject);
        }

        //if duplicate selected players exist, then change it
        GameObject _mySelectedCharacter = selectedCharacter;
        List<GameObject> _selectableCharacters = new List<GameObject>
        {
            yeloriyaPrefab,
            keplerPrefab,
            tulgaPrefab
        };
        bool _checkerFlag = false;
        int _selectableCharNum = 0;
        ExitGames.Client.Photon.Hashtable _hashtable = new ExitGames.Client.Photon.Hashtable();
        _hashtable.Add("selectedCharacter", selectedCharacter.name);
        PhotonNetwork.LocalPlayer.SetCustomProperties(_hashtable);

        while (_checkerFlag == false)
        {
            _checkerFlag = true;
            foreach (var playerPair in PhotonNetwork.CurrentRoom.Players)
            {
                if (playerPair.Value.ActorNumber != PhotonNetwork.LocalPlayer.ActorNumber)
                {
                    if (_mySelectedCharacter == selectedCharacter)
                    {
                        _mySelectedCharacter = _selectableCharacters[_selectableCharNum];
                        _selectableCharNum++;
                        _checkerFlag = false;
                    }
                }
            }
        }
        selectedCharacter = _mySelectedCharacter;
        _hashtable["selectedCharacter"] = selectedCharacter.name;
        PhotonNetwork.LocalPlayer.SetCustomProperties(_hashtable);


        foreach (var playerPair in PhotonNetwork.CurrentRoom.Players)
        {
            if (playerPair.Value.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber)
            {
                Debug.Log("MY PLAYER " + playerPair.Value.NickName + " selected " + playerPair.Value.CustomProperties["selectedCharacter"]);
            }
            else
            {
                Debug.Log("Player " + playerPair.Value.NickName + " selected " + playerPair.Value.CustomProperties["selectedCharacter"]);
            }
        }
    }
    public override void OnPlayerEnteredRoom(Player newPlayer) //executes when different player joined room while we are in the room
    {
        //update room information
        roomInfoText.text = "Room Name: " + PhotonNetwork.CurrentRoom.Name + " Players/Max Players: " + PhotonNetwork.CurrentRoom.PlayerCount + "/" + PhotonNetwork.CurrentRoom.MaxPlayers;

        Debug.Log(newPlayer.NickName + " joined to " + PhotonNetwork.CurrentRoom.Name);
        Debug.LogWarning(PhotonNetwork.CurrentRoom.PlayerCount + " players in " + PhotonNetwork.CurrentRoom.Name);

        GameObject _playerListObject = Instantiate(playerListPrefab);
        _playerListObject.transform.SetParent(playerListContent.transform);
        _playerListObject.transform.localScale = Vector3.one;

        _playerListObject.transform.GetChild(0).GetComponent<Text>().text = newPlayer.NickName;

        if (newPlayer.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber)
        {
            _playerListObject.transform.GetChild(1).gameObject.SetActive(true);
        }
        else
        {
            _playerListObject.transform.GetChild(1).gameObject.SetActive(false);
        }

        playerListsGameObject.Add(newPlayer.ActorNumber, _playerListObject);
    }
    public override void OnPlayerLeftRoom(Player otherPlayer)
    {
        //update room information
        roomInfoText.text = "Room Name: " + PhotonNetwork.CurrentRoom.Name + " " + "Players/Max Players: " + PhotonNetwork.CurrentRoom.PlayerCount + "/" + PhotonNetwork.CurrentRoom.MaxPlayers;

        Destroy(playerListsGameObject[otherPlayer.ActorNumber].gameObject);
        playerListsGameObject.Remove(otherPlayer.ActorNumber);

        if (PhotonNetwork.LocalPlayer.IsMasterClient)
        {
            startGameButton.SetActive(true);
        }
    }
    public override void OnLeftRoom()
    {
        ShowPanel(lobbyPanel.name);

        foreach (GameObject playerListGameObject in playerListsGameObject.Values)
        {
            Destroy(playerListGameObject);
        }

        playerListsGameObject.Clear();
        playerListsGameObject = null;
    }
    public override void OnCreatedRoom()
    {
        Debug.Log("Room \"" + PhotonNetwork.CurrentRoom.Name + "\" is created!");
    }
    #endregion
}

Meet The Team

Prototyping Stage

Virtual Prototype – Made with Construct 2, not working