본문 바로가기

Starters 부트캠프/B - log

유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 21주차 학습 일지

지난 몇 주 그리고, 이번 주를 마지막으로 어몽어스의 기능들을 거의 모두 만들었고, 어몽어스 클론 코딩(?) 수업이 마무리되었다.

정말 오랜 기간 이 게임을 가지고 분투하고 나니, 이 작은 게임에도 얼마나 많은 정성이 들어가는지 이젠 확실하게 알았다.

 

개인적으로는 테트리스 게임에 추가로 몇 가지 기능을 붙여 새로운 게임을 얼추 만들었다. 

세상에 없던 게임을 만드는 거라 처음 생각만큼 재밌지 않은 부분도, 버그도 많았고, 완성도 아직 못 했다.

하지만, 늦어지더라도 끝까지 완성을 할 생각이다.

 

그리고, 이번 주부터 본격적으로 같은 교육생 중에 마음에 맞는 사람들과 같이 새롭게 게임 만들기에 돌입했다.

처음부터 새로운 게임을 만들기보다는 기존에 있는 게임을 만들기로 했고, '롱 빈터'라는 게임을 구현해보기로 했다.

 

(Longvinter 스팀 페이지)

 

Longvinter on Steam

Fish, farm, craft, gather, cook, loot or steal from other players and build a campsite or a village with your friends in an open-world multiplayer sandbox game without rules!

store.steampowered.com

 


 

Starters 21주차 강의 -  '어몽어스 - Vent / 투표 / 미션'

 

Vent

vent는 어몽어스에서 임포스터인 플레이어가 하수구를 통해서 맵의 여러 군데로 드나드는 기술이다.

사용하기 위해서는 먼저 하수구에 접촉을 하고, 누른 뒤에 활성화되는 화살표를 누르면서 원하는 지점에서 다시 하수구를 눌러 나오면 된다.

먼저, 근처에 다가가면 붉은 테두리가 활성화되고, 누르면 하수구에 들어가도록 Interact 기능은 다음과 같이 만들었다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Interactable : MonoBehaviour
{
    [SerializeField] GameObject miniGame;
    [SerializeField] GameObject highLight;

    private void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Player")
        {
            if (this.gameObject.tag == "Vent" && other.GetComponent<AU_PlayerController>().isImposter)
            {
                if (other.GetComponent<AU_PlayerController>().photonView.IsMine)
                {
                    highLight.SetActive(true);
                }
            }
            else if (this.gameObject.tag != "Vent" && other.GetComponent<AU_PlayerController>().photonView.IsMine)
                highLight.SetActive(true);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.tag == "Player")
        {
            highLight.SetActive(false);
            if (miniGame.activeSelf) miniGame.SetActive(false);
        }
    }

    public void PlayMiniGame()
    {
        miniGame.SetActive(true);
        Debug.Log("Minigame");
    }
}

 

interactable 클래스는 벤트 외에도 아래 미션을 할 수 있는 오브젝트에도 사용할 것이라 트리거 시 조건을 더 달았다.

첫 번째로 오브젝트의 태그가 vent인지,

두 번째로 접촉한 플레이어의 isImposter가 true인지

 

이렇게 하면, 벤트 오브젝트에 임포스터가 접촉했을 때와 벤트 오브젝트가 아닐 때에 임포스터가 아닌 플레이어가 접촉했을 때의 두 가지 경우만 highLight가 켜지고, 미니 게임(혹은 벤트 화살표) 패널이 열리게 된다.

 

다음으로 다른 벤트 지점을 클릭할 수 있도록 벤트 화살표가 나오는 패널을 만들어준다.

물론 모든 벤트 지점에 해당하는 패널들을 모두 만들어 주었고, 그때마다 화살표의 방향도 달라지도록 했다.

 

VentPanels에 담겨있는 각 벤트 패널들

 

애로사항이 있었다면, 플레이어 프리팹 안에 카메라가 달려있어서 게임을 실행하기 전까지는 화살표의 방향이 맞는지 확인하기가 힘들었다.

 

