어몽어스 - 캐릭터 색상 선택 (1)

본 포스트는 베르님의 Make the 어몽어스를 정리한 포스트입니다.
https://www.youtube.com/watch?v=xht2y9op61E&list=PLYQHfkihy4Aw6QjsZqwwbD4ihpwvm7N0U&index=8

이번 시간에는 대기실에서 캐릭터의 색상을 선택하는 기능을 만듭니다.

먼저 베르님이 제공해주신 하단의 링크에서 리소스를 다운받습니다.
https://drive.google.com/file/d/1nXp2zFBBRl5WMxkY_1WcaIPvrg8Xk7fW/view

1. 플레이어 색상 자동 지정 기능

해당 기능을 구현하기 전 플레이어가 방에 접속했을 때 색상을 자동으로 지정해주는 기능을 구현하겠습니다. AmongUsRoomManager 스크립트를 열면 OnRoomServerConnect 함수가 있는데, 이는 캐릭터가 대기실에 생성되기 이전의 시점입니다. RoomPlayer를 찾아 색상 지정을 하기 위해서는 생성된 이후로 코드를 이동해야합니다. AmongUsRoomPlayer의 start 함수가 가장 확실한 지점이기에 해당 시점으로 이동시킵니다. (AmongUsRoomManger에서 Spawn과 관련된 부분을 잘라서 Player부분으로 들고옵니다.)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror; // 선언

public class AmongUsRoomPlayer : NetworkRoomPlayer
{
    [SyncVar] // 네트워크 통해 동기화
    public EPlayerColor playerColor;

    private void SpawnLobbyPlayerCharacter(){
        // 대기실에 접속 중인 플레이어를 가져옴
        var roomSlots = (NetworkManager.singleton as AmongUsRoomManager).roomSlots;
        EPlayerColor color = EPlayerColor.Red;
        // 플레이어들이 든 roomSlots를 돌면서 사용하지 않는 색상 고름
        for(int i = 0; i < (int)EPlayerColor.Lime + 1; i++){
            bool isFindSameColor = false;
            foreach(var roomPlayer in roomSlots){
                var amongUsRoomPlayer = roomPlayer as AmongUsRoomPlayer;
                if(amongUsRoomPlayer.playerColor == (EPlayerColor)i && roomPlayer.netId != netId){
                    isFindSameColor = true;
                    break;
                }
            }
            if(!isFindSameColor){
                color = (EPlayerColor)i;
                break;
            }
        }
        // 자신의 색상으로 지정
        playerColor = color;
		
        // AmongUsRoomManager에서 옮겨옴
        Vector3 spawnPos = FindObjectOfType<SpawnPositions>().GetSpawnPosition();

     
        var player = Instantiate(AmongUsRoomManager.singleton.spawnPrefabs[0], spawnPos, Quaternion.identity); 
        NetworkServer.Spawn(player,connectionToClient); 
       // 

}

그 다음 AmongUsRoomPlayer에서 생성한 SpawnLobbyPlayerCharacter 함수에서 가장 먼저할 일은 새로 생성한 플레이어의 색상을 고르는 일입니다. 다른 플레이어와 색상을 겹치지 않게 하고자 다른 플레이어들의 오브젝트를 가져와 가지고 있는 색상을 검사해야합니다.

그 다음 생성된 캐릭터가 자기 RoomPlayer의 색상을 가져와 색상을 바꾸도록 해야합니다. CharacterMover 스크립트를 열어 다음과 같이 수정합니다.

  1. SpriteRenderer 변수를 선언하고 Start 함수에서 가져와 자신의 색을 한 번 초기화합니다.
  2. hook 기능을 이용해 클라이언트에서 동기화된 값이 변경되었을 때 처리해야할 기능을 구현합니다. (서버에서 CharacterMover의 playerColor 변경 시 클라이언트에서 해당 변경을 감지하고 오브젝트의 색상을 변경합니다.)
  • Hook: SyncVar로 동기화된 변수가 서버에서 변경될 시, 클라이언트에서 hook으로 등록한 함수가 호출되도록 만드는 기능

public class CharacterMover : NetworkBehaviour
{
                                    .
                                    .

    private SpriteRenderer spriteRenderer;
    [SyncVar(hook = nameof(SetPlayerColor_Hook))]
    public EPlayerColor playerColor;
    public void SetPlayerColor_Hook(EPlayerColor oldColor, EPlayerColor newColor){
        if(spriteRenderer == null){
            spriteRenderer = GetComponent<SpriteRenderer>();
        }
        spriteRenderer.material.SetColor("_PlayerColor", PlayerColors.GetColor(newColor));

    }

