본문 바로가기

Starters 부트캠프/B - log

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

이번 주는 다시 이전 강사님이 복귀하셔서 현업과 관련된 12월의 강의 커리큘럼을 시작하였다.

이전에 좋은 기억을 가지고 아쉽게 가셨던 강사님을 뵈어서 기분이 좋았고, 우리 교육생들의 피드백을 바로바로 받아서 커리큘럼을 수정하시는 모습이 인상 깊었다.

 

팀 프로젝트 역시 꽤 선방한 한 주였다.

전 주에부터 시작했던 기능 연결, 씬 연결이 모두 잘 되었고, 가장 문제였던 동기화 문제도 거의 마무리했다.

더불어 원 게임에 있는 오브젝트들을 잘 찾아서 가져와서 프리팹으로 만드는 작업도 잘 되어서 원 게임의 기능들을 더 세밀하게 따라 하는 게 가능해졌다.

 


 

스타터스 24주차 수업 - '협업 툴 설명' 

24주 차의 수업은 우리가 가장 궁금하고 힘들어했던, 깃허브 및 협업 툴, 방식 등이 주를 이루었다.

특히나 지금 프로젝트를 하는 과정이니만큼 더더욱 챙겨 들어야 할 중요한 강의였다.

그걸 아시는지 강사님도 이론부터 실전에서 사용하는 명령어, 툴 모두 시간을 꽉 채워서 가르쳐 주셨다.

 

협업 방식

협업 혹은 기업에서 일을 처리하는 방식으로 회사 전체가 돌아가는 구조이니 만큼 우리도 알 필요가 있었다.

또한, 회사가 아니더라도 우리가 현재 하는 프로젝트에서도 더 효율적으로 빠르게 프로젝트를 만들기 위해서 필요한 것들을 많이 알게 되었다.

 

폭포수 모델

고전적인 모델로 프로젝트의 이슈를 기능 단위로 쪼개지 않고, 전체를 한 묶음으로 해결하려고 하는 방식이다.

즉, 설계부터 프로그램이 정상적으로 실행, 배포되는 모든 것이 하나의 묶음이 될 수도 있고, 이 절차가 각각 차례로 되어서 다음 기능을 맡은 사람에게 계단식(폭포식)으로 내려온다 하여 폭포수 모델이라고 하는 듯하다.

 

이 경우는 프로그램의 배포까지 모두 끝난 단계라면, 효율적일 수 있는 모델이다.

어차피 문제의 해결이 곧바로 다시 배포할 수 있는 상황을 만들어야 하기 때문에 기능을 쪼갤 이유가 없을 수 있기 때문이다.

하지만 보통의 경우에는 리소스가 너무 많이 들어가고, 이전 단계에서 해결이 안 되면, 다음 단계가 진행이 안되니 더 시간이 지체될 수 있다.

 

정리하면...

1. 리소스가 너무 많이 들어감
2. 여러 기능을 한 번에 묶어 어떤 단계에서 해결이 되지 않으면 다음 프로세스로 진행이 불가능함
3. 유지보수, 배포 시에는 더 나은 모델이 될 수 있음 

 

Agile 

유닛이라는 하나의 기능 별로 잘라서 해결하는 방식이다.

각 기능별로 담당자, 보고자, 관찰자를 따로 정해두고, 매일 혹은 매 주기마다 각각 해결할 사항들을 정해서 해결하는 방식이다.

이는 당연히 단계가 각 기능 별로 돌아가므로 전체 프로젝트의 단계에 따라서 시간이 지체될 염려가 적다.

또한, unit test라고, 기능 별로 테스트를 하면서 디버깅 속도 역시 향상할 수 있다.

 

정리하면...

1. 기능 별로 구분해서 담당자, 보고자, 관찰자를 정하고, 각 주기마다 각자 할 일을 정해서 수행
2. 기능별 테스트로 디버깅 속도가 빠름
3. 깃허브 등을 통해서 각자 수행한 기능을 합치게 됨

 

그래서 보통의 회사, 협업에서는 Agile 모델을 많이 사용하는 추세인데, 이를 위해서는 각각의 역할이 매우 중요하다.

 