vent에서 나갈 수 있는 버튼도 추가

 

물론 벤트에서 나갈 수 있는 버튼도 추가하고, 

각 화살표에는 아래의 클래스를 달아 벤트 기능을 구현했다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AU_Vent : MonoBehaviour
{
    [SerializeField] GameObject currentPanel;
    [SerializeField] GameObject nextPanel;
    [SerializeField] Transform nextPos;

    public void ChangeVent()
    {
        currentPanel.SetActive(false);
        nextPanel.SetActive(true);
        AU_PlayerController.localPlayer.transform.position = nextPos.position;
    }

    public void VentExit()
    {
        currentPanel.SetActive(false);
        AU_PlayerController.localPlayer.VentExit();
    }
}

 

각 화살표에는 현재의 패널과 화살표를 눌렀을 때에 이동하는 패널을 등록하게 하였고, 

마찬가지로 눌렀을 때에 이동하는 위치도 등록하게 하였다.

 

그리고, 화살표에는 onClick에 ChangeVent()를, ExitButton에는 VentExit() 함수를 등록하였다.

ChangeVent()는 위에서 지정한 현재 패널을 끄고, 화살표에 맞는 다음 위치로 이동해 다음 패널을 키는 기능이고,

VentExit()는 그냥 현재 패널을 끄고, 플레이어 컨트롤러의 VentExit()를 실행하도록 했다. 플레이어의 VentExit()는 vent에서 나오는 애니메이션과 이름표 등을 다시 키는 함수이다.

(localPlayer는 pv.IsMine인 플레이어 캐릭터의 AU_PlayerController 클래스를 static으로 넣어주었다.)

 

시체 신고, 투표

투표 기능은 생각보다 구현하기 어려웠던 기능이었다.

일단 스크립트를 보자.

 

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

public class AU_VotePlayer : MonoBehaviour
{
    [SerializeField] Text nickname;
    [SerializeField] Text statusText;
    int actorNumber;
    public int Actornumber { get { return actorNumber; } }
    Button voteBtn;
    VoteManager voteManager;

    void Awake()
    {
        voteBtn = GetComponentInChildren<Button>();
        voteBtn.onClick.AddListener(OnVotePressed);
    }

    void Init()
    {
        voteBtn = GetComponentInChildren<Button>();
        voteBtn.onClick.AddListener(OnVotePressed);
    }

    void OnVotePressed()
    {
        // 버튼을 누르면 해당 플레이어 프리팹의 ActorNumber가 인자로 넘어감 
        voteManager.CastVote(Actornumber);
    }

    // 투표 창 초기화 
    public void Initialize(Player player, VoteManager vm)
    {
        actorNumber = player.ActorNumber;
        nickname.text = player.NickName;
        statusText.text = "Not Voted";
        voteManager = vm;
    }

    // 플레이어가 투표를 받았는지 아닌지 상태 표시를 바꿈 
    public void UpdateStatus(string status)
    {
        statusText.text = status;
    }

    // 투표 후에 투표를 이중으로 못하게 막는 역할을 하는 함수 
    public void ToggleButton(bool isInteractable)
    {
        Init();
        voteBtn.interactable = isInteractable;
    }

    void Update()
    {

    }
}

 

먼저 투표 페이지에 생성되는 각 플레이어 칸에 붙게 될 스크립트이다.

 

채팅창 뒤로 보이는 각 플레이어 칸들에 위 스크립트가 붙는다.

 

초기화 함수(Init())를 통해 플레이어 이름, 투표 현황 등을 가져오고, 

투표 버튼을 누르면(해당 플레이어의 투표 버튼) 해당 플레이어의 number를 가지고, voteManager의 castVote 함수를 실행하게 된다.

 

투표 후에는 voteManager에서 모든 투표 칸에 대해서 ToggleButton()을 flase로 실행해서 중복 투표를 막게 된다.

 

