PlayerStats의 Level 시스템을 개발하기 위해서는 레벨업과 관련된 이벤트를 먼저 만들어야 하는데, 이왕 만들어야 하는 김에 중립 기체와의 기본적인 상호작용을 개발했다.
Probe는 내가 임의로 중립기체에 붙인 명칭으로, NPC라고 생각하면 된다.
Probe에는 다음과 같은 기능이 내장되어야 한다.
일단 아무 곳이나 이동하는 기능을 탑재하게 되면 오히려 디버깅이 귀찮아질 수 있기 때문에 이동기능은 구현하지 않았고,
파괴시 파괴자에게 보상을 제공하는 기능을 먼저 개발했다.
private NetworkVariable<HealthState> _health;
private Transform _lastHit, _healthBar;
private NetworkVariable<HealthState> Health { get => _health; set => _health = value; }
public Transform LastHit { get => _lastHit; set => _lastHit = value; }
필드 변수는 총 3개로, health 정보를 담을 NetworkVariable, 하위 오브젝트로 부착되어있는 체력바, 그리고 파괴자를 기록하기 위해 가장 최근에 공격한 플레이어를 담는다.
private struct HealthState : INetworkSerializable
{
private short _health, _maxHealth;
internal short CurrentHP { readonly get => _health; set => _health = value; }
internal short MaxHealth { readonly get => _maxHealth; set => _maxHealth = value; }
void INetworkSerializable.NetworkSerialize<T>(BufferSerializer<T> serializer)
{
serializer.SerializeValue(ref _health);
serializer.SerializeValue(ref _maxHealth);
}
}
NetworkVariable에는 Serializable 한 데이터만 담을 수 있으므로, 따로 Health State를 만들어주었다.
private void Awake()
{
var readPerm = NetworkVariableReadPermission.Everyone;
var writePerm = NetworkVariableWritePermission.Server;
_health = new(readPerm: readPerm, writePerm: writePerm);
_healthBar = transform.GetChild(1);
}
public override void OnNetworkSpawn()
{
Health.Value = new HealthState { CurrentHP = 100, MaxHealth = 100 };
Health.OnValueChanged += OnHealthChanged;
}
초기화는 위와 같이 한다. 다만, 현재는 Max Health와 Current HP가 하드코딩 되어있는데, 나중에는 Probe에도 Level을 부여해서 Level에 비례하도록 수정할 예정이다.
#region vital
[ServerRpc]
public void TakeDamageServerRpc(short damage)
{
TakeDamage(damage);
}
private void TakeDamage(short damage)
{
if (!IsServer) return;
short HP = Health.Value.CurrentHP;
HP -= damage;
Health.Value = new HealthState { CurrentHP = HP, MaxHealth = Health.Value.MaxHealth };
}
private void OnTriggerEnter2D(Collider2D collider)
{
if (!IsOwner) return;
Transform obj = collider.transform;
if (obj.CompareTag("Projectile"))
{
_lastHit = obj.GetComponent<Projectile>().Shooter;
short damage = obj.GetComponent<Projectile>().Damage;
TakeDamageServerRpc(damage);
TakeDamage(damage);
}
}
private void OnHealthChanged(HealthState _, HealthState newHealth)
{
_healthBar.GetComponent<HealthBar>().SetHealth(newHealth.CurrentHP, newHealth.MaxHealth);
if (newHealth.CurrentHP <= 0) DieServerRpc();
}
[ServerRpc]
private void DieServerRpc()
{
Die();
}
private void Die()
{
if (!IsServer) return;
if (_lastHit != null)
{
var state = new ProbeDropState
{
Killer = _lastHit.GetComponent<PlayerStats>().OwnerClientId,
Fuel = 100,
Ammo = 20,
Exp = 100
};
OnKilledServerRpc(state);
}
GetComponent<NetworkObject>().Despawn();
Destroy(gameObject);
}
#endregion
체력 관련 코드는 위와 같다. 이전 포스트의 내용과 거의 일치하므로 설명은 생략한다.
차이점은 Probe의 경우 Owner인 Client가 없기 때문에 ClientRPC는 제거된 것을 볼 수 있다.
#region kill
[ServerRpc]
private void OnKilledServerRpc(ProbeDropState state)
{
PlayerStats killer = NetworkManager.Singleton.ConnectedClients[state.Killer].PlayerObject.GetComponent<PlayerStats>();
killer.AddFuelServerRpc(state.Fuel);
killer.AddAmmoServerRpc(state.Ammo);
killer.AddExpServerRpc(state.Exp);
Debug.Log($"Player {state.Killer} killed a probe");
Debug.Log($"Acquired: \nFuel: {state.Fuel}, \nAmmo: {state.Ammo}, \nExp: {state.Exp}");
}
#endregion
어차피 이벤트 처리후 파괴될 예정이고 Owner Client도 없기 때문에 ServerRpc로 한번만 실행해주면 전혀 상관이 없다.
그런데, 여기서 ProbeDropState가 무엇인지 궁금할 것이다.
private struct ProbeDropState : INetworkSerializable
{
private ulong _killer;
private short _fuel, _ammo, _exp;
internal ulong Killer { readonly get => _killer; set => _killer = value; }
internal short Fuel { readonly get => _fuel; set => _fuel = value; }
internal short Ammo { readonly get => _ammo; set => _ammo = value; }
internal short Exp { readonly get => _exp; set => _exp = value; }
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref _killer);
serializer.SerializeValue(ref _fuel);
serializer.SerializeValue(ref _ammo);
serializer.SerializeValue(ref _exp);
}
}
ProbeDropState는 다음과 같은 struct다.
파괴자에게 제공되는 보상을 담고 있다.
OnKilledServerRpc 메서드에서는 Network Manager가 가지고 있는 Client 리스트에서 파괴자 오브젝트를 찾아내고, 해당 오브젝트에 보상을 제공하는 기능이 담겨있다.
일단 플레이어가 접속하기 전에 서버에서 Probe를 소환해야 하기 때문에 Spawner 클래스를 따로 만들었다.
아래와 같이 서버가 시작되면 콜백으로 Probe Spawn 메서드가 실행된다.
using Unity.Netcode;
using UnityEngine;
public class ProbeSpawner : MonoBehaviour
{
public GameObject probe;
void Awake()
{
NetworkManager.Singleton.OnServerStarted += SpawnProbe;
}
private void SpawnProbe()
{
GameObject npc = Instantiate(probe, new Vector3(0, 0, 0), Quaternion.identity);
npc.GetComponent<NetworkObject>().Spawn();
}
}
아직 소환되는 좌표는 하드코딩했지만, 조만간 플레이어 소환과 함께 수정할 예정이다.