Agile 모델에서의 단계 및 역할

모든 일을 시작하기에 앞서서 당연하지만, Agile 모델은 기능을 쪼개야 할 의무가 있다.

그래서 프로젝트 오너 즉, PO은 전체적인 프로젝트뿐만 아니라 각각의 기능을 나누는 것을 가장 먼저 해야 한다.

 

그러고 나서는 각 기능의 담당자, 팀원, 보고자, 관찰자 등을 정하게 되는데 이는 큰 규모의 프로젝트일수록 더 자세히 나눠진다.

그리고, 항상 아침에 회의로 오늘 작업에 대해 정하기 시작하는데 보통은 매일의 작업 스케줄, 과정을 관리하는 스크럼 매니저(SM)의 리드 하에 Plan Poker 방식으로 진행된다.

 

플랜 포커는 각자 스크럼 매니저를 통해서 백로그(이전 작업까지의 설명과 함께 오늘 작업의 큰 틀이 되는 이슈 설정)와 그 백로그에서 자신이 맡을 사이클(세부적인 백로그 안에서의 기능), 기한, 중요도 등을 전달하고, 스크럼 매니저의 리드하에 작업을 조율하는 과정이다.

 

우리 역시 프로젝트를 하는 데에 이 같은 Agile 모델을 한 번 채택하기로 했고, 거기서 난 스크럼 매니저(SM)를 하기로 했다.

 

요약) PO과 SM의 차이

PO
- 프로젝트의 전체 구상, 기술 유닛 분할
- 프로젝트 외적인 자본 문제 등의 외압을 해결
- 백로그를 정하고, 큰 단위의 우선순위를 정함

SM
- 개발에 초점이 맞춰진 리더
- 하루, 한 주 등의 기간 내의 스크럼(작업)의 우선순위를 정함
- 각 사이클을 조율하고, 보고를 받음

주로 PO는 큰 프로젝트의 계획 외에는 외부적인 관계를 처리하고, SM이 개발 단위의 대부분을 리드하게 된다.

 

요약) Agile 모델 용어

Scrum 방식 : 기능 단위로 나눠서 중요도를 나눈 다음, 크기에 따라 백로그, 스프린트로 묶어서 일을 진행하는 방식으로, 팀원의 일 개수에는 제한이 없고, 기간에 대한 제한만 둔다.

Kanban 방식 : 열과 행으로 나눠서 일을 진행한다. 열은 진행 단계로 todo(할 일), In-Progress(진행 중), Done(완료), Blocked(미뤄둔 작업) 등으로 단계를 나누고, 행은 우선순위로 중요한 작업부터 위에 게시한다. Scrum 방식과 다르게 일 개수의 제한을 두고, 기간에 대한 제한은 따로 정해두지 않는다.

Back Log : 백로그는 '캐릭터 조작' 등 하나의 큰 기능 범위를 정의하는 것을 말한다.

Sprint : 백로그의 하위로 세부적인 기능을 정의하는 것을 말한다. 보통 스크럼 방식에서는 하나의 스프린트의 기간 제한을 1달 이내로 잡는다. ex) 물약 먹기 등

Daily Scrum : 스크럼 방식에서의 하루 개발의 범위로 이를 위해 아침마다 회의가 이루어진다. 회의에서는 보통 어제(이전) 작업 보고, 작업해야 할 기능 보고, 이슈 보고 등이 이루어진다.

Plan Poker : 참여하는 각 사람들이 시간/일자, 중요도, 기간 등이 적힌 카드를 제시하고 SM 주도 하에 서로 타협을 보는 방식

 

아래는 Kanban 방식으로 계획을 짠 모습이다.

 

Kanban 방식

 

깃허브

깃허브는 대부분의 회사, 협업에서 사용하는 버전 관리 툴이자 클라우드 서비스이다.

그만큼 잘 알아두는 것이 중요하고, 특히나 프로젝트 파일들을 관리하는 만큼 규칙 등을 잘 알고 이행하는 것이 매우 중요하다.

 

깃허브 커밋 규칙