    // Start is called before the first frame update
    void Start()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
        spriteRenderer.material.SetColor("_PlayerColor",PlayerColors.GetColor(playerColor));
        
                                    .
                                    .
        }
    }

다시 AmongUsRoomPlayer의 SpawnLobbyPlayerCharacter 함수에서 다음의 내용을 변경합니다.

        Vector3 spawnPos = FindObjectOfType<SpawnPositions>().GetSpawnPosition();
        // 생성한 플레이어의 LobbyCharacterMover를 가져와 playerColor를 변경함
        var playerCharacter = Instantiate(AmongUsRoomManager.singleton.spawnPrefabs[0], spawnPos, Quaternion.identity).GetComponent<LobbyCharacterMover>();
        NetworkServer.Spawn(playerCharacter.gameObject,connectionToClient);
        playerCharacter.playerColor = color;

그 다음 SpawnLobbyPlayerCharacter()를 호출하기 위해 start 함수를 생성합니다. 이 때 부모 클래스인 NetworkRoomPlayer 클래스에서 start 함수가 작성되어 있기 때문에 정상적인 동작을 위해 base.Start로 부모 클래스의 Start 함수를 호출합니다.

 public void Start(){
        base.Start(); // 부모 클래스 start 함수 호출
        if(isServer){
            SpawnLobbyPlayerCharacter();
        }
    }

에디터로 돌아와서 Lobby Player Character prefab을 열어 Sprite Renderer의 Material을 M_Crew로 변경합니다. 게임을 실행해보면 접속한 캐릭터들의 색상이 겹치지 않게 소환되는 것을 확인할 수 있습니다.

게임을 실행하면 다음과 같이 입장하는 플레이어마다 색상이 다르게 적용되는 것을 확인할 수 있습니다.
(+ 현재 크루원의 그림자가 계속 하얀색으로 뜨는데 해당 부분은 해결 방법을 찾으면 수정하도록 하겠습니다. shader 부분의 문제인 것 같긴 한데..)

해당 부분의 문제점이 확인되어 수정하였습니다.
기존의 shader graph에서 색을 빼기 위해 100으로 강화한 흰새을 사용하고 있어 문제가 발생한 것으로 확인되었습니다.

다음과 같이 노드 연결을 수정하니 해결되었습니다.

2. Customize UI

이전에 Settings UI 작업과 동일하게 시작합니다.

  1. Game Room Scene > Canvas > Customize UI (Create Empty)
  2. Anchor stretch-strech로 변경 후 Left, Top, Right, Bottm 0으로 변경
  3. 자식으로 Background 생성 (UI > Image)
  4. Anchor stretch-strech로 변경 후 Left, Top, Right, Bottm 0으로 변경
  5. 이미지 색상 alpha 값 0으로 변경

UI 패널과 버튼들을 배치해야 하는데, 그 전에 앞서 import한 스프라이트 리소스들에 대해 각각 Sprite Editor를 열어 이미지의 크기가 바뀌어도 문제가 없도록 9-슬라이싱을 한 후 apply합니다.

그 다음 패널과 버튼을 배치합니다. 이때, Panel을 생성한 후 나머지는 Panel의 자식들로 생성합니다. 버튼 아래에는 서브 Panel과 캐릭터의 색상을 보여 줄 캐릭터 이미지를 배치합니다.

(차례대로 Panel, Button, 서브 패널 아래의 캐릭터 inspector)

색상 버튼을 배치할 건데, 이미 선택된 색상은 반투명한 x 표시 버튼이 표시되게 만들어야 합니다. 다음과 같이 컬러 버튼 아래에 이미지를 추가하여 x 표시 버튼을 생성합니다.

그 다음 ColorSelectButton 스크립트를 생성하고 다음과 같이 작성합니다. 작성을 마치면 색상 버튼의 컴포넌트로 붙인 후 프로퍼티에 x가 표시된 이미지를 할당해줍니다.

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

public class ColorSelectButton : MonoBehaviour
{
    [SerializeField]
    private GameObject x;

    public bool isInteractable = true;

    // isInteractable의 상태와 x 이미지의 활성화 상태를 변경함
    public void SetInteractable(bool isInteractable){
        this.isInteractable = isInteractable;
        x.SetActive(!isInteractable); // x자 표시는 interactable 불가할 때 활성화되야하므로
    }
}

색상 버튼을 빠르게 정렬하기 위해 서브 패널 오브젝트에 Grid Layout Group 컴포넌트를 추가하고 다음과 같이 설정한 다음 버튼을 복사합니다. (12개) 그리고 버튼마다 색상을 지정해줍니다.

