Git

https://github.com/mjmj1/The-Zoo

InputHandler.cs

internal class InputHandler : MonoBehaviour
{
	public Vector2 MoveInput { get; private set; }
	public Vector2 LookInput { get; private set; }
	
	private void Awake()  
	{
	    InputActions = new PlayerInputActions();
	    
	    InputActions.Player.Move.performed += ctx => MoveInput = ctx.ReadValue<Vector2>();
	    InputActions.Player.Move.canceled += ctx => MoveInput = Vector2.zero;
	    
	    InputActions.Player.Look.performed += ctx => LookInput = ctx.ReadValue<Vector2>();
	    InputActions.Player.Look.canceled += ctx => LookInput = Vector2.zero;
	}
}
  • Input System을 활용하여 이벤트 기반으로 매 프레임 마다 호출하는 대신 performed/canceled 이벤트로 불필요 연산을 최소화할 수 있음
  • 추가적인 동작을 정의하고 싶다면 인터페이스만 맞춰서 구독하면 동작하므로 확장성이 좋음

PlanetGravity.cs

public class PlanetGravity : MonoBehaviour
{
    public static PlanetGravity Instance { get; private set; }
  
    private readonly float gravityStrength = 9.81f;
    private readonly HashSet<Rigidbody> affectedBodies = new();
  
    private void Awake()  
    {
        if (!Instance) Instance = this;  
        else Destroy(gameObject);
    }
    
    private void FixedUpdate()  
	{  
	    ApplyGravity();  
	}
    
    private void ApplyGravity()
	{
	    foreach (var rb in affectedBodies)
	    {
	        if (!rb) continue;
	        rb.AddForce(GetGravityDirection(rb.position) * gravityStrength, ForceMode.Acceleration);  
	    }
	}
	
	public Vector3 GetGravityDirection(Vector3 position)
	{  
	    return (transform.position - position).normalized;  
	}
	
	public void Subscribe(Rigidbody rb)  
	{  
	    if (rb) affectedBodies.Add(rb);
	}  
	  
	public void Unsubscribe(Rigidbody rb)  
	{  
	    if (rb) affectedBodies.Remove(rb);
	}
}
  • 구(球) 형태의 맵을 돌아다니는 것으로 기획을 하였기 때문에 기본 중력을 사용하지 않고 직접 구현한 중력을 사용함
  • 싱글톤 패턴을 사용하여 오직 하나만 존재하는 것을 보장하며 PlanetGravity.Instance으로 간단하게 접근 가능
  • 구독 형식으로 구현하여 힘을 받는 오브젝트를 동적으로 관리할 수 있도록 함
  • 구 형태의 게임 오브젝트에 부착하면 효과를 받는 오브젝트로부터 구 형태의 게임 오브젝트의 중심으로 일정 간격마다 (FixedUpdate) 힘이 가해짐

PlayerController.cs

public class PlayerController : NetworkTransform  
{
	... // 중략
	
	private void Update()  
	{  
	    if (!IsOwner) return;  
	  
	    AlignToSurface();  
	}
	
	private void AlignToSurface()  
	{  
	    if (!PlanetGravity.Instance) return;  
	  
	    var gravityDirection = -PlanetGravity.Instance.GetGravityDirection(transform.position);  
	  
	    var targetRotation = Quaternion.FromToRotation(  
	        transform.up, gravityDirection) * transform.rotation;  
	    transform.rotation = Quaternion.Slerp(  
	        transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);  
	}
	
	... // 중략
}
  • 구 형태의 맵에 맞춰 지속적으로 플레이어의 Y축을 정렬

RoleManager.cs

public struct PlayerData : INetworkSerializable, IEquatable<PlayerData>  
{  
    public ulong ClientId;
    public FixedString32Bytes Name;
    public int AnimalIndex;
  