아래는 VoteManager 클래스이다.

 

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

public class VoteManager : MonoBehaviourPun
{
    [SerializeField] GameObject votePanel;
    [SerializeField] GameObject kickoutPanel;
    [SerializeField] Text kickoutText;
    [SerializeField] AU_VotePlayer votePlayerPrefab;
    [SerializeField] Transform votePlayerContainer;

    // 투표 플레이어 총 리스트 
    List<AU_VotePlayer> votePlayerList = new List<AU_VotePlayer>();
    // 투표한 사람 리스트 
    List<int> playersVotedList = new List<int>();
    // 표를 받은 사람 리스트 
    List<int> playersHaveBeenVotedList = new List<int>();
    // 투표로 쫒겨나간 사람 리스트 
    List<int> playerKickoutList = new List<int>();
    bool hasVoted;

    [SerializeField] Button skipBtn;

    public static VoteManager instance;
    public PhotonView deadBodyPV;
    List<int> reportedBodyList = new List<int>();
    public List<int> killedBodyList = new List<int>();

    void Awake()
    {
        instance = this;
    }

    public void ReportDeadBody()
    {
        if (deadBodyPV == null) return;
        if (reportedBodyList.Contains(deadBodyPV.OwnerActorNr)) return;
        photonView.RPC("ReportBody_RPC", RpcTarget.All, deadBodyPV.OwnerActorNr);
    }

    void ListInit()
    {
        playersVotedList.Clear();
        playersHaveBeenVotedList.Clear();
        hasVoted = false;
        ToggelAllBtn(true);
        PopulatePlayerList();
    }

    [PunRPC]
    public void ReportBody_RPC(int actorNumber)
    {
        reportedBodyList.Add(actorNumber);
        // 리스트들 초기화 후에 창을 열기 
        ListInit();
        votePanel.SetActive(true);
    }

    void PopulatePlayerList()
    {
        while (votePlayerContainer.childCount != 0)
        {
            Destroy(votePlayerContainer.GetChild(0));
        }
        votePlayerList.Clear();

        foreach (KeyValuePair<int, Player> player in PhotonNetwork.CurrentRoom.Players)
        {
            // if (player.Value.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber) continue;
            // if (reportedBodyList.Contains(player.Value.ActorNumber)) continue;
            // if (playerKickoutList.Contains(player.Value.ActorNumber)) continue;

            AU_VotePlayer newVotePlayer = Instantiate(votePlayerPrefab, votePlayerContainer);
            newVotePlayer.Initialize(player.Value, this);

            if (!reportedBodyList.Contains(player.Value.ActorNumber) && !playerKickoutList.Contains(player.Value.ActorNumber) && !killedBodyList.Contains(player.Value.ActorNumber))
            {
                votePlayerList.Add(newVotePlayer);
            }
            else
            {
                newVotePlayer.gameObject.GetComponent<Image>().color = Color.grey;
                if (player.Value.ActorNumber == PhotonNetwork.LocalPlayer.ActorNumber)
                {
                    hasVoted = true;
                    ToggelAllBtn(false);
                }
            }
        }

        if (!reportedBodyList.Contains(PhotonNetwork.LocalPlayer.ActorNumber) && !playerKickoutList.Contains(PhotonNetwork.LocalPlayer.ActorNumber) && !killedBodyList.Contains(PhotonNetwork.LocalPlayer.ActorNumber))
            ToggelAllBtn(true);
    }

    void ToggelAllBtn(bool isOn)
    {
        skipBtn.interactable = isOn;
        foreach (AU_VotePlayer votePlayer in votePlayerList)
        {
            votePlayer.ToggleButton(isOn);
        }
    }

    public void OnSkipBtn()
    {
        // -1은 스킵한 것으로 취급 
        CastVote(-1);
    }