깃허브를 사용하면, 매일 하나의 스프린트를 해결할 때마다 혹은 다른 이슈가 생길 때마다 매번 커밋을 하게 된다.

이 경우 상대방이 잘 알아볼 수 있도록 작성하는 것이 상당히 중요하다.

 

Update Character Movements #001
// 제목은 Camel 혹은 Pascal 방식으로 최대한 영어로 적고, #이슈 번호 적기
------------------------------------------------------------------

Detail mention... 
// 디테일한 내용 작성

Fixed #003
// 연관된 이슈 작성
------------------------------------------------------------------

 

깃허브 직성 규칙

깃허브는 프로젝트 전체 레포지토리 내에 root가 되는 main 브런치와 다른 여러 브런치들로 나누어져 있다.

이 브런치는 최대한 기능, 배포, 단계에 따라서 만들어야 한다.

 

- Main (Release 혹은 Live 등으로 정해둔 경우도 많다.)
   - dev_Android_LoginService
   - dev_IOS_Service_Beta_0.3
...

 

회사 규모의 프로젝트에서는 각각의 브런치마다 젠킨스 등을 이용해서 자동으로 배포를 하기도 한다.

이 경우에는 위와 같이 Beta, Alpha, Release 등의 이름이 붙기도 한다.

여담으로 이 버전에 맞는 정품과 복제품을 구별하기 위해서 각 버전 별로 배포 시에 GUID(Global Unique ID)를 넣어두는데, 이는 32글자의 A-Z 혹은 0-9로 이루어진 문자열이다. (버전 생성 시에 랜덤으로 생성된다.)

즉, 32의 36승의 경우의 해당 문자와 실행한 프로그램이 다른 경우 이를 복제품으로 판별하게 된다.

 

Conflict의 경우의 수

협업을 통해서 merge나 pull, push 등을 하면, 서로의 작업이 겹치거나 차이가 생겨서 Conflict(충돌)가 나는 경우가 비일비재하게 생겨난다.

보통의 경우에는 다음과 같은 두 가지의 경우로 conflict가 일어나게 된다.

 

  1. 합쳐질 브런치 안에 충돌하는 스크립트의 코드 자체가 서로 다른 경우
  2. 합쳐질 브런치들의 버전이 다른 경우 - 보통 behind, above 등으로 마스터 브런치와 어느 정도 차이가 남을 명시해준다.

 

이런 충돌은 웬만하면, 피하는 것이 좋지만, 아무리 열심히 해도 매번 모든 충돌을 피하기란 쉽지가 않다.

그렇기에 우리는 충돌 시에 이 세 가지를 잘 생각해서 merge 해야 한다.

 

  1. 겹치거나 다른 코드를 두 파일의 내용 중에서 선택적으로 가져오기. 
  2. 두 파일의 내용을 전부 가져오기.
  3. 두 파일의 내용 중에 한쪽만 선택하고, 다른 한 편은 완전히 무시하기.

 

1번은 겹치는 부분을 각 파일에서 맞는 코드들로 선택을 하여 합치는 방법으로 우리 프로젝트에서 각자 같은 프리 팹을 만졌을 경우에도 이런 방식으로 충돌을 해결했었다.

 

3번은 1번과 비슷하게 생각되지만, 한쪽의 파일로 완전히 적용시키고, 다른 한쪽은 아무 내용도 포함시키지 않고 버리는 것을 의미한다. 이는 discard를 통해 해당 파일의 change를 무시해버리는 것으로, 한쪽이 더 최신인 경우가 확실할 때에 이 방식으로 충돌을 대처하게 된다.

 

2번이 가장 까다로운데, 모든 파일을 다 유지한 채로 합치는 방법이다.

보통 이럴 일이 있을까 싶은데, 유니티의 경우 meta 파일 등에 컴포넌트 속성 등이 들어가 있는데, 각 개발자마다 다른 컴포넌트를 넣거나, 다른 id 등을 가져야 하는 경우에 이런 방식이 필요해진다. 

meta 파일의 경우 간단하게 gitignore의 내용을 바꿔서 '*. meta'로 meta 파일의 변경사항을 무시해버리면 둘 다 살릴 수 있게 된다.

 

 

