본 포스트는 베르님의 Make the 어몽어스를 정리한 포스트입니다.
https://www.youtube.com/watch?v=NHUHKP9YXh4&list=PLYQHfkihy4Aw6QjsZqwwbD4ihpwvm7N0U&index=7
이번 시간에는 대기실에서 움직이는 캐릭터를 구현합니다. 먼저 필요한 리소스를 베르님의 영상 하단에서 다운로드 받은 후 작업합니다.
https://drive.google.com/file/d/1eznS5o5O6zL1zCerOjkpWTJU_kzrCLJG/view
2D Object > Sprites > Square를 눌러 새로운 오브젝트를 생성합니다. (Lobby Player Character) 그리고 해당 오브젝트의 가시성을 위해 다운받은 리소스 중 idle을 Sprite property로 넣습니다. 또한 크기 조정을 위해 scale을 0.5로 변경합니다. (x,y)
- Network Transform 컴포넌트: 오브젝트의 위치와 회전 크기를 동기화하는 기능을 제공함
자신의 캐릭터가 움직이는 권한을 각 클라이언트에게 주기 위해 Network Transform 컴포넌트의 Client Authority를 체크합니다. 다음으로 동기화되는 반응속도를 높이고자 Sync interval을 0.01로 변경합니다. 이후 해당 오브젝트를 prefab화 한 다음 hierarchy 창에서 제거합니다.
- Sync Interval을 다룰 때 주의해야할 점은 이 값이 작아질수록 초당 동기화 횟수가 많아져 반응 속도가 빨라지지만 그만큼 소모하는 데이터 양이 늘어납니다.
메인 메뉴 scene으로 돌아간 다음 AmongUsRoomManager inspector 창 하단의 Registered Spawnable Prefabs에 방금 만든 prefab을 등록해줍니다. 그러면 게임 내에서 해당 prefab을 spawn할 수 있도록 만들어 줍니다.
다음 AmongUsRoomManager 스크립트를 켜서 다음의 내용을 작성합니다.
public class AmongUsRoomManager : NetworkRoomManager
{
// 서버에서 새로 접속한 클라이언트 감지 시 동작하는 함수
public override void OnRoomServerConnect(NetworkConnection conn)
{
base.OnRoomServerConnect(conn);
// lobby character prefab을 spawn prefabs로 부터 가져와서 인스턴스화 한 다음
var player = Instantiate(spawnPrefabs[0]);
// 해당 함수로 클라이언트들에게 게임 오브젝트가 소환되었음을 알림
// 두 번째 매개변수에 방금 서버에 접속한 플레이어의 정보를 담고있는 네트워크 connection을 전달하여
// 방금 소환된 오브젝트가 새로 접속한 플레이어의 소유임을 알림
NetworkServer.Spawn(player,conn);
}
}
각 캐릭터들을 움직이는 기능을 만들기 위해 CharacterMover 스크립트를 생성합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror; // mirror name space using 선언
public class CharacterMover : NetworkBehaviour
{
public bool isMoveable; // 캐릭터 움직일 수 있는 상태인지 확인
[SyncVar] // 네트워크로 동기화 되도록 함
public float speed = 2f; // 캐릭터의 속도
// Start is called before the first frame update
void Start()
{
if(hasAuthority){
// 대기실 Scene의 기본으로 배치되어있는 카메라를 찾아 클라이언트가 소유한 캐릭터에 붙이도록 함
Camera cam = Camera.main;
cam.transform.SetParent(transform);
cam.transform.localPosition = new Vector3(0f,0f,-10f);
cam.orthographicSize = 2.5f;
}
}
// Update is called once per frame
void Update()
{
}
void FixedUpdate(){
Move();
}
public void Move(){
// 클라이언트가 해당 오브젝트에 대한 권한을 가지고 있다면 입력을 받아 이동 기능 처리
if(hasAuthority && isMoveable){
// 컨트롤 타입 - keyboardMouse일 경우
if(PlayerSettings.controlType == EcontrolType.KeyboardMouse){
// 키보드 입력이 있으면 움직이게 함
Vector3 dir = Vector3.ClampMagnitude(new Vector3(Input.GetAxis("Horizontal"),Input.GetAxis("Vertical"),0f),1f);
// 이동 방향에 따라 캐릭터의 이미지를 뒤집어줌
if(dir.x < 0f) transform.localScale = new Vector3(-0.5f,0.5f,1f);
else if (dir.x > 0f) transform.localScale = new Vector3(0.5f,0.5f,1f);
//
transform.position += dir * speed * Time.deltaTime;
}
// 마우스 입력이 있으면 움직이게 함
else{
if(Input.GetMouseButton(0)){
Vector3 dir = (Input.mousePosition - new Vector3(Screen.width * 0.5f, Screen.height * 0.5f, 0f)).normalized;
// 이동 방향에 따라 캐릭터의 이미지를 뒤집어줌
if(dir.x < 0f) transform.localScale = new Vector3(-0.5f,0.5f,1f);
else if (dir.x > 0f) transform.localScale = new Vector3(0.5f,0.5f,1f);
//
transform.position += dir * speed * Time.deltaTime;
}
}
}
}
}
코드 작성이 끝나면 해당 스크립트를 lobby player character prefab의 컴포넌트로 추가합니다. 게임을 빌드해보면 캐릭터가 잘 움직이는 것을 테스트할 수 있습니다.
Lobby Player Character prefab을 클릭한 후 ctrl+6로 애니메이션을 생성합니다. 먼저 frame rate를 30으로 조정한 후 idle sprite를 드래그하여 애니메이션 창에 넣습니다.
다음으로 Create New Clip을 눌려서 Walk animation clip도 frame rate를 30으로 조정합니다.
그리고 Walk 폴더 안에 있는 스프라이트를 전체 선택하여 애니메이션창에 드래그 하면 다음과 같이 걷는 애니메이션이 완성됩니다.
그 다음 애니메이터 view를 열고 Bool type의 isMove parameter를 만듭니다.
idle과 walk 사이에 transition을 생성한 다음 condition에 isMove를 추가하여 true인 경우 idle -> walk, false인 경우 walk -> idle 애니메이션이 진행되도록 설정합니다. 추가적으로 Transition duration은 영상에서 진행한 값과 동일하게 0.1로 변경하였습니다.
이후 다시 CharacterMover 스크립트를 열어 다음의 내용을 추가합니다.
public class CharacterMover : NetworkBehaviour
{
private Animator animator; // 애니메이터 변수 추가
.
.
void Start()
{
animator = GetComponent<Animator>();
.
.
}
public void Move(){
if(hasAuthority && isMoveable){
bool isMove = false; // 이동 입력에 따라 애니메이터의 isMove parameter값을 변경함
if(PlayerSettings.controlType == EcontrolType.KeyboardMouse){
.
.
isMove = dir.magnitude != 0f;
}
}
else{
if(Input.GetMouseButton(0)){
.
.
isMove = dir.magnitude != 0f;
}
}
animator.SetBool("isMove",isMove);
}
다음으로 Lobby Player Character prefab에 network animator component를 추가한 후 캐릭터에 붙어있는 Animator component를 프로퍼티에 넣어줍니다. 그리고 network animator 또한 클라이언트에서 권한을 가지도록 Client Authority를 체크합니다.
- Network Animator: 네트워크를 통해 애니메이션을 동기화해주는 역할
게임을 실행하면 다음과 같이 애니메이션이 동기화되는 것을 볼 수 있습니다.
추가적으로 플레이어가 방에 입장할 때 의자로 텔레포트 되어 일어서는 모습을 연출하겠습니다. 다시 prefab을 열어 새로운 animation clip을 만듭니다. (Anim_LobbySpawn) 앞과 동일한 방식으로 frame rate 조절 > Spawn에 있는 Sprites 전체 선택 후 드래그 > 움직이는 속도 조절 순서로 완성합니다.
Spawn 애니메이션의 경우 반복 재생을 막기 위해 해당 애니메이션을 클릭한 후 Loop Time을 해제합니다.
다음 animator view를 열어 방금 만든 애니메이션을 default state로 변경한 후 idle로 넘어가도록 수정합니다.
Spawn -> Idle로 넘어가는 transition은 오른쪽의 창과 같이 값들을 설정합니다.
- Has Exit Time: 트랜지션 조건이 언제든지 효력을 발할 수 있게 할 것인지(해제 시), 스테이트의 exit 시간 사이에만 효력을 발할 수 있게 할 것인지(설정 시)를 설정합니다.
https://docs.unity3d.com/kr/530/Manual/class-Transition.html
Spawn하는 동안에는 캐릭터가 움직이지 못하게 해야합니다. 따라서, 캐릭터 Mover class에서 제약을 가해야합니다. 이 CharacterMover 컴포넌트는 인게임에서 재활용할 수 있어야 하기에 로비에서만 사용할 LobbyCharacterMover를 CharacterMover 클래스로 상속받아 다시 만들어줍니다. 따라서 LobbyCharacterMover 스크립트를 다음과 같이 새로 작성합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LobbyCharacterMover : CharacterMover
{
// Spawn 완료 후 움직일 수 있도록 함
public void CompleteSpawn(){
if(hasAuthority){
isMoveable = true;
}
}
}
스크립트 작성을 마친 후 Lobby Player Character prefab을 열어 기존에 붙어있던 Character Mover 컴포넌트를 제거한 후 방금 생성한 스크립트를 컴포넌트로 추가합니다.
그 다음 Spawn 애니메이션의 끝 지점을 선택하여 애니메이션 event를 만들고, CompleteSpawn() 함수를 호출하게 합니다.
지금부터는 캐릭터를 의자 위치에서 소환시키는 작업을 진행하겠습니다. 먼저 GameRoomScene에서 2D object > Sprites를 생성한 다음 Spawn001 이미지를 소스에 넣어줍니다. 그리고 다음과 같이 의자에 각 캐릭터들을 배치시킨 후 Spawn Positions 오브젝트를 생성하고(Create Empty), 만든 스프라이트들을 넣습니다. 이후 Sprite Render 체크를 해제합니다.
그 다음 앉아 있는 위치들을 저장할 SpawnPositions 스크립트를 생성한 후 다음과 같이 작성합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnPositions : MonoBehaviour
{
[SerializeField]
private Transform[] positions; // Spawn 위치들 저장할 배열
private int index; // Spawn 위치 겹치지 않게 하기 위한 인덱스 변수
// index에 따라 위치 반환
public Vector3 GetSpawnPosition(){
Vector3 pos = positions[index++].position;
if(index >= positions.Length){
index = 0;
}
return pos;
}
}
그 다음 GameRoomManager 스크립트를 열어 다음 내용을 추가합니다.
public class AmongUsRoomManager : NetworkRoomManager
{
// 서버에서 새로 접속한 클라이언트 감지 시 동작하는 함수
public override void OnRoomServerConnect(NetworkConnection conn)
{
base.OnRoomServerConnect(conn);
// 1) SpawnPositions 오브젝트를 찾아 함수로 스폰 위치를 가져
Vector3 spawnPos = FindObjectOfType<SpawnPositions>().GetSpawnPosition(); 옴
// 2) 해당 스폰 위치에 프리팹을 생성하도록 함
var player = Instantiate(spawnPrefabs[0], spawnPos, Quaternion.identity);
.
.
}
}
그 다음 SpawnPositions 스크립트를 Spawn Positions 오브젝트의 컴포넌트로 붙이고 Sprite positions 안의 자식들을 해당 컴포넌트로 드래그하여 추가해줍니다.
(+ 추가로 스크립트 생성후 저장을 하고 다른 스크립트에서 가져다 쓰거나, 에디터로 돌아오거나 해야합니다. GameRoomManger에서 계속 오류가 나서 확인해보니 방금 생성한 스크립트를 저장하지 않아 인식을 못하더라구요..!)
게임을 실행해보면 다음과 같이 캐릭터가 정해진 위치에서 생성되는 것을 확인할 수 있습니다.
이제 캐릭터가 대기실 밖으로 나가지 못하게 만드는 작업을 하겠습니다. 먼저 우주선 오브젝트를 선택한 후 자식에 새로운 오브젝트를 추가하여 Ship Collider로 rename합니다. 그 다음 컴포넌트로 Polygon Collider 2D를 추가합니다. 그리고 Edit Collider 버튼을 눌려 우주선 형태에 맞게 콜라이더를 지정합니다.
(플레이어가 해당 부분에 테두리 내에서만 움직일 수 있다고 생각하고 콜라이더를 편집하면 됩니다.)
- Polygon Collider 2D: 2D 콜라이더의 형태를 사용자가 원하는대로 편집할 수 있는 컴포넌트
박스 오브젝트들에도 동일하게 collider 작업을 해줍니다.
그 다음 Lobby Player Character prefab을 열어 box collider 2D 컴포넌트를 추가하여 다리 부분에 맞게 collider 설정을 합니다. 이후 RigidBody 2D 컴포넌트를 추가하고 캐릭터가 중력에 의해 밑으로 떨어지지 않도록 Gravity Scale을 0으로 설정합니다. 또한 Constraints의 Freeze Rotation에서 z를 체크하여 충돌 후 캐릭터가 회전하지 않도록 합니다.
한편 캐릭터끼리는 충돌하지 않도록 해야합니다. 이를 위해 레이어를 이용하여 같은 레이어를 가진 플레이어끼리의 충돌은 무시하도록 만들겠습니다. 먼저 다음과 같이 Project Settings를 변경합니다.
Edit > Project Settings > Tags and Layer & Physics 2D
그 다음 Lobby Player Character prefab의 레이어를 player로 설정합니다.
게임을 실행해보면 캐릭터가 상자 뒤에 있어도 앞에 그려지는 모습이 보입니다. 따라서, 지금부터는 스프라이트 오브젝트들을 위치에 따라 정렬하는 기능을 만들겠습니다. 새로운 오브젝트를 생성하여(Sprite Sorter) 자식으로 두 오브젝트를 추가한 각각 우주선의 앞과 뒤에 배치합니다. 이 두 오브젝트의 거리를 이용해 스프라이트들을 정렬할 것입니다.
다음으로 SpriteSorter 스크립트를 생성한 다음 다음과 같이 작성합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpriteSorter : MonoBehaviour
{
[SerializeField]
private Transform Back;
[SerializeField]
private Transform Front;
// 매개변수로 받은 오브젝트와 Back의 거리, Back과 Front의 거리를 이용해 SortingOrder를 구해서 반환함
public int GetSortingOrder(GameObject obj){
float objDist = Mathf.Abs(Back.position.y - obj.transform.position.y);
float totalDist = Mathf.Abs(Back.position.y - Front.position.y);
return (int) (Mathf.Lerp(System.Int16.MinValue, System.Int16.MaxValue, objDist/totalDist));
}
}
그 다음 정렬 대상이 될 SortingSprite 스크립트를 생성하고 다음과 같이 작성합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SortingSprite : MonoBehaviour
{
public enum ESortingType{ // EsortingType 열거형 (종류: Static, Update)
Static, Update
}
[SerializeField]
private ESortingType sortingType;
private SpriteSorter sorter;
private SpriteRenderer spriteRenderer;
// 씬의 SpriteSorter와 게임오브젝트에 붙은 SpriteRender를 찾아와 저장
void Start()
{
sorter = FindObjectOfType<SpriteSorter>();
spriteRenderer = GetComponent<SpriteRenderer>();
// Sorter로 구한 order를 넣어줌
spriteRenderer.sortingOrder = sorter.GetSortingOrder(gameObject);
}
// sortingType이 update일때만 sortingOrder를 구해서 변경
void Update()
{
if(sortingType == ESortingType.Update){
spriteRenderer.sortingOrder = sorter.GetSortingOrder(gameObject);
}
}
}
코드 작성이 마치면 이전에 만들었던 Sprite Sorter 오브젝트에 SpriteSorter 스크립트를 컴포넌트로 추가한 후 자식인 Back과 Front를 할당해줍니다. 그리고 씬에 있는 박스들에 SortingSprite 스크립트를 컴포넌트로 붙이고 타입을 Static으로 지정합니다. 그리고 Lobby Player Character prefab에도 해당 스크립트를 붙인 후 타입을 Update로 지정합니다.
게임을 실행하면 캐릭터가 상자 뒤에 있어도 이미지가 위로 올라오는 현상이 사라진 것을 확인할 수 있습니다.