    void CountingVotedNumbers(int remainingPlayers)
    {
        Dictionary<int, int> playerVoteCount = new Dictionary<int, int>();

        // 투표받은 리스트에서 계산 시작 
        foreach (int votedPlayer in playersHaveBeenVotedList)
        {
            // 만약 계산 리스트에 없으면, 넣고 한 표 추가 
            // 있으면 그냥 한 표 추가 
            if (!playerVoteCount.ContainsKey(votedPlayer))
            {
                playerVoteCount.Add(votedPlayer, 0);
            }
            playerVoteCount[votedPlayer]++;
        }
        int mostVotedPlayer = -1;
        // 그냥 가장 낮은 값으로 하려로 MinValue 씀 (-1000 같이 엄청 큰 거면 상관 X)
        int mostVotes = int.MinValue;
        foreach (KeyValuePair<int, int> playerVote in playerVoteCount)
        {
            if (playerVote.Value > mostVotes)
            {
                mostVotes = playerVote.Value;
                mostVotedPlayer = playerVote.Key;
            }
        }

        // int / int ==> 무조건 버림으로 계산되어서 결과가 나옴 
        float halfNum = (remainingPlayers / 2f);
        if (mostVotes >= halfNum)
        {
            photonView.RPC("KickPlayer_RPC", RpcTarget.All, mostVotedPlayer);
        }
    }

    // 프리팹에서 투표 버튼 --> 이 함수 실행 
    public void CastVote(int targetActorNumber)
    {
        if (hasVoted) return;
        hasVoted = true;
        // 투표 한 번 누르면 모든 버튼 활성화 off (중복 투표 방지)
        ToggelAllBtn(false);
        // targetActorNumber = 투표를 받게 되는 사람 넘버
        photonView.RPC("CastVote_RPC", RpcTarget.All, PhotonNetwork.LocalPlayer.ActorNumber, targetActorNumber);
    }

    // 자신과 투표를 할 사람의 넘버를 인자로 받음 
    [PunRPC]
    public void CastVote_RPC(int actorNumber, int targetActorNumber)
    {
        int remainingPlayers = PhotonNetwork.CurrentRoom.PlayerCount
            - (playerKickoutList.Count + killedBodyList.Count);

        foreach (AU_VotePlayer votePlayer in votePlayerList)
        {
            // 스킵한 사람은 스킵했다고 뜨고, 투표한 사람은 투표했다고 상태창에 뜸 
            if (votePlayer.Actornumber == actorNumber)
            {
                votePlayer.UpdateStatus(targetActorNumber == -1 ? "스킵함" : "투표함");
                break;
            }
        }

        // 투표를 한 리스트에 내가 없다면,
        if (!playersVotedList.Contains(actorNumber))
        {
            // 투표한 리스트에 나를 추가하고, 투표를 받은 리스트에 해당 플레이어 추가 
            playersVotedList.Add(actorNumber);
            playersHaveBeenVotedList.Add(targetActorNumber);
        }

        // 계산은 방장만 함 
        if (!PhotonNetwork.IsMasterClient) return;

        // 전원이 투표하기 전에 계산하지 않도록 함 
        if (playersVotedList.Count < remainingPlayers) return;

        CountingVotedNumbers(remainingPlayers);
    }

    [PunRPC]
    public void KickPlayer_RPC(int actorNumber)
    {
        votePanel.SetActive(false);
        kickoutPanel.SetActive(true);
        string playerNick = string.Empty;
        if (actorNumber != -1) playerKickoutList.Add(actorNumber);
        foreach (KeyValuePair<int, Player> player in PhotonNetwork.CurrentRoom.Players)
        {
            if (player.Value.ActorNumber == actorNumber)
            {
                playerNick = player.Value.NickName;
            }
        }
        kickoutText.text = actorNumber == -1 ? "아무도 퇴출되지 않았습니다." : $"플레이어 {playerNick}이(가) 퇴출당했습니다.";
        StartCoroutine(FadeKickoutPanel(actorNumber));
    }

