
만들고자 하는 게임이 슈팅 게임이기에 기본적 이동 구현이 완료되었으니 이제 투사체 발사를 구현할 차례다.
게임 시스템 상에서 구현되어야 하는 투사체의 종류가 좀 많기 때문에 먼저 투사체에 대한 인터페이스부터 만들었다.
using UnityEngine;
public interface IShootable
{
void Shoot(Vector3 direction, float speed);
void DestroyProjectile();
void OnTriggerEnter2D(Collider2D col);
}
이렇게 두 메서드를 implement 해야 하는 인터페이스를 만들었다.
그리고, default로 발사하는 투사체는 총알이기 때문에 IShootable을 상속하는 Bullet 클래스도 만들었다.
using UnityEngine;
public class Bullet : MonoBehaviour, IShootable
{
private Transform _shooter;
public Transform Shooter { get => _shooter; set => _shooter = value; }
public void Shoot(Vector3 direction, float speed)
{
GetComponent<Rigidbody2D>().AddForce(speed * direction);
Invoke(nameof(DestroyProjectile), 3);
}
private void OnTriggerEnter2D(Collider2D collider) {
if (collider.transform != _shooter && collider.transform.CompareTag("Player"))
DestroyProjectile();
}
public void DestroyProjectile()
{
Destroy(gameObject);
}
}
shooter 필드는 해당 투사체를 발사한 주체가 누구인지 저장하는 용도다.
나중에 설명하겠지만, 피격 이벤트에서 활용될 예정이다.
이렇게 기본적인 투사체 클래스를 제작했고, 플레이어에게 부착되는 PlayerShooting 클래스도 제작했다.
public class PlayerShooting : NetworkBehaviour
PlayerShooting 클래스는 투사체의 정보를 서버와 클라이언트 모두에게 전달해야 하기 때문에 NetworkBehaviour를 상속한다.
필드 변수는 아래와 같다.
[SerializeField] private Bullet _bullet;
[SerializeField] private float _bulletSpeed;
[SerializeField] private float _cooldown = 0.5f;
[SerializeField] private Transform _spawner;
private float _lastFired = float.MinValue;
발사되는 투사체가 Bullet 클래스의 오브젝트이기 때문에 해당 오브젝트를 수집한다.
lastFired는 연사 속도를 제한하기 위한 용도로, 발사되었던 시간을 기록한다.
void Update()
{
if (!IsOwner) return;
if (Input.GetMouseButtonDown(0) && _lastFired + _cooldown < Time.time)
{
_lastFired = Time.time;
Vector2 worldMousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
var dir = (Vector3)worldMousePos - transform.position;
dir = dir.normalized;
RequestFireServerRpc(dir);
ExecuteShoot(dir);
}
}
마우스의 왼쪽 버튼을 누르면 최근 발사했던 시간과 쿨타임을 계산하여 로컬과 서버에서 모두 투사체를 발사한다.
방향은 마우스 위치를 계산하여 정규화된 방향벡터를 사용한다.
[ServerRpc]
private void RequestFireServerRpc(Vector3 dir)
{
FireClientRpc(dir);
}
[ClientRpc]
private void FireClientRpc(Vector3 dir)
{
if (!IsOwner) ExecuteShoot(dir);
}
이렇게 서버와 클라이언트에서 각각 투사체를 발사하는데, 중복이 있으면 안되기 때문에 ClientRpc 메서드에서는 Owner가 아닐 경우만 발사한다.
private void ExecuteShoot(Vector3 dir)
{
var bullet = Instantiate(_bullet, _spawner.position, Quaternion.identity);
bullet.Shooter = transform;
bullet.Shoot(dir, _bulletSpeed);
}
실제로 투사체를 발사하도록 트리거하는 메서드다.
아까 만들었던 Bullet 클래스의 Shoot 메서드를 호출한다.
이때, 발사한 주체가 누구인지 기록하기 위해 Shooter에 현재 플레이어의 transform을 저장한다.
투사체는 Prefab으로 만들어놓을텐데, Rigidbody2D 컴포넌트의 옵션을 잘 설정해야 한다.
Dynamic으로 설정해놓지 않으면 AddForce 함수가 영향을 주지 않기 때문에 꼭 Dynamic으로 설정해야 한다.
피격 이벤트를 구현하려면, 누가 발사해서 누가 맞았는지를 알아야 한다.
그러기 위해서는 먼저 플레이어들에게 고유번호가 있어야 한다.
따라서 고유번호를 부여하는 메서드를 구현했다.
딱히 어느 클래스에 넣어야 올바를지 모르겠어서 우선은 PlayerTransform 클래스에 넣었지만, 추후 수정될 수 있다.
private NetworkVariable<FixedString32Bytes> _id;
public NetworkVariable<FixedString32Bytes> Id { get => _id; set => _id = value; }
이렇게 추가로 id를 담을 필드변수를 추가했다. 다른 클라이언트도 상대의 id를 알아야 하기 때문에 NetworkVariable로 구성한다.
private void Awake()
{
_rb = GetComponent<Rigidbody2D>();
_jet = transform.GetChild(0).gameObject;
Id = new();
var permission = _usingServerAuth ? NetworkVariableWritePermission.Server : NetworkVariableWritePermission.Owner;
_playerState = new NetworkVariable<PlayerTransformState>(writePerm: permission);
}
Awake 함수에서 초기화 해준다.
[ServerRpc(RequireOwnership = false)]
public void SendIdServerRpc(string id)
{
Id.Value = id;
}
그리고, 이렇게 NetworkVariable에 값을 저장하는 함수를 만든다.
RequireOwnership이 false로 되어있는 이유는 클라이언트와 서버 모두 다른 플레이어와 자신의 id에 접근할 수 있어야 하기 때문이다.
public override void OnNetworkSpawn()
{
if (!IsOwner) Destroy(transform.GetComponent<PlayerController>());
SendIdServerRpc($"Player {OwnerClientId}");
}
그 후 이렇게 OnNetworkSpawn에서 클라이언트 id를 담아준다.
이제 플레이어에게 고유값이 부여된다.
우선, OnCollisionEnter 말고 이 메서드를 사용하는 이유는 물리 계산을 하지 않아 더 효율적이기 때문이다.
다른 플레이어들을 구분할 수 있는 방법이 생겼기 때문에 피격 이벤트 구현은 매우 간단하다.
먼저 Bullet Prefab의 Collider에 isTrigger 옵션을 체크해준다.
OnTriggerEnter2D 함수는 충돌 이벤트가 발생했을 때 두 물체 중 어느 하나가 Trigger 옵션을 달고 있어야지만 호출된다.
private void OnTriggerEnter2D(Collider2D collider)
{
if (collider.transform.CompareTag("Projectile"))
Debug.Log(collider.transform.GetComponent<Bullet>().Shooter.GetComponent<PlayerTransform>().Id.Value);
}
보이는 것처럼, Projectile 태그를 달고 있을 경우에, 해당 투사체를 발사한 플레이어의 id를 불러올 수 있다.
public void OnTriggerEnter2D(Collider2D col) {
if (col.transform != _shooter && col.transform.CompareTag("Player"))
DestroyProjectile();
}
이렇게 Bullet 클래스에서는 발사자 자기 자신을 제외한 나머지 플레이어 오브젝트와 충돌하였을 때 소멸하는 코드를 작성했다.