버튼 순서 대로 해당 컬러를 지정해주면 됩니다.
Red: FF0000, Blue: 0036FF, Green: 07BA00, Pink: FF75C3, Orange: FF9200, Yellow: FFFA00, Black: 313131, White: F6F6F6, Purple: 5F13CF, Brown: 5F13CF, Cyan: 00FFF2, Lime: 13FF00

그리고 Game Option 오브젝트도 미리 만들어두고 우선 보이지 않게 해놓습니다.

3. 색상 선택 기능 구현

이렇게 UI 배치가 끝나면 CustomizeUI 스크립트를 생성하고 다음과 같이 작성합니다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using  UnityEngine.UI; // 선언
using Mirror; // 선언

public class CustomizeUI : MonoBehaviour
{
    [SerializeField]
    private Image characterPreview; // 선택한 캐릭터의 색상을 미리 보여줄 image

    [SerializeField]
    private List<ColorSelectButton> colorSelectButtons; // 색상 버튼 상태
    // characterPreview의 material을 건드릴 때 원본 material에 영항을 미치지지 않도록 이미지 material을 인스턴싱
    void Start()
    {
        var inst = Instantiate(characterPreview.material);
        characterPreview.material = inst;
        
    }
    // 하단의 함수들 호출
    public void OnEnable(){
        UpdateColorButton();
        var roomSlots = (NetworkManager.singleton as AmongUsRoomManager).roomSlots;

        // 프리뷰에 자신의 캐릭터 색상이 보여야 하므로, RoomSlots에서 자기 캐릭터 찾아 호출
        foreach(var player in roomSlots){
            var aPlayer = player as AmongUsRoomPlayer;
            if(aPlayer.isLocalPlayer){
                UpdatePreviewColor(aPlayer.playerColor);
                break;
            }
        }
    }
    // 플레이어들의 색상 상태에 따라 버튼 상태 업데이트
    public void UpdateColorButton(){
        var roomSlots = (NetworkManager.singleton as AmongUsRoomManager).roomSlots;
        // 컬러버튼 모두 interactable true로 한 다음
        for(int i = 0; i < colorSelectButtons.Count; i++){
            colorSelectButtons[i].SetInteractable(true);
        }
        // 플레이어의 색상은 interactable false로
        foreach(var player in roomSlots){
            var aPlayer = player as AmongUsRoomPlayer;
            colorSelectButtons[(int)aPlayer.playerColor].SetInteractable(false);
        }
    }
    // 받아온 EPlayerColor에 따라 캐릭터 이미지 색상 변경
    public void UpdatePreviewColor(EPlayerColor color){
        characterPreview.material.SetColor("_PlayerColor",PlayerColors.GetColor(color));
    }

    // 색상 버튼 누를 때 기능
    public void OnClickColorButton(int index){
        // 버튼이 선택 가능한 색상이라면 자신의 RoomPlayer를 가져와 색상을 변경했다는 신호 보냄
        // RoomPlayer 가져오는 작업은 AmongUsRoomPlayer class에서 작업
        if(colorSelectButtons[index].isInteractable){
   

        }
    }
}

AmongUsRoomPlayer 스크립트에 다음 내용을 추가합니다. CmdSetPlayerColor는 start 함수 밑에 정의하였습니다.

public class AmongUsRoomPlayer : NetworkRoomPlayer
{
    private static AmongUsRoomPlayer myRoomPlayer;
    public static AmongUsRoomPlayer MyRoomPlayer{
        get{
            // myRoomPlayer가 비어있는 경우 한 번만 찾아서 저장한 뒤 반한하는 프로퍼티 생성
            if(myRoomPlayer == null){
                var players = FindObjectsOfType<AmongUsRoomPlayer>();
                foreach(var player in players){
                    if(player.hasAuthority){
                        myRoomPlayer = player;
                    }
                }
            }
            return myRoomPlayer;
        }
    }
                                  .
                                  .
    public CharacterMover lobbyPlayerCharacter;
    
    // 클라이언트에서 서버로 신호를 보내는 기능 
    [Command]
    public void CmdSetPlayerColor(EPlayerColor color){
        playerColor = color;
    }
 }
 }
  • [Command]: 해당 attribute는 Mirror API에서 제공하는 것으로 클라이언트에서 함수를 호출하면 함수 내부의 동작이 서버에서 실행되도록 만들어 줌 (command를 attribute로 사용하는 함수는 반드시 접두사로 Cmd를 붙여야 함)

