저번 주에 이어서 윙또 프로젝트를 마무리하고, 새로운 프로젝트에 돌입했다.
오늘 블로그를 쓰기 전에 닷지, 리듬게임 그리고 윙또 총 3개의 개인 프로젝트가 깃허브에 채워진 모습을 보고 새삼 놀랍고, 만족스러웠다.
혼자 집에서 독학을 할 적에는 하나 프로젝트도 마무리하기 어려웠는데, 지금은 약 한 달만에 이 많은 프로젝트 기능을 어느 정도 완성할 수 있을 정도로 내가 성장했나 보다.
이번 주는 수업으로 '시네 머신 카메라' 사용법을 배웠다.
게임의 연출을 확 올려줄 만한 좋은 패키지로 꼭 배우고 싶었는데, 이번 기회에 배우고 경험할 수 있어 좋았다.
알고리즘 스터디 역시 꾸준히 하고 있다.
이번 주는 DP 유형의 문제를 건드리기 시작했는데, 사실 아직 완전히 그 개념을 이해하지 못한지라 개념을 완전히 이해하고 나서 게시글로 한 번 정리해보도록 하겠다.
그리고, 이번 주부터는 개인 프로젝트이지만, 다른 두 분과 같이 진행하기로 했다.
RPG라는 장르에 맞춰서 일주일 정도 시간을 가지고 계속 일정 기능을 구현해 오기로 했다.
이번주부터는 주마다 어떤 목표를 가지고 어떻게 만들고 있는지 적어볼 계획이다.
Starters 15주차 - '시네 머신 카메라'
시네 머신은 카메라의 위치와 자연스러운 이동 등의 카메라 효과를 극대화시켜주는 좋은 패키지이다.
실제로 게임에서 시네마틱 영상이나 중간 영상 씬들을 제작할 때에도 이를 이용하면, 자연스러운 화면 이동으로 영상을 만들 수 있다.
기본적으로 메인 카메라 외에 가상 카메라를 몇 개 두고 사용하게 되는데, 각 상황마다 알맞은 가상 카메라의 위치, 효과 등을 메인 카메라에서 받아와서 화면을 전환, 이동시키는 것 같다.
정말 많은 기능들이 시네 머신 카메라에 있지만, 일단 가장 신기하면서도 내가 자주 쓸만한 두 가지의 기능만 아래 소개하려고 한다.
TargetGroup
기본적으로 시네 머신은 타깃을 정해서 카메라가 해당 타겟을 따라 이동하고, 각도를 조절해서 화면에 타겟의 모습을 비치도록 한다.
Target Group은 그 타겟을 여러 개 설정해서 같이 잡아주는 카메라 기능이라고 보면 된다.
TargetGroup 카메라를 생성하면, 가상 카메라 외에 TargetGroup이라는 오브젝트가 더 생성된다.
이 오브젝트에서는 여러 타겟을 선택할 수가 있는데, 여기에 포커스 되길 원하는 타깃들을 모두 집어넣는다.
그렇게 되면, 이 오브젝트들이 화면에 들어오면 자연스럽게 카메라가 타깃 그룹 카메라로 전환되면서 타깃들의 중앙 위치로 화면이 이동되게 된다.
Dolly Camera, Dolly Cart
혹시 영화 촬영 비하인드 영상 등에서 dolly camera를 본 적이 있을까?
나는 명칭은 몰랐지만, 본 적이 있다. 분명 아래 사진을 보면 기억나는 사람들이 있을 것이다.
이런 식의 카메라를 놀랍게도 유니티에서도 구현할 수 있다.
거기에 시네 머신으로 간편함을 더한..!
돌리 카메라 역시 하이어라키에서 우클릭 후에 Cinemachine - Dolly Camera with track 혹은 Dolly track with cart로 생성할 수 있다.
생성하고 나면, 돌리 트랙이 생성되는데 트랙을 추가하고, 위치를 지정하면 된다.
이후에 생성한 돌리 카메라의 Body를 아래와 같이 설정하고, 돌리 트랙을 넣으면 된다.
돌리 카트의 경우는 위와 같이 설정할 필요도 없이 같이 생성된 카트의 하위 오브젝트로 카메라를 넣으면 된다.
아래는 돌리 카트를 이용해서 찍은 영상이다.
나만의 게임 만들기 - 'RPG 장르 게임 만들기'
이번에 만들 게임의 큰 장르는 RPG이다.
이번 주부터는 위에 서술한 대로 셋이서 장르를 정해서 일주일 정도의 시간을 가지고, 기능 하나씩 구현하기로 했다.
실제로 깃허브도 하나 새로 레포지토리를 만들어서 프로젝트 파일을 올리기로 했다.
이번 주의 목표는 탑 뷰로 캐릭터의 이동, 카메라 이동, 몬스터의 이동까지 구현하기로 하였고, 현재는 캐릭터의 이동까지 구현이 되어가는 상태이다.
그리고, 여기에 더해서 개인적인 목표로 포톤을 이용해 로비, 방, 게임 접속까지 만들어 보기로 했다.
2.5D 캐릭터 이동 구현하기
캐릭터를 이동하는 것, 추후에 몬스터의 이동 등은 기본적으로 '골드 메탈'님의 '3D 탑뷰 액션 게임' 강의를 참고하기로 했다.
이동은 일단은 강의에서 하는 대로 position값을 더해서 구현했다.
void GetInput()
{
// 플레이어의 rigidbody - collision detection을 continuous로 하면,
// cpu를 조금 더 잡아먹는 대신에 계속 충돌을 잡음
// transform으로 이동해서 충돌을 무시하는 경우를 방지하지만,
// 여전히 가까이에서 이동하면 뚫리는 편..
// 입력을 받기 - 좌우키, 상하키, shift키, space키
hAxis = Input.GetAxisRaw("Horizontal");
vAxis = Input.GetAxisRaw("Vertical");
wDown = Input.GetButton("Walk");
jDown = Input.GetButtonDown("Jump");
}
void Move()
{
// 방향이나 보정된 거리를 이동할 때에 normalized 사용
moveVec = new Vector3(hAxis, 0, vAxis).normalized;
// 대쉬 중에서는 이동 방향을 중간에 바꿀 수 없게 고정
if (isDodge)
moveVec = dodgeVec;
// position을 조절해서 이동
// 이렇게 하니 짧은 거리를 순간이동 하는 식이라 그런지 벽을 뚫는 버그 발생
// velocity를 조절해서 이동하도록 바꿀 예정
transform.position += moveVec * moveSpeed * (wDown ? 0.3f : 1f) * Time.deltaTime;
// 애니메이션 bool값 변경
anim.SetBool("isRun", moveVec != Vector3.zero);
anim.SetBool("isWalk", wDown);
}
하지만, 벽에 닿은 상태로 캐릭터가 이동하면 비비면서 뚫고 지나가는 버그가 있는 이유가 이 때문인 것 같아서 rigidbody의 addforce를 이용하던가 velocity 자체를 조절해서 이동시키기로 할 예정이다.
2D 애니메이션
개인적으로 이전부터 계속 '돈 스타브'같이 2.5D 게임을 만들고 싶었기 때문에 캐릭터만 2D 스프라이트로 만들기로 했다.
더불어 3D 에셋을 만드는 것보다 2D 그림을 그리는 것이 훨씬 나에게 쉬워서 그런 것도 있었다.
하지만, 그럼에도 RPG라서 애초에 애니메이션이 많은데 2D는 애니메이션 하나하나 마다 새로 그려야 해서 너무 귀찮고, 힘든 면이 있었다.
그래서 혹시나 예전에 맥시모에서 뼈대를 잡고 애니메이션을 만드는 것이 2D에도 있나 해서 검색해보니... 2D animation이라는 패키지로 가능하다는 것을 알게 되었다! (심지어 공식 패키지인 것 같다.)
이를 사용하기 위해서는 원래 psd importer 패키지를 이용해 psb 파일(포토샵)을 가져와 쓰면 좋다고 한다.
레이어를 구분해서 이미지 파일을 가져오기 때문에 팔, 다리, 몸 등을 구분해서 가져와야 하는 것 때문에 상당히 유용하다고 한다.
다만, 포토샵을 사용하지 않기 때문에 나는 일부로 캐릭터의 이미지를 팔, 다리, 몸, 머리 등을 나눠서 그려서 png 파일로 가져왔다.
그리고, 스프라이트 에디터에서 Skinning Editor 탭에서 캐릭터를 더블 클릭 후에 뼈대를 심어준다.
이전에 만든 뼈를 누르고 새롭게 뼈를 만들면, 관절로 뼈가 연결되는 것처럼 각 뼈대가 이동시에 서로 영향을 주고받게 된다.
실제로 오른편에 Visibility를 누르면, 각 뼈가 연결된 관계를 보고 수정할 수 있다.
이후에 Auto Geometry를 하고, Edit Geometry로 뼈대에 붙는 몸체, 질량 등을 지정해주면, 뼈대를 심는 것은 끝이다.
처음에는 이러고, 캐릭터 스프라이트 파일을 하이어라키에 넣었을 때에 psb 파일과 다르게 뼈대가 같이 생성이 안 되어서 당황을 했었다.
문제 해결은 생각보다 간단했는데, 해당 스프라이트 오브젝트에 Sprite Skin 컴포넌트를 추가하고, Create Bone을 누르면, 아까 뼈대를 생성한 대로 뼈 오브젝트가 하위 오브젝트로 생성된다.
이후에는 애니메이션 창을 열어서 뼈대를 움직이면서 녹화를 하면, 쉽게 애니메이션을 만들 수 있게 된다.
덕분에 몇 가지 애니메이션을 만들었고, 하나의 스프라이트로 나름 자연스러운 움직임을 만들 수 있었다.
포톤으로 로비와 방 만들기
이번 주의 메인은 사실 포톤이었다.
개인적으로 저번 게임을 마무리하면서 멀티까지 하고 싶었기 때문에 더 열심히 배우고 적용하려 했다.
다만, 이전 게임 '윙또'에서는 비주얼 스크립팅으로 움직임의 대부분을 만들어서 포톤하고 연결하기 쉽지 않을 것 같아 일단은 싱글 플레이로 마무리하기로 했다.
포톤은 pun2를 사용했고, 이전에 배우고 썼던 플레이 팹을 같이 사용했다.
이는 이전부터 참고했던 '고라니 TV - 게임 개발 채널'님의 영상을 참고했다.
플레이 팹과 포톤을 같이 쓰기 위해서는 둘의 역할을 나눌 필요가 있었다.
나는 플레이 팹에서 회원 가입, 로그인과 동시에 닉네임 리스트를 리더보드에도 따로 저장하여 관리를 하였다.
그리고, 이를 나중에 방을 생성하거나 들어갈 때에 해당 방에 연결된 RoomInfo 컴포넌트의 호스트 플레이어 닉네임과 일치하는 리더보드의 유저 이름을 가져오고, 다시 해당 플레이어가 설정한 방 이름을 플레이어 타이틀 데이터에서 가져와 비교해 방을 입장시켰다.
// 플레이어 스탯(id, 닉네임 등등)을 리더보드에 저장
void SetStat()
{
// 플레이어 스탯을 설정
// IDInfo(string) = 0(int)
var request = new UpdatePlayerStatisticsRequest { Statistics = new List<StatisticUpdate> { new StatisticUpdate { StatisticName = "IDInfo", Value = 0 } } };
PlayFabClientAPI.UpdatePlayerStatistics(request, (result) => { }, (error) => print("값 저장실패"));
}
// 플레이어 목록들을 가져와 방 입장 시, 로비 접속 시 비교해서 인원 수 등을 표시
void GetLeaderboard(string myID)
{
// 리더보드에 플레이어 정보를 저장했다가 가져옴
// 재로그인일 수 있기 때문에 초기화
playFabUserList.Clear();
// 100(리더보드 최대) x (0인덱스 ~ 10인덱스의 플레이어) 총 1000명의
for (int i = 0; i < 10; i++)
{
var request = new GetLeaderboardRequest
{
StartPosition = i * 100,
StatisticName = "IDInfo",
MaxResultsCount = 100,
ProfileConstraints = new PlayerProfileViewConstraints() { ShowDisplayName = true }
};
PlayFabClientAPI.GetLeaderboard(request, (result) =>
{
// 0, 100, 200... 인덱스부터 100개씩 플레이어 정보를 가져오는데
// 만약 해당하는 100명의 정보가 0개인 경우 정보 가져오기를 끝냄
// (이전 100명까지 해서 모든 플레이어 정보를 가져온 경우)
if (result.Leaderboard.Count == 0) return;
for (int j = 0; j < result.Leaderboard.Count; j++)
{
playFabUserList.Add(result.Leaderboard[j]);
// 로그인 시에 획득한 id와 대조해서 내 playfabinfo를 가져옴
if (result.Leaderboard[j].PlayFabId == myID) myPlayFabInfo = result.Leaderboard[j];
}
},
(error) => { });
}
}
// 해당 플레이어 방의 이름 데이터를 저장
void SetData(string curData)
{
var request = new UpdateUserDataRequest()
{
// Home에 현재 입력한 값이 덮어씌워짐
Data = new Dictionary<string, string>() { { "Home", curData } },
// 정보를 가져올 수 있도록 공개를 함
Permission = UserDataPermission.Public
};
PlayFabClientAPI.UpdateUserData(request, (result) => { }, (error) => print("데이터 저장 실패"));
}
// 해당 유저 방의 이름 데이터를 가져옴
void GetData(string curID)
{
// 플레이팹 아이디로 어떤 값이든 유저의 데이터를 가져올 수 있음
PlayFabClientAPI.GetUserData(new GetUserDataRequest() { PlayFabId = curID }, (result) =>
// 유저 방 정보창에 미리 아이디와 유저 테이블의 Home의 값을 넣음
userHouseDataText.text = curID + "\n" + result.Data["Home"].Value,
(error) => print("데이터 불러오기 실패"));
}
비밀번호 역시 여기에 저장하려고 했는데, 이후에 포톤을 공부하던 중에 방을 생성할 때에 hashtable을 달 수 있다는 사실을 알게 되었다.
해당 테이블에 호스트 아이디, 닉네임 그리고, 방 비밀번호를 테이블로 달았고, 해당 방이 잠금 방일 때에는 이 테이블과 inputfield의 값을 비교해서 입장시켰다.
public void JoinOrCreateUserRoom(string roomName, string hostName, int max, string password = "")
{
// 유저 방 참여 호출
//PlayFabUserList의 표시이름과 입력받은 닉네임이 같다면 PlayFabID를 커스텀 프로퍼티로 넣고 방을 만든다
for (int i = 0; i < playFabUserList.Count; i++)
{
// 가져온 모든 플레이어와 해당 방 이름을 대조해서 일치하면, 해당 방으로 입장
if (playFabUserList[i].DisplayName == hostName)
{
RoomOptions roomOptions = new RoomOptions();
// 최대 인원 수 조정
roomOptions.MaxPlayers = (byte)max;
// 방에 해시태그를 담
// 기존 유니티의 hashtable과 겹치므로 위에 using hashtable을 써줌
// 현재 방의 커스텀 테이블을 작성 - "PlayFabID"를 key로, 실제 playFabId를 value로 넣음 (+ 닉네임, 방 비밀번호)
roomOptions.CustomRoomProperties = new Hashtable() { { "PlayFabID", playFabUserList[i].PlayFabId }, { "Password", password }, { "HostNickName", playFabUserList[i].DisplayName } };
roomOptions.CustomRoomPropertiesForLobby = new string[] { "PlayFabID", "Password", "HostNickName" };
PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, null);
return;
}
}
print("일치하는 방이 없습니다");
}
마지막으로 방 리스트가 변동이 있을 때에 로비에서 방 목록이 바뀌어야 하므로 아래의 코드들을 추가했다.
이 부분에서 골머리를 앓았고, 실제로 일주일 중에서 3 ~ 4일을 여기에 썼다.
코드를 잠깐 설명하자면, 로비에서 생성된 방의 변동이 있을 때에 자동으로 실행되는 OnRoomListUpdate 함수를 override 해서 존재하는 방들만 다시 가져와 리스트로 저장하는 작업을 한다.
그리고, 바로 리스트를 초기화한 뒤에 방금 새로 저장된 리스트에 있는 방들로 방 목록을 다시 생성하게 된다.
// NetworkManager.cs
// 리스트 갱신
public void RenewalRoomList()
{
lobbyUIManager.ClearRoom();
for (int i = 0; i < myList.Count; i++)
{
// 방을 만드는 데에 필요한 정보를 전달하여 새로운 방 리스트를 생성
print(myList[i].CustomProperties["HostNickName"].ToString());
print(myList[i].CustomProperties["Password"].ToString());
lobbyUIManager.CreateRoom(myList[i].Name, myList[i].CustomProperties["HostNickName"].ToString(), myList[i].CustomProperties["Password"].ToString(), myList[i].MaxPlayers);
}
}
// 로비에서 방에 변동사항이 있는 경우 변동사항이 있는 방들만 가져옴
public override void OnRoomListUpdate(List<Photon.Realtime.RoomInfo> roomList)
{
int roomCount = roomList.Count;
for (int i = 0; i < roomCount; i++)
{
// 지워질 방은 myList에서 빼고, 아닌 방만 추가하거나 인덱스를 조정함
if (!roomList[i].RemovedFromList)
{
if (!myList.Contains(roomList[i])) myList.Add(roomList[i]);
else myList[myList.IndexOf(roomList[i])] = roomList[i];
}
else if (myList.IndexOf(roomList[i]) != -1) myList.RemoveAt(myList.IndexOf(roomList[i]));
}
// 방 리스트 변동이 있으므로 리스트 갱신을 실행
RenewalRoomList();
}
// LobbyManager.cs
// 프리팹으로 저장된 방을 리스트에 추가하면서 데이터를 전달
public void CreateRoom(string roomName, string hostName, string password, int max)
{
// 필요한 정보들을 모두 전달하면서 필요한 오브젝트만 활성화
GameObject room = Instantiate(roomPrefab);
room.transform.parent = contentBox.transform;
RoomInfo info = room.GetComponent<RoomInfo>();
info.roomName = roomName;
info.roomNameText.text = roomName;
info.hostName = hostName;
info.password = password;
info.maxCount.value = max;
info.maxCount.gameObject.SetActive(false);
info.roomNameInput.gameObject.SetActive(false);
info.roomNameText.gameObject.SetActive(true);
info.createBtn.gameObject.SetActive(false);
info.joinBtn.gameObject.SetActive(true);
info.lockToggle.gameObject.SetActive(false);
if (password == "")
{
info.isLocked = false;
info.passwordInput.interactable = false;
}
else
{
info.isLocked = true;
info.passwordInput.interactable = true;
}
info.lockToggle.gameObject.SetActive(false);
rooms.Add(info.gameObject);
}
// 방 리스트를 초기화
public void ClearRoom()
{
if (rooms.Count != 0)
{
for (int i = 0; i < rooms.Count; i++)
{
Destroy(rooms[i]);
}
}
}
여하튼, 결과적으로 방에 무사히(?) 두 플레이어가 접속하는 것을 확인했고, 이제는 방에서 게임으로 넘어가고, 채팅을 치는 등의 기능을 더 구현해야 할 것 같다.
마무리하기 전에 저번 주에 마무리하지 못한 윙또 프로젝트를 싱글 플레이까지는 완성했다.
3가지 스킬 모두 구현했고, 모바일 환경 역시 구현했다.
그리고, 치명적인 버그와 아래 신기록까지 남은 거리를 표시하던 창의 배경이 되는 렌더 텍스쳐 역시 잘 나오도록 수정을 하였다.
플레이 영상은 아까 위에서 소개했고, 아래에 완성한 윙또 프로젝트 깃허브 링크를 남겨두었다.
혹여나 자세한 코드가 궁금한 사람은 이 페이지에서 다운로드하여서 봐도 될 것 같다.
윙또 프로젝트 깃허브 페이지 바로가기 :
GitHub - MoHoDu/Make_WingTTo_Project: 게임 '윙또' 유니티 비주얼 스크립팅으로 만들기
게임 '윙또' 유니티 비주얼 스크립팅으로 만들기. Contribute to MoHoDu/Make_WingTTo_Project development by creating an account on GitHub.
github.com
유데미 코리아 바로가기 :
Udemy Korea - 실용적인 온라인 강의, 글로벌 전문가에게 배워보세요. | Udemy Korea
유데미코리아 AI, 파이썬, 리엑트, 자바, 노션, 디자인, UI, UIX, 기획 등 전문가의 온라인 강의를 제공하고 있습니다.
www.udemykorea.com
💡 본 포스팅은 유데미-웅진씽크빅 취업 부트캠프 유니티 1기 과정 후기로 작성되었습니다.
'Starters 부트캠프 > B - log' 카테고리의 다른 글
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 17주차 학습 일지 (0) | 2022.10.16 |
---|---|
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 16주차 학습 일지 (0) | 2022.10.08 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 14주차 학습 일지 (1) | 2022.09.25 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 13주차 학습 일지 (0) | 2022.09.18 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 12주차 학습 일지 (7) | 2022.09.11 |