    private IEnumerator FadeKickoutPanel(int actorNumber)
    {
        yield return new WaitForSeconds(2.5f);
        if (PhotonNetwork.LocalPlayer.ActorNumber == actorNumber)
        {
            AU_PlayerController.localPlayer.photonView.RPC("Die", RpcTarget.All);
        }
        yield return new WaitForSeconds(2f);
        kickoutPanel.SetActive(false);
        DestroyAllBodies();
    }

    void DestroyAllBodies()
    {
        foreach (GameObject body in GameObject.FindGameObjectsWithTag("Body"))
        {
            AU_PlayerController.localPlayer.DestroyBody(body.transform);
            Destroy(body);
        }
    }

    void Update()
    {

    }
}

 

함수가 상당히 많고 복잡해 보인다.

내가 보기에도 복잡해 보이긴 하지만, 크게 어려운 내용은 없다.

 

플레이어가 시체를 발견하고 R키를 누르면, 먼저 ReportDeadBody()가 실행되고, 곧이어 ReportedBody_RPC()가 실행된다.

이는 리포트 명단, 추방 명단 등을 초기화하고, 플레이어 수만큼 투표 칸, 투표권을 주는 역할을 한다.

 

그리고, PopulatePlayerList()가 바로 투표 칸(권)을 알맞게 생성하는 역할을 한다.

가장 구현에 애를 먹었던 함수가 이 함수였는데, 킬을 당하거나, 추방당한 사람 즉, 이미 죽은 사람들은 칸은 만들되 투표를 못 하도록 해야 하고, 나머지는 칸과 투표권 모두 주어야 하는 기능을 만드는데 엄청 머리를 쥐어뜯었던 것 같다.

 

CastVote()와 CastVoteRPC 함수는 특정 칸에 투표를 하거나 스킵 버튼을 누르면 지명한 플레이어의 넘버와 자신의 넘버를 동시에 넘겨주게 된다.

이로써 투표해야 하는 리스트와 투표당한 리스트에 각 플레이어를 넣어주어서 투표를 다 하였는지 그리고, 누가 과반수 이상의 가장 많은 지목을 받았는지 구해서 (CountingVoteNumbers) 드디어 추방시키게 된다. (KickPlayer_RPC)

 

이후 코루틴으로 플레이어를 추방시키는 이미지를 띄우고, 추방당한 플레이어 그리고, 이전에 있던 모든 시체들을 지우게 된다. 

 

개인 미션 - '카드 인식하기'

카드 인식에도 맨 위에서 설명한 Interactable 클래스를 사용했다.

Vent 오브젝트가 아니고, 임포스터가 아닌 플레이어가 접근했을 때에 해당 오브젝트에 파란 라이트가 켜지도록 하였고, 클릭 시에 미니게임(미션 패널)이 열리도록 하였다.

 