TIP) 헷갈리는 Git 용어 정리 

Fork : 레포지토리를 복사해서 내 다른 레포지토리로 복사된 레포지토리의 내용을 가져오는 것이다.
Clone : 레포지토리를 내 로컬 파일로 가져오는 것이다.

Revert : 커밋을 되돌리는 것으로 커밋 행동을 돌리고, 돌렸다는 히스토리가 추가된다.
Amend : 커밋의 내용을 수정하는 것으로 메시지, 로그 등을 수정하는 것이다.

 

Git 명령어

github을 우리는 이전까지 github desktop으로 사용해왔지만, 문제가 생기거나 다른 환경에서는 커맨드 명령어로 쓸 수 있기 때문에 이번 기회에 최대한 많이 배우고, 정리하고자 했다.

 

git branch : head 위치에 브런치 새로 만들기
git checkout <브런치 이름> : 해당 위치로 이동
git merge <id 혹은 브런치 이름> : 현재 브런치에 원하는 브런치를 병합시키기
git checkout <id> : 해당 아이디 위치에 HEAD 위치를 이동
git checkout HEAD : HEAD 위치로 이동

git checkout HEAD^ : HEAD 위치 기준으로 이전으로 한 칸 이동
git checkout HEAD~<숫자> : HEAD 위치 기준으로 이전으로 숫자만큼 이동

git -f branch <브런치 이름> : 해당 HEAD 위치에 브런치를 생성 혹은 브런치를 해당 위치의 파일로 적용

git rebase <브런치 이름> : 해당 브런치를 다른 브런치의 하위로 이동(순서대로 commit 한 것처럼)
git cherry-pick <id> <id> … : 각 해당 영역의 커밋의 복제품을 현재 브런치의 하위로 넣기 (복사된 브런치는 그 전 그 상태로 살아있음)

git reset <id> : 브런치 자체를 이전으로 이동
git revert <id> : 해당 위치의 커밋을 취소하는 새로운 커밋

git rebase -i : 커밋을 무시, 순서 변경
git commit —amend : 커밋 내용 정정


# 원격 브런치와의 상호작용-----------------------------

git clone : 깃허브의 브런치를 로컬로 완전히 가져옴

git fetch : origin/브런치 이름
git pull —rebase : pull 받으면서 현재 위치한 커밋을 o/main으로 rebase

git checkout -b <로컬 새 브런치 이름> <따라갈 원격 저장소 이름> : 새로운 로컬 브런치를 만들고, 그 브런치를 checkout 한 뒤에 원격 저장소를 따라가게끔 함
git branch -u <따라갈 원격 저장소> <보유한 로컬 저장소> : 로컬 저장소가 해당 원격 저장소를 따라가게끔 함

git push origin <place : 원하는 로컬 브런치 이름> : 해당 로컬 브런치가 위치한 곳까지의 커밋들을 원격으로 push → HEAD가 어디에 있든 가능
git push origin <로컬 저장소 위치>:<원격 저장소 브런치 이름> : 해당 위치까지의 커밋을 원격 저장소의 브런치로 push (해당 이름의 저장소가 없으면 새롭게 그 이름으로 만들어서 push)

 

다른 명령어들도 많은데, 수업 시간에도 그렇게 아래의 사이트가 배우기에 너무 잘 되어 있어서 이 사이트에서 더 많은 명령어를 체험해보는 것을 추천한다.

 

 

Learn Git Branching

An interactive Git visualization tool to educate and challenge!

learngitbranching.js.org

 

 

'Long-Vinter' 팀 프로젝트 주간 스크럼

1주일 간에 일단 내가 개발한 스크럼과 각 스프린트들을 정리하였다.

크게 새로운 기능을 넣은 것은 많이 없지만, 기존에 만들었던 기능들을 합치고, 필요한 모델링과 프리팹들을 많이 가져오고 만들었다.

 

11. 28. 방 리스트, 씬 변경 버그, 총알 레이어 수정