함수 내에서 색상을 멤버 변수에 저장한 다음 Lobby Player Character에 전달해야 하는데, Scene의 오브젝트 중 일일이 찾아내는 방법은 성능이 불리합니다. 따라서 다른 방식으로 진행합니다.
1. CharacterMover 타입으로 lobbyPlayerCharacter 멤버 변수 선언
2. LobbyCharacterMover 클래스 수정

using Mirror;

public class LobbyCharacterMover : CharacterMover
{
    [SyncVar(hook = nameof(SetOnwerNetId_Hook))]
    public uint ownerNetID;

    // ownerNetId가 변경되면 클라이언트에서 호출
    // 받아온 ownerNetId를 이용해 자신의 RoomPlayer를 찾은 다음 거기에 생성된 LobbyCharacterMover 저장
    public void SetOnwerNetId_Hook(uint _, uint newOwnerId){
        var players = FindObjectsOfType<AmongUsRoomPlayer>();
        foreach(var player in players){
            if(newOwnerId == player.netId){
                player.lobbyPlayerCharacter = this;
                break;
            }
        }
    }
  1. AmongUsRoomPlayer spawn 부분 수정
	Vector3 spawnPos = FindObjectOfType<SpawnPositions>().GetSpawnPosition(); 

        var playerCharacter = Instantiate(AmongUsRoomManager.singleton.spawnPrefabs[0], spawnPos, Quaternion.identity).GetComponent<LobbyCharacterMover>(); 
        NetworkServer.Spawn(playerCharacter.gameObject,connectionToClient); 
        // 추가
        playerCharacter.ownerNetID = netId;
        //
        playerCharacter.playerColor = color;
  1. AmongUsRoomPlayer CmdSetPlayer 함수를 다음과 같이 수정 (캐릭터 오브젝트를 일일이 찾는 대신 미리 저장해둔 lobbyPlayerCharacter를 사용해 플레이어의 색상 변경함)
// 클라이언트에서 서버로 신호를 보내는 기능 
    [Command]
    public void CmdSetPlayerColor(EPlayerColor color){
        playerColor = color;
        lobbyPlayerCharacter.playerColor = color;
    }
  1. CustomizeUI 스크립트에서 자기 플레이어의 CmdSetPlayerColor 함수 호출
 public void OnClickColorButton(int index){
        if(colorSelectButtons[index].isInteractable){
            AmongUsRoomPlayer.MyRoomPlayer.CmdSetPlayerColor((EPlayerColor)index);
            UpdatePreviewColor((EPlayerColor)index);

        }
    }

그 다음 다른 사람이 색상을 변경했을 때 버튼이 업데이트 되는 기능을 구현하겠습니다. 로비에서 UI를 관리할 LobbyUIManager 스크립트를 생성하고 다음과 같이 작성합니다. (간단한 singleton으로 만들고, CustomizeUI를 호출할 수 있도록 함)

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

public class LobbyUIManager : MonoBehaviour
{
    public static LobbyUIManager Instance;
    [SerializeField]
    private CustomizeUI customizeUI;

    public CustomizeUI CustomizeUI {get {return customizeUI;}}
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

그 다음 AmongUsRoomPlayer 스크립트를 열고 다음을 수정합니다.



    [SyncVar(hook = nameof(SetPlayerColor_Hook))] // 네트워크 통해 동기화
    public EPlayerColor playerColor;
    // RoomPlayer의 PlayerColor 변경될 때 호출되는 hook
    public void SetPlayerColor_Hook(EPlayerColor oldColor, EPlayerColor newColor){
        LobbyUIManager.Instance.CustomizeUI.UpdateColorButton();
    }

코드 작성을 마치면 에디터로 돌아와 Canvas에 LobbyUIManager 스크립트를 컴포넌트로, Customize UI 오브젝트에 CustomizeUI 스크립트를 컴포넌트로 붙입니다. 그 다음 LobbyUIManager에는 CustomizeUI를 할당하고, CustomizeUI에는 캐릭터 프리뷰 이미지와 색상 버튼들을 할당합니다.

그리고 색상 버튼들의 On Click() 이벤트에 Customize UI의 OnClickColorButton 함수를 등록한 후 순서대로 매개변수를 입력합니다. (0~11)

마지막으로 캐릭터 프리뷰 이미지의 material에 M_Crew material을 할당합니다.

여기까지 전반적인 캐릭터의 색상 변경에 대한 작업을 마쳤습니다. 다음 시간에는 Cusotmize UI 열기/닫기에 대한 기능을 구현하겠습니다.

좋은 웹페이지 즐겨찾기