미션 패널

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class CardMission : MonoBehaviour
{
    [SerializeField] Transform Card;
    [SerializeField] Text missionText;
    [SerializeField] Text clearText;
    [SerializeField] Image redLight;
    [SerializeField] Image greenLight;
    
    public float slideTime = 0;
    public float correctTime = 1f;
    public float offsetTime = 0.2f;
    bool isActive = false;
    bool isClick = false;
    bool isMove = false;
    bool isClear = false;
    Vector2 startPos;
    Vector2 startMPos;

    void Awake()
    {
        EventTrigger et = Card.GetComponent<EventTrigger>();

        EventTrigger.Entry newEntry_Down = new EventTrigger.Entry();
        newEntry_Down.eventID = EventTriggerType.PointerDown;
        newEntry_Down.callback.AddListener((data) => { OnClickCard((PointerEventData)data); });
        et.triggers.Add(newEntry_Down);

        EventTrigger.Entry newEntry_Up = new EventTrigger.Entry();
        newEntry_Up.eventID = EventTriggerType.PointerUp;
        newEntry_Up.callback.AddListener((data) => { isClick = false; isMove = false; });
        et.triggers.Add(newEntry_Up);

        EventTrigger.Entry newEntry_Drag = new EventTrigger.Entry();
        newEntry_Drag.eventID = EventTriggerType.BeginDrag;
        newEntry_Drag.callback.AddListener((data) => isMove = true);
        et.triggers.Add(newEntry_Drag);
    }

    void OnClickCard(PointerEventData data)
    {
        if (!isActive)
        {
            Card.GetComponent<Animator>().SetBool("isClick", true);
            Invoke("SetStartPosition", 1.2f);
        }
        else
            isClick = true;
    }

    void SetStartPosition()
    {
        isActive = true;
        startPos = Card.GetComponent<RectTransform>().anchoredPosition;
        Card.GetComponent<Animator>().enabled = false;
        Card.GetComponent<Animator>().SetTrigger("isWait");
        Card.GetComponent<RectTransform>().anchoredPosition = startPos;
        Card.GetComponent<RectTransform>().sizeDelta = new Vector2(392.26f, 238.46f);
    }

    void CloseMiniGame()
    {
        this.gameObject.SetActive(false);
    }

    void Update()
    {
        if (!isActive || isClear) return;

        if (isClick && !isMove)
        {
            startMPos = Mouse.current.position.ReadValue();
        }
        else if (isClick && isMove)
        {
            redLight.gameObject.SetActive(true);

            slideTime += Time.deltaTime;
            float moveX = (Mouse.current.position.ReadValue().x - startMPos.x) < 0 ? 0 : (Mouse.current.position.ReadValue().x - startMPos.x);
            Card.GetComponent<RectTransform>().anchoredPosition = startPos + new Vector2(moveX, 0);

            if ((Card.GetComponent<RectTransform>().anchoredPosition - startPos).magnitude >= 800f)
            {
                if (slideTime < correctTime - offsetTime)
                {
                    missionText.text = "너무 빠르게 긁으셨습니다.";
                    slideTime = 0;
                    isClick = false;
                }
                else if (slideTime > correctTime + offsetTime)
                {
                    missionText.text = "너무 느리게 긁으셨습니다.";
                    slideTime = 0;
                    isClick = false;
                }
                else
                {
                    isClear = true;
                    slideTime = 0;
                    missionText.text = "카드가 인식되었습니다.";

                    Card.GetComponent<Animator>().SetBool("isClick", false);
                    Card.GetComponent<Animator>().enabled = true;
                    clearText.text = "임무 완료!";
                    greenLight.gameObject.SetActive(false);
                    Invoke("CloseMiniGame", 2.2f);
                }
            }
        }
        else
        {
            Card.GetComponent<RectTransform>().anchoredPosition = startPos;
            greenLight.gameObject.SetActive(true);
            redLight.gameObject.SetActive(false);
        }
    }
}

 

먼저 bool값으로 isStart와 isMove가 있다.

isStart는 카드를 지갑에서 눌렀을 때에 true가 되면서 OnClickCard()가 실행된다.

해당 함수는 카드가 지갑에서 카드 슬롯(?)의 시작 지점으로 이동하는 애니메이션을 실행시키는 함수이다.

 

그리고, 다시 그 카드를 드래그 시작하면, isMove가 true 되면서 마우스 x 위치의 변화만큼 카드를 긁게 된다.

(추가로 빨간 불도 이동하는 동안 잠깐 꺼진다.)

이때에부터 시간이 도는데, 카드를 긁고 난 위치에 올 때까지의 시간을 측정한다.

해당 시간이 만약에 적절한 시간 + 오차보다 크거나 작은 경우 혹은 중간에 카드를 놓은 경우에 오류를 띄우고, 다시 원 위치로 카드를 돌린다.

당연히 적절한 시간에 카드를 스캔하면, 카드를 지갑에 다시 넣고, 미션 완료를 띄운다.

 

투표, 미션 동영상

 


 

개인 프로젝트 -  '테트리스 vs 광부들 / Longvinter 팀 프로젝트'

 