- 방 비밀번호를 재설정해도 로비 방 리스트에 제대로 업데이트되지 않던 버그를 수정
- 방 인원을 2명으로 하고 방을 개설하면, 방 설정을 제대로 가져오지 못하던 버그를 수정
- 로비에서 타이틀로 나가면, 네트워크 매니저가 중복으로 생성되던 버그 수정
- 총알 레이어를 변경해서 총알끼리 서로 충돌이 되지 않도록 함

 

Title.cs 타이틀 화면에 생성

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

public class Title : MonoBehaviourPun
{
    // 네트워크 매니저를 처음 생성할 때에 네트워크 매니저에 전달할 변수들 저장
    public string playerPrefabName = "";
    [SerializeField] GameObject roomPrefab;
    [SerializeField] GameObject networkManagerObject;

    void Awake()
    {
        // 처음 게임을 접속하는 경우에만 네트워크 매니저 오브젝트를 활성화하고, 네트워크 매니저 스크립트를 생성
        if (!PhotonNetwork.InLobby && !PhotonNetwork.InRoom)
            networkManagerObject.SetActive(true);
        NetworkManager.Instance.Init(playerPrefabName, roomPrefab);
    }
}

 

NetworkManager.cs 수정

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Photon.Realtime;
using Photon.Pun;
using TMPro;
using Hashtable = ExitGames.Client.Photon.Hashtable;
using System.Linq;

public class NetworkManager : MonoBehaviourPunCallbacks
{
    // 싱글톤으로 생성
    private static NetworkManager instance;
    public static NetworkManager Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject obj;
                // 네트워크 매니저가 중복으로 생성되는 것을 방지
                obj = GameObject.Find("NetworkManager");
                if (obj == null)
                {
                    // 게임 시작 시에 네트워크 매니저 컴포넌트 생성 후 instance로 정의
                    obj = new GameObject("NetworkManager");
                    obj.AddComponent<PhotonView>();
                    instance = obj.AddComponent<NetworkManager>();
                }
                else
                {
                    obj.AddComponent<NetworkManager>();
                    instance = obj.GetComponent<NetworkManager>();
                }
            }
            return instance;
        }
    }
    public List<RoomInfo> rooms = new List<RoomInfo>();
    public string nickName = "";
    public bool isLobby = false;
    bool returnLobby = false;
    public string playerPrefabName;

    Vector3 respawnPos = new Vector3(0, 0, 0);
    GameObject roomPrefab;

    void Awake()
    {
        currentVersion = Application.version;
        Screen.SetResolution(1920, 1080, FullScreenMode.Windowed);
        if (NetworkManager.instance != null) Destroy(this.gameObject);
    }

    public void Init(string _playerName, GameObject _roomPrefab)
    {
        // isLobby 변수로 처음 접속하는지 판단 후, 변수를 전달 받고, 서버에 접속
        if (!isLobby)
        {
            playerPrefabName = _playerName;
            roomPrefab = _roomPrefab;
            DontDestroyOnLoad(this.gameObject);
            PhotonNetwork.ConnectUsingSettings();
        }
        isLobby = false;
    }

 

Room.cs 수정

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

public class Room : MonoBehaviourPunCallbacks
{
    [SerializeField] TextMeshProUGUI roomName;
    [SerializeField] Button roomJoinBtn;
    [SerializeField] TextMeshProUGUI countPlayer;

    public string password = "";
    public int maxPlayers = 1;
    private string roomN = "";

    ...

    // 방 리스트 클릭 시에 네트워크 매니저에서 비교할 정보들을 전달 -> 맞으면 방 접속
    public void ClickEnterRoom()
    {
        // Init으로 받은 정보와 실제 플레이어가 입력한 정보를 비교하고, 맞으면 방 입장을 시키는 함수
        // 접속 성공 시에 return으로 bool값을 전달한다.
        bool isConnect = NetworkManager.Instance.OnClickJoinRoom(roomN, GameObject.Find("PasswordInput").GetComponent<TMP_InputField>().text, maxPlayers, password);

        // 방 접속이 실패 시에 애니메이션 효과 넣을 예정
        if (!isConnect)
        {
            // 창이 흔들리는 효과
        }
    }

