010-Repeat Unity FPS Sample - Bootstrapper - 03
#유니티, 유니티 튜토리얼, 유니티 강좌, Unity, Unity tutorial, HDRP, FPS Sample, 게임 개발, C#
안녕하세요?
이번에도 계속해서 Bootstrapper scene을 작성해 보겠습니다. 지난번에 살펴본 바와 같이 Bootstrapper scene에는 "Game" 게임오브젝트가 하나만 추가되어 있으며, Movable Box Prototype을 제외한 나머지 Field의 값/Asset은 모두 작성을 끝내고 할당하는 작업을 마쳤었습니다. 이제 Movable Box Prototype에 MovableCube Prefab을 작성하여 할당해 보겠습니다.
MovableCube는 아래의 폴더에 위치하고 있습니다.
MovableCube Prefab을 Double 클릭해 봅니다(지난 글에 이미 했었지만, 한 번 더 해봅니다.).
보기만 해도 뭔가 많이 복잡한 Prefab인 것 같습니다.
작성을 시작합니다.
bootstrapper scene에 Cube를 추가하고, 이름을 "MovableCube"로 변경합니다.
Inspector창에서 Add Component를 눌러 Game Object Entity Script를 추가합니다.
Inspector에서 순서는 그리 중요하지 않지만, Game Object Entity Script를 드래그하여 Transform 바로 다음에 위치하도록 이동시킵니다.
Transform을 변경하지 않았군요. 아래와 같이 Y position 값을 수정합니다.
다시 Add Component를 눌러 RigidBody를 추가합니다. 설정은 아래와 동일하다면, 변경하지 않습니다.
Add Component 버튼을 눌러 Movable Script를 추가합니다.
Add Component 버튼을 눌러 Replicated Entity Script를 추가합니다.
아래와 같이 2개의 Script가 추가되었습니다.
하지만 Replicated Entity Script에서 뭔가 문제가 있어 보이는 군요. 우선 Material을 수정하고 다시 돌아오겠습니다.
Cube에 할당된 Material은 DefaultHDMaterial인데 색이 하얀색입니다.
Base Color 부분의 색을 아래와 같이 회색으로 변경합니다. HEX : BABABA
다시 Replicated Entity Script로 돌아와서 아까 본 오류를 해결해 보겠습니다. FPS Sample에서 해당 Script 부분을 Inspector에서 확인하니, Entity registry index가 18이고 Unregister 버튼도 보입니다.
오류가 난 부분의 Description을 보니 ReplicatedEntityRegistry가 프로젝트에 없다고 나오는 군요.
Visual Studio로 Script를 Open합니다.
하지만 별다른 단서를 얻지 못합니다. Entity를 처리해주는 것 밖에 없고, 별도 GUI 등에 대한 처리는 찾을 수가 없네요.
별도 Code를 접은글로 추가했으니, 한번 살펴 보세요.
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
// TODO (mogensh) non generic base interface for predicted and interpolated data handlers. Used to find correct
// serializers when replicated entity is registered. Someone with more C# generics and reflection knowledge should
// be able to get rid of these
public interface IPredictedDataBase
{}
public interface IInterpolatedDataBase
{}
// Interface used for components that should always be serialized from server to client
public interface INetSerialized
{
void Serialize(ref NetworkWriter writer, IEntityReferenceSerializer refSerializer);
void Deserialize(ref NetworkReader reader, IEntityReferenceSerializer refSerializer, int tick);
}
// Interface for components that are replicated only to predicting clients
public interface INetPredicted<T> : IPredictedDataBase
{
void Serialize(ref NetworkWriter writer, IEntityReferenceSerializer refSerializer);
void Deserialize(ref NetworkReader reader, IEntityReferenceSerializer refSerializer, int tick);
#if UNITY_EDITOR
bool VerifyPrediction(ref T state);
#endif
}
// Interface for components that are replicated to all non-predicting clients
public interface INetInterpolated<T> : IInterpolatedDataBase
{
void Serialize(ref NetworkWriter writer, IEntityReferenceSerializer refSerializer);
void Deserialize(ref NetworkReader reader, IEntityReferenceSerializer refSerializer, int tick);
void Interpolate(ref T first, ref T last, float t);
}
public interface IEntityReferenceSerializer
{
void SerializeReference(ref NetworkWriter writer, string name, Entity entity);
void DeserializeReference(ref NetworkReader reader, ref Entity entity);
}
public struct ReplicatedDataEntity : IComponentData, INetSerialized
{
public int typeId;
public int id;
public int predictingPlayerId;
public void Serialize(ref NetworkWriter writer, IEntityReferenceSerializer refSerializer)
{
writer.WriteInt32("predictingPlayerId",predictingPlayerId);
}
public void Deserialize(ref NetworkReader reader, IEntityReferenceSerializer refSerializer, int tick)
{
predictingPlayerId = reader.ReadInt32();
}
}
[ExecuteAlways, DisallowMultipleComponent]
[RequireComponent(typeof(GameObjectEntity))]
public class ReplicatedEntity : MonoBehaviour, INetSerialized
{
public byte[] netID; // guid of instance. Used for identifying replicated entities from the scene
public int registryId = -1;
[NonSerialized] public int id = -1; // network id, used to identify the entity for references and towards the network layer
[NonSerialized] public int predictingPlayerId = -1; // The player id that is predicting this entity or -1 if none
public void Serialize(ref NetworkWriter writer, IEntityReferenceSerializer refSerializer)
{
writer.WriteInt32("predictingPlayerId",predictingPlayerId);
}
public void Deserialize(ref NetworkReader reader, IEntityReferenceSerializer refSerializer, int tick)
{
predictingPlayerId = reader.ReadInt32();
}
#if UNITY_EDITOR
public static Dictionary<byte[], ReplicatedEntity> netGuidMap = new Dictionary<byte[], ReplicatedEntity>(new ByteArrayComp());
private void Awake()
{
if (EditorApplication.isPlaying)
return;
SetUniqueNetID();
}
private void OnValidate()
{
if (EditorApplication.isPlaying)
return;
PrefabType prefabType = PrefabUtility.GetPrefabType(this);
if (prefabType == PrefabType.Prefab || prefabType == PrefabType.ModelPrefab)
{
netID = null;
}
else
SetUniqueNetID();
}
private void SetUniqueNetID()
{
// Generate new if fresh object
if (netID == null || netID.Length == 0)
{
netID = System.Guid.NewGuid().ToByteArray();
EditorSceneManager.MarkSceneDirty(gameObject.scene);
}
// If we are the first add us
if (!netGuidMap.ContainsKey(netID))
{
netGuidMap[netID] = this;
return;
}
// Our guid is known and in use by another object??
var oldReg = netGuidMap[netID];
if (oldReg != null && oldReg.GetInstanceID() != this.GetInstanceID() && ByteArrayComp.instance.Equals(oldReg.netID, netID))
{
// If actually *is* another ReplEnt that has our netID, *then* we give it up (usually happens because of copy / paste)
netID = System.Guid.NewGuid().ToByteArray();
EditorSceneManager.MarkSceneDirty(gameObject.scene);
}
netGuidMap[netID] = this;
}
#endif
}
[DisableAutoCreation]
public class UpdateReplicatedOwnerFlag : BaseComponentSystem
{
ComponentGroup RepEntityGroup;
ComponentGroup RepEntityDataGroup;
int m_localPlayerId;
bool m_initialized;
public UpdateReplicatedOwnerFlag(GameWorld world) : base(world)
{}
protected override void OnCreateManager()
{
base.OnCreateManager();
RepEntityGroup = GetComponentGroup(typeof(ReplicatedEntity));
RepEntityDataGroup = GetComponentGroup(typeof(ReplicatedDataEntity));
}
public void SetLocalPlayerId(int playerId)
{
m_localPlayerId = playerId;
m_initialized = true;
}
protected override void OnUpdate()
{
var entityArray = RepEntityGroup.GetEntityArray();
var replicatedArray = RepEntityGroup.GetComponentArray<ReplicatedEntity>();
for (int i = 0; i < entityArray.Length; i++)
{
var repEntity = replicatedArray[i];
var locallyControlled = m_localPlayerId == -1 || repEntity.predictingPlayerId == m_localPlayerId;
SetFlagAndChildFlags(entityArray[i], locallyControlled);
}
entityArray = RepEntityDataGroup.GetEntityArray();
var repEntityDataArray = RepEntityDataGroup.GetComponentDataArray<ReplicatedDataEntity>();
for (int i = 0; i < entityArray.Length; i++)
{
var repDataEntity = repEntityDataArray[i];
var locallyControlled = m_localPlayerId == -1 || repDataEntity.predictingPlayerId == m_localPlayerId;
SetFlagAndChildFlags(entityArray[i], locallyControlled);
}
}
void SetFlagAndChildFlags(Entity entity, bool set)
{
SetFlag(entity, set);
if (EntityManager.HasComponent<EntityGroupChildren>(entity))
{
var buffer = EntityManager.GetBuffer<EntityGroupChildren>(entity);
for (int i = 0; i < buffer.Length; i++)
{
SetFlag(buffer[i].entity, set);
}
}
}
void SetFlag(Entity entity, bool set)
{
var flagSet = EntityManager.HasComponent<ServerEntity>(entity);
if (flagSet != set)
{
if (set)
PostUpdateCommands.AddComponent(entity, new ServerEntity());
else
PostUpdateCommands.RemoveComponent<ServerEntity>(entity);
}
}
}
단서를 찾기 위해 "Replica" 로 검색을 해봅니다. 몇가지 단서가 나오네요. Editor 관련 이름을 가진 Script가 있군요.
ReplicatedEntityEditor를 Open해 봅니다. 그랬더니, CustomEditor attribute에서 ReplicatedEntity 형에 추가되는 것임을 알 수 있고, 오류 문구와 동일한 것을 뿌려주는 HelpBox 함수를 쓰는 부분도 있습니다, 그래서 이 스크립트가 오류 메시지를 뿌린 그분인가 봅니다.
[CustomEditor(typeof(ReplicatedEntity))]
public class ReplicatedEntityEditor : Editor
{
public override void OnInspectorGUI()
{
var registry = ReplicatedEntityRegistry.GetReplicatedEntityRegistry();
if (registry == null)
{
EditorGUILayout.HelpBox("Make sure you have a ReplicatedEntityRegistry in project", MessageType.Error);
return;
}
var registry = ReplicatedEntityRegistry.GetReplicatedEntityRegistry(); 부분에서 뭔가 이루어지지 않으면 오류 메시지를 뿌리도록 되어 있는 것을 보니 ReplicatedEntityRegistry class를 또 찾아봐야 할 것 같습니다.
동일한 이름의 Script가 검색되어 열어보니, 이제야 작은 비밀이 나타납니다.
public static ReplicatedEntityRegistry GetReplicatedEntityRegistry()
{
var registryGuids = AssetDatabase.FindAssets("t:ReplicatedEntityRegistry");
if (registryGuids == null || registryGuids.Length == 0)
{
GameDebug.LogError("Failed to find ReplicatedEntityRegistry");
return null;
}
if (registryGuids.Length > 1)
{
GameDebug.LogError("There should only be one ReplicatedEntityRegistry in project");
return null;
}
var guid = registryGuids[0];
var registryPath = AssetDatabase.GUIDToAssetPath(guid);
var registry = AssetDatabase.LoadAssetAtPath<ReplicatedEntityRegistry>(registryPath);
return registry;
}
var registryGuids = AssetDatabase.FindAssets("t:ReplicatedEntityRegistry");
ReplicatedEntityRegistry 형식의 Asset을 찾아서 Load 하는 군요. Load가 실패하면 위에서 오류를 표시하도록 되어 있네요. 그렇다면 ReplicatedEngityRegistry 가 어딘가에 있다는 말이고, 이것을 만들어야 한다는 것까지 판단이 섭니다.
그리고 아래의 Class 선언부에서 FPS Sample > ReplicatedEntity > ReplicatedEntityRegistry 라는 메뉴가 존재한다는 것도 알아냈습니다.
[CreateAssetMenu(fileName = "ReplicatedEntityRegistry",
menuName = "FPS Sample/ReplicatedEntity/ReplicatedEntityRegistry")]
public class ReplicatedEntityRegistry : RegistryBase
ReplicatedEntityRegistry가 상속 받은 RegistryBase class로 이동해 보니, ScriptableObject 형인 것을 알 수 있습니다.
public abstract class RegistryBase : ScriptableObject
{
#if UNITY_EDITOR
public abstract void GetSingleAssetGUIDs(List<string> guids, bool serverBuild);
public virtual bool Verify()
{
return true;
}
#endif
}
그럼 FPS Sample에서 이 Scriptable Object는 어디에 생성이 되어 있을까요?
위치는 Assets/BundledResources/Shared/ReplicatedEntityRegistry.asset 입니다.
동일하게 폴더를 구성합니다.
그리고 아래와 같이 ReplicatedEntityRegistry Scriptable Object를 생성합니다.
아래와 같이 ReplicatedReplicatedEntityRegistry가 생성되었습니다.
이렇게 하고 다시 MovableCube로 돌아가 Replicated Entity Script가 잘 작동하는지 살펴 봅니다.
이번에는 Replicated Entity가 Prefab의 Root에 있어야 한다고 하네요. 그럼 MovableCube에 Component 들도 모두 추가했으니, Prefab으로 만들어 보겠습니다. 그럼 Replicated Entity script가 Root에 존재해야 한다는 조건도 만족하겠네요.
Prefab이 저장된 위치를 FPS Sample과 동일하게 하도록 폴더를 생성합니다. 저장 위치는 Assets/Prefabs/ReplicatedEntities 입니다.
그리고 MovableCube를 Hierarchy 창에서 Drag하여 Project 창에서 Drop 합니다. 이렇게 하면 Prefab이 만들어 집니다.
만들어진 Prefab을 더블클릭하니, 이제는 등록된 것이 없다고 나옵니다. 이제 이곳에다 뭔가 또 등록을 해야 하는가 봅니다.
일단 작성을 했으니, "Game" 게임오브젝트에 할당합니다.
bootstrapper scene에서 Prefab을 만들기 위해 추가했던 MovableCube 게임오브젝트는 삭제합니다.
이제 ReplicatedEntityRegistry에 무언가 할당을 하기 위해 FPS Sample에서 어떤 것이 이 Scriptable Object에 할당되어 있는지 확인합니다.
엄청나게 많군요. 이 Prefab, Scriptable Object 들을 만들자면 또 한참이 걸릴 것 같습니다.
다음에 또 이어서 작성하겠습니다.
- 다소승탁진 -