테트리스 게임

한 명의 플레이어가 테트리스를 하는 동안 나머지 플레이어가 테트리스 블록을 캐어서 점수를 획득하는 게임을 기획하고 만들었다.

1대 다의 멀티 플레이 게임이고, 현재는 대충 플레이가 가능한 정도로 개발되었다.

당장 모든 기능과 코드를 정리해서 보여주긴 어렵지만(특히나 너무 코드도 난잡해서 정리가 필요할 것 같다.)

언젠가는 다 완성하고 다시 정리하도록 하겠다.

 

이렇게 말하고도.. 매번 완전히 완성하지 못하고 다음 게임으로 넘어가는 것은 내 고질적인 문제인 것 같다.

그래도 정말 대충이라도 완성을 지어서 보여주도록 하겠다.

 

(버그가 생겨서 플레이가 제대로 안 되는데, 빠르게 고쳐서 영상을 업로드할 예정ㅠ)

 

Longvinter 팀 프로젝트

내년부터는 스타터스 역시 팀 프로젝트가 시작된다.

프로젝트 주간에는 분명 깃 등의 협업 툴을 사용하게 될 텐데 지금부터 이에 적응하고, 실제로 협업해서 새로운 프로젝트를 미리 해보는 것은 어떤가 하여 해당 프로젝트를 기획하고 시작하게 되었다.

 

롱 빈터는 위에 스팀 페이지에서 확인할 수 있는데, 닌텐도 사의 동물의 숲 같은 귀여운 캐릭터들이 섬에서 집을 짓고, 채집을 하다가 서로 샷건을 들고 싸운, 약탈(?)을 하는 캐주얼한 게임이다.

포톤을 통해 멀티 플레이 게임 개발을 공부하고 있는 지금 만들기에 좋은 게임인 것 같아서 해당 게임을 구현해보는 것으로 목표를 잡았고, 당장 첫 번째 주의 역할을 정했다.

 

나는 포톤을 통해 서버 접속, 방 만들기, 입장 등을 구현하는 역할을 맡았고, 다른 분들은 각각 UI 디자인, 캐릭터 조작 + 애니메이션 등의 각각의 역할을 맡게 되었다. 

 

아 그리고, 서로 변수나 함수의 이름 등을 사전에 정해두고, 기능 역시 정리해두고 개발에 착수했다.

 

 

당장은 서버에 입장하고 방을 만들고 들어오는 것까지 기능 구현만 한 상태이다.

기능에 대한 구현은 사실 일주일 보다도 훨씬 더 빨리 구현하지 않을까 싶었는데 문제는 깃허브에 있었다.

 

바로 깃허브에 유니티 프로젝트를 업로드하고, 코밋, 병합하는 과정에서 충돌이 엄청나게 일어나는 것이 문제였다...

애초에 gitignore도 제대로 못 써먹기도 하였고, 처음에는 각자 다른 프로젝트를 생성해서 합치려고 하여서 충돌이 더 심하게 많이 났었다.

 

현재는 대충 이 문제에 대해서 해결하고, 첫 번째 병합을 완료했지만, 앞으로 깃허브와 완전히 친해지기 전까지는 다시 그런 문제에 많이 부딪힐 것 같다.

여하튼 이 프로젝트는 당장은 12월 말까지 계속될 예정이라 다음 주, 그다음 주 계속 블로그에도 후기를 적도록 하겠다.

 

 

 

유데미 코리아 바로가기 : 

 

Udemy Korea - 실용적인 온라인 강의, 글로벌 전문가에게 배워보세요. | Udemy Korea

유데미코리아 AI, 파이썬, 리엑트, 자바, 노션, 디자인, UI, UIX, 기획 등 전문가의 온라인 강의를 제공하고 있습니다.

www.udemykorea.com

 

💡 본 포스팅은 유데미-웅진씽크빅 취업 부트캠프 유니티 1기 과정 후기로 작성되었습니다.