    // 방 생성, 갱신 시에 방 정보를 변수로도 저장 -> 이 변수로 클릭, 입력한 정보와 비교할 예정
    public void RoomInit(string name, int curPlayers, int _maxPlayers, string _password)
    {
        // updateRoomList로 가져온 방 정보들을 변수로 저장
        roomName.text = name;
        maxPlayers = _maxPlayers;
        password = _password;
        countPlayer.text = $"{curPlayers}/{maxPlayers}";
    }
}

 

 

11. 29. 집 오브젝트 생성, 방 정보 출력 오류 수정

- 터렛을 설치할 수 있는 집 오브젝트를 생성할 수 있도록 BuildManager 스크립트 수정
- 방의 maxPlayer를 바꿀 때에 슬라이더 값이 표기되지 않던 문제 해결

 

BuildManager.cs 수정

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

public enum BuildType
{
    none = 0,
    turret,
    house,
    other,
    countIndex
}
public class BuildManager : MonoBehaviourPun
{
    ...
    // 집을 여러 개 지을 수 있도록 리스트로 선언
    List<GameObject> myHomeAreas = new List<GameObject>();
    // 마우스 위치에 가장 가까운 집 영역
    GameObject myHomeArea;
    ...
    // 모든 플레이어의 집 영역 리스트
    List<GameObject> homeAreas = new List<GameObject>();
    // 포톤으로 생성하기 위해 프리팹의 이름을 선언
    [SerializeField] string homeAreaPrefabName;

    public void SetBuildType(int buildtypeNumber)
    {
        // 플레이어 자신의 입력에만 반응하도록 함
        if (PhotonNetwork.LocalPlayer.ActorNumber != PlayerStat.LocalPlayer.ownerPlayerActorNumber)
            return;

        buildType = BuildType.none;
        Destroy(buildObject);
        buildObject = null;

        buildType = (BuildType)buildtypeNumber;
        buildPrefabName = buildPrefabNameList[(int)buildType];
        buildObject = Instantiate(buildObjectPrefab[(buildtypeNumber)], Vector3.zero, Quaternion.identity);

        PlayerStat.LocalPlayer.gameObject.GetComponent<PlayerController>().isBuilding = true;
    }
    
    // 해당 위치에 설치물을 설치할 수 있는지 확인하는 함수
    bool CheckBuildPosition(Vector3 _mousePosition)
    {
        if (myHomeArea == null)
            foreach (GameObject area in buildArea)
            {
                // 여러 집을 생성하고, 받아올 수 있게끔 homeArea를 리스트로 선언하고, add로 받아옴
                if (area.name == PhotonNetwork.LocalPlayer.NickName + "HomeArea" && !myHomeAreas.Contains(area)) myHomeAreas.Add(area);
            }
        if (buildType == BuildType.turret)
        {
            // GameObject myHomeArea == null -> 내 집의 개수 <= 0으로 조건 변경
            if (myHomeAreas.Count <= 0 || Vector3.Distance(PlayerStat.LocalPlayer.gameObject.transform.position, _mousePosition) > 4f)
            {
                buildObject.transform.GetChild(0).GetComponent<MeshRenderer>().material.color = buildObjectColors[1];
                return false;
            }
            
            // 가장 가까운 집 영역이 어디인지 확인하는 코드
            float _d = 1000f;
            foreach (GameObject _homeArea in myHomeAreas)
            {
                float _thisHomeDistance = Vector3.Distance(_mousePosition, _homeArea.transform.position);
                if (_d >= _thisHomeDistance)
                {
                    _d = _thisHomeDistance;
                    myHomeArea = _homeArea;
                }
            }

            // 해당 영역의 중심 + 반 지름 안에 마우스가 있는 것이 아니면, false를 리턴
            if (_d > (myHomeArea.transform.lossyScale.x * 0.5f) - buildObject.transform.localScale.x * 0.5f)
            {
                buildObject.transform.GetChild(0).GetComponent<MeshRenderer>().material.color = buildObjectColors[1];
                return false;
            }
        }
        // 집을 지을 경우 
        else if (buildType == BuildType.house)
        {
            // 플레이어와 마우스 위치가 많이 떨어지지 않았는지 확인
            if (Vector3.Distance(PlayerStat.LocalPlayer.gameObject.transform.position, _mousePosition) > 4f)
            {
                buildObject.transform.GetChild(0).GetComponent<MeshRenderer>().material.color = buildObjectColors[1];
                return false;
            }
            float _distance = 1000f;
            float homeAreaRadius = -1f;
            // 모든 영역 중에 집에 해당하는 영역, 그 중에서도 가장 가까운 영역을 가져오기
            foreach (GameObject home in buildArea)
            {
                if (home.TryGetComponent(out GroundTrigger groundTrigger) == false)
                    continue;
                if (homeAreaRadius == -1f) homeAreaRadius = home.transform.lossyScale.x;
                float _d = Vector3.Distance(home.transform.position, _mousePosition);
                if (_distance > _d)
                    _distance = _d;
            }

            // 만약 가장 가까운 집 영역과 마우스의 간격이 너무 가까우면, false 리턴.
            // 집 영역이 겹치는 것을 방지하기 위함
            if (_distance <= homeAreaRadius)
            {
                buildObject.transform.GetChild(0).GetComponent<MeshRenderer>().material.color = buildObjectColors[1];
                return false;
            }
        }
        buildObject.transform.GetChild(0).GetComponent<MeshRenderer>().material.color = buildObjectColors[0];
        return true;
    }
    