    public PlayerData(ulong id, FixedString32Bytes name, int index)  
    {
        ClientId = id;  
        Name = name;  
        AnimalIndex = index;  
    }  
    public bool Equals(PlayerData other)  
    {
        return ClientId == other.ClientId && Name.Equals(other.Name) && 
        AnimalIndex.Equals(other.AnimalIndex);  
    }  
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter  
    {  
        serializer.SerializeValue(ref ClientId);  
        serializer.SerializeValue(ref Name);  
        serializer.SerializeValue(ref AnimalIndex);  
    }
}
  • 네트워크에 동기화될 데이터를 정의한 구조체
public class RoleManager : NetworkBehaviour  
{  
    public NetworkList<PlayerData> HiderIds = new();  
    public NetworkList<PlayerData> SeekerIds = new();
    
    internal void AssignRole()  
	{  
	    var clients = NetworkManager.Singleton.ConnectedClientsList;  
	    var seeker = Random.Range(0, clients.Count);  
	  
	    for (var i = 0; i < clients.Count; i++)  
	    {
	        var entity = clients[i].PlayerObject.GetComponent<PlayerEntity>();  
	  
	        var playerName = entity.playerName.Value;  
	        var index = entity.animalIndex.Value;  
	  
	        var data = new PlayerData(clients[i].ClientId, playerName, index);  
	  
	        if (seeker == i)  
	            SeekerIds.Add(data);  
	        else  
	            HiderIds.Add(data);  
	    }
	}
}
  • 인게임 진입 시 플레이어들에게 역할(Hider, Seeker)를 부여하는 스크립트
  • 자동으로 동기화해주는 NetworkList를 사용, 단 NetworkList는 INetworkSerializable, IEquatable을 상속받은 값만 받을 수 있음

PlayerSpawner.cs

public class SpawnObjectStore : MonoBehaviour
{
	[SerializeField] private List<AnimalData> animalDataList;
 
	public static SpawnObjectStore Instance { get; private set; }
 
	public void Awake()
	{
		if (!Instance) Instance = this;
		else Destroy(gameObject);
	}
 
	public AnimalData GetAnimalData(AnimalType type)
	{
		var data = animalDataList.Find(d => d.type == type);
		
		return data;
	}
 
	public int GetLength()
	{
		return animalDataList.Count;
	}
}
  • 스크립터블 오브젝트로 NPC Prefab과 Player Prefab을 한번에 관리하고 이를 가지고 있는 클래스
  • 싱글톤으로 구현
public class PlayerSpawner : NetworkBehaviour
{
	private readonly NetworkList<int> spawnedAnimals = new();
 
	protected override void OnNetworkSessionSynchronized()
	{
		Spawn();
 
		base.OnNetworkSessionSynchronized();
	}
 
	[Rpc(SendTo.Owner)]
	private void AddRpc(AnimalType type)
	{
		spawnedAnimals.Add((int)type);
	}
 
	[Rpc(SendTo.Owner)]
	internal void RemoveRpc(AnimalType type)
	{
		spawnedAnimals.Remove((int)type);
	}
 
	private void Spawn()
	{
		var length = SpawnObjectStore.Instance.GetLength();
 
		var type = GetRandomAnimalTypeDistrict(length);
 
		SpawnPlayerObject(type);
 
		AddRpc(type);
	}
 
	private AnimalType GetRandomAnimalTypeDistrict(int max)
	{
		var candidates = Enumerable
			.Range(0, max)
			.Where(i => !spawnedAnimals.Contains(i))
			.ToList();
 
		if (candidates.Count == 0) return 0;
 
		return (AnimalType)candidates[Random.Range(0, candidates.Count)];
	}
 
	private void SpawnPlayerObject(AnimalType type)
	{
		var data = SpawnObjectStore.Instance.GetAnimalData(type);
		var prefab = data.playerPrefab;
 
		var netObj = prefab.InstantiateAndSpawn(NetworkManager,
			NetworkManager.LocalClientId,
			isPlayerObject: true);
 
		netObj.GetComponent<PlayerEntity>().animalType.Value = type;
	}
}
  • 모든 동물 프리팹 중 이미 소환되어 있는 프리팹을 제외하고 랜덤하게 플레이어로 소환
  • 소환된 동물은 enum 으로 관리하고 networklist를 통해 동기화