    ...
}

 

 

11. 30. 총, 총알 등 동기화

- 총을 바꿀 때마다 해당하는 총을 액티브 true 하고, 무기 정보를 주는 것이 다른 플레이어 컴퓨터에서 동기화가 안 되던 것을 수정
- 총알이 동기화가 되지 않던 것을 수정

 

ItemDrag.cs, PlayerController.cs 수정

...
public class ItemDrag : MonoBehaviour, IPointerClickHandler
{
...
    private void InputEquipItem(GameObject item)
    {
        ...
        int _index = gameObject.GetComponent<Item>().equipment.emIndex;
        player.GetComponent<PhotonView>().RPC("ActiveOffEquipment", RpcTarget.All, _index);
        player.GetComponent<PhotonView>().RPC("SetWeaponData", RpcTarget.All, false, _index);
        ...
    }
    
    private void InputBag(GameObject go)
    {
        ...
        player.GetComponent<PhotonView>().RPC("ActiveOffEquipment", RpcTarget.All, _index);
        player.GetComponent<PhotonView>().RPC("SetWeaponData", RpcTarget.All, true, 0);
        ...
    }
}

...
public class PlayerController : MonoBehaviourPunCallbacks, IPunObservable
{
    ...
    [PunRPC]
    private void SetWeaponData(bool isNull = true, int _index = 0)
    {
        Debug.Log("Set " + _index);
        if (isNull)
            weaponData = null;
        else
        {
            weaponData = weaponDatas[_index];
        }
    }

    [PunRPC]
    private void ActiveOffEquipment(int _index)
    {
        bagEquipPoint.transform.GetChild(_index).gameObject.SetActive(false);
    }
    ...
}

 

Weapon.cs 수정

...
var bulletInstance = PhotonNetwork.Instantiate(bulletPrefab.name, firePoint.position, firePoint.rotation);
...
bulletInstance.GetComponent<PhotonView>().RPC("Shoot", RpcTarget.All, firePoint.forward.x, firePoint.forward.y, firePoint.forward.z, this.damage, bulletDirectionOffset.x, bulletDirectionOffset.y, bulletDirectionOffset.z);

 

 

12. 01, 12. 02 플레이어 체력 UI, 자동적으로 감소하는 체력, 체력 동기화

- 플레이어 체력 UI 생성
- 자동적으로 체력이 감소하도록 함
- 체력 동기화

 

PlayerStat.cs 수정

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

public class PlayerStat : MonoBehaviourPunCallbacks, IPunObservable
{
    private static PlayerStat localPlayer;
    public static PlayerStat LocalPlayer
    {
        get { return localPlayer; }
    }

    [Header("OwnerPlayerInfo")]
    public int ownerPlayerActorNumber;

    public enum Status
    {
        idle = 1,
        walk,
        attack,
        damaged,
        die
    }
    
    public Status status;

    public float hp;
    public float maxHp = 100f;
    public int money;

    public bool isFight = false;
    public bool isCold = false;
    public bool inWater = false;
    public float autoDamageValue = 0.15f;
    [SerializeField] float damagedTime = 1f;
    [SerializeField] float damagePercentInSnowField = 3.3f;
    [SerializeField] float damagePercentWhileWalk = 5.5f;
    [SerializeField] float damagePercentInWater = 13.3f;
    float startTime = 0f;
    [SerializeField]
    private Color normalColor;
    [SerializeField]
    private Color warningColor;
    private Image currentHPImage;
    public Animator currentHPAnimator;

    void Awake()
    {
        currentHPImage = GameObject.Find("HPvalue").GetComponent<Image>();
        currentHPAnimator = GameObject.Find("MaskImage").GetComponent<Animator>();
        hp = maxHp;
        if (photonView.IsMine)
        {
            localPlayer = this;
            photonView.RPC("AddPlayerStatAndCharacter", RpcTarget.AllBuffered);
        }
    }

    public void ChangeStatus(int _index)
    {
        status = (Status)_index;
    }

    [PunRPC]
    void AddPlayerStatAndCharacter()
    {
        PlayerList.Instance.playerStats.Add(this);
        PlayerList.Instance.playerCharacters.Add(this.gameObject);
        PlayerList.Instance.playersWithActorNumber.Add(photonView.Owner.ActorNumber, this.gameObject);
        status = Status.idle;

        foreach (GameObject player in PlayerList.Instance.playersWithActorNumber.Values)
        {
            Debug.Log(player.GetPhotonView().Owner.NickName);
        }
    }

    void LoseStamina()
    {
        if (!photonView.IsMine) return;

        startTime += Time.deltaTime;

        float _targetDamage = autoDamageValue;
        if (isCold)
        {
            if (status == Status.walk) _targetDamage = autoDamageValue * damagePercentWhileWalk;
            else _targetDamage = autoDamageValue * damagePercentInSnowField;
        }

        if (inWater) _targetDamage = autoDamageValue * damagePercentInWater;

        if (startTime >= damagedTime)
        {
            if (hp <= (maxHp / 9f) && !isCold && !inWater)
            {
                startTime = 0f;
            }
            else
            {
                Debug.Log((_targetDamage));
                photonView.RPC("ChangeHp", RpcTarget.AllViaServer, -1f * _targetDamage);
                startTime = 0f;
            }
        }

        if (hp <= (maxHp / 9f) * 3f)
        {
            currentHPImage.color = warningColor;
        }
        else
            currentHPImage.color = normalColor;
    }

    void Update()
    {
        LoseStamina();

        float _hpValue = hp / maxHp;
        currentHPImage.fillAmount = _hpValue;
    }

    [PunRPC]
    public void ChangeHp(float _hp)
    {
        hp += _hp;
        if (hp < 0)
        {
            hp = 0;
            ChangeStatus((int)Status.die);
        }
    }

    public void ChangeMoney(int _money)
    {
        money += _money;
    }

    // 플레이어 자원 동기화
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            stream.SendNext(hp);
            stream.SendNext(maxHp);
            stream.SendNext(money);
            stream.SendNext((int)status);
            stream.SendNext((bool)isFight);
            stream.SendNext((bool)isCold);
            stream.SendNext((bool)inWater);
        }
        else
        {
            hp = (float)stream.ReceiveNext();
            maxHp = (float)stream.ReceiveNext();
            money = (int)stream.ReceiveNext();
            status = (Status)(int)stream.ReceiveNext();
            isFight = (bool)stream.ReceiveNext();
            isCold = (bool)stream.ReceiveNext();
            inWater = (bool)stream.ReceiveNext();
        }
    }
}

 

 

 

유데미 코리아 바로가기 : 

 

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

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

www.udemykorea.com

 

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