유니티 RPG - 60. 멀티플레이모드 구현 - 중립오브젝트 및 공격판정
Intro
이번 포스팅에서는 두 플레이어가 협동하여 처치할 몬스터를 소환하고, 플레이어가 무기를 장착할 수 있도록 하는것을 추가하였습니다.
-
몬스터 게임오브젝트 생성(네트워크 동기화)
-
플레이어 무기생성 및 장착
-
공격판정
중립오브젝트 생성
멀티플레이 환경에서 공유될 오브젝트를 생성할 때 주의할점
- 마스터클라이언트가 중립 오브젝트를 생성하여 네트워크 동기화
- PhotonNetwork.Instantiate() 로 오브젝트 생성
- 생성할 오브젝트는 반드시 Resources 폴더 아래에 존재
-
동기화될 오브젝트는 PhotonView 컴포넌트 필수
- 체력이 깎이는등의 메서드는 RPC 호출
몬스터 오브젝트에 PhotonView, PhotonTransformView, PhotonAnimatorView를 추가합니다.
-
PhotonTransformView : 위치, 회전, 스케일 동기화를 위해 필요
-
PhotonAnimatorView : 애니메이션 동기화를 위해 필요
- Discrete : 파라미터 값이 바뀔때 1회 전송. Bool 파라미터에 사용
- Continuous : 프레임마다 값 전송. int, float 등에 사용
- Trigger 파라미터 : Disable 후 RPC 호출로 조작하는것이 안전
몬스터의 소환은 던전매니저가 수행합니다.
// 몬스터 소환
private void SpawnMonster()
{
// 마스터 클라이언트만
if (!PhotonNetwork.IsMasterClient) return;
for(int i = 0;i<monsterSpawnPositions.Length;i++)
{
PhotonNetwork.Instantiate("Monsters/TurtleShell_Multi",
monsterSpawnPositions[i].position, monsterSpawnPositions[i].rotation);
}
}
몬스터 로직 수정
현재 몬스터의 구조는 다음과같습니다
- Monster : 몬스터 공통 부분
- TurtleShell : 몬스터 개별 부분
- FSM 객체 생성, 상태전환 조건처리
- TurtleShell : 몬스터 개별 부분
- FSM : 몬스터의 상태 관리
- IDLE, CHASE, ATTACk, DIE STATE 존재
문제는 ATTACK, DIE 상태클래스에서 직접 애니메이션 트리거호출을 하고있으므로,
애니메이션을 네트워크 동기화하기위해 RPC 메서드를 만들어 이 메서드를 호출하도록 변경하였습니다.
Monster 클래스에서 수정될 부분은 다음과같습니다.
-
Target Player 동적 할당
-
애니메이션 트리거 호출함수 추가
-
메서드에 RPC 어트리뷰트 추가
// - Monster 클래스
// 타깃플레이어할당 - 가장 가까운 플레이어
protected void FindClosestPlayer()
{
float minDistance = float.MaxValue;
PlayerController closest = null;
foreach(var player in FindObjectsOfType<PlayerController>())
{
float distance = Vector3.Distance(transform.position, player.transform.position);
if(distance < minDistance)
{
minDistance = distance;
closest = player;
}
}
if (closest != null)
targetPlayer = closest;
}
// 공격받음
[PunRPC]
public void GetDamaged(float damage)
{
float minDamage = damage * 0.8f;
float maxDamage = damage * 1.2f;
int randomDamage = (int)Random.Range(minDamage, maxDamage);
DamageTextManager.Instance.ShowDamage(damageTextPos, randomDamage);
curHp -= randomDamage;
}
// Attack 애니메이션 트리거 RPC 호출
[PunRPC]
public void RPC_TriggerAttackAnim()
{
TriggerAttackAnim();
}
public void TriggerAttackAnim()
{
anim.SetTrigger("Attack");
}
// Die 애니메이션 트리거 RPC 호출
[PunRPC]
public void RPC_TriggerDieAnim()
{
TriggerDieAnim();
}
public void TriggerDieAnim()
{
anim.SetTrigger("Die");
}
// 공격판정
public virtual void Attack()
{
}
각 상태클래스의 애니메이션 트리거 호출하는 부분을 아래와같이 변경
// - AttackState
// 공격준비가 되었을 때
if(monster.isAttackReady)
{
if(GameManager.Instance.isMultiPlaying)
{
monster.photonView.RPC(nameof(monster.RPC_TriggerAttackAnim), Photon.Pun.RpcTarget.All);
}
else
{
monster.TriggerAttackAnim();
}
// ...
}
플레이어 무기 생성
플레이어의 무기는 WeaponManager 클래스의 SetWeapon() 호출을 통해 생성됩니다.
문제는 무기가 생성될 위치(weaponTransform)를 미리 할당해놓는 방식이었습니다.
따라서, 플레이어 오브젝트가 새롭게 생성될 때 동적으로 할당하도록 하였습니다.
또한 멀티상에서도 플레이어 무기를 세팅하는 로직을 작성하였습니다.
// - WeaponManager
// 현재 무기 세팅(기본값 Punch)
public void SetWeapon(string type, string weapon)
{
if(Enum.TryParse(type, out WeaponType result))
{
// 싱글플레이
if(!GameManager.Instance.isMultiPlaying)
{
// ...
}
// 멀티플레이
else if(GameManager.Instance.isMultiPlaying)
{
currentWeapon = null;
Destroy(myWeaponGo);
weaponPoint = GameManager.Instance.player.WeaponPoint;
// 무기 위치를 참조하기위해 참조 오브젝트 생성
GameObject weaponRef = Resources.Load<GameObject>("Weapons/" + weapon);
Vector3 refLocalPos = weaponRef.transform.localPosition;
Quaternion refLocalRot = weaponRef.transform.localRotation;
// 무기 생성
GameObject newWeapon = PhotonNetwork.Instantiate("Weapons/" + weapon, weaponPoint.position, weaponPoint.rotation);
// 무기를 플레이어 자식으로 및 위치 지정
newWeapon.transform.SetParent(weaponPoint);
newWeapon.transform.localPosition = refLocalPos;
newWeapon.transform.localRotation = refLocalRot;
myWeaponGo = newWeapon;
currentWeapon = myWeaponGo.GetComponent<Weapon>();
myWeaponType = result;
CurWeaponType = (int)myWeaponType;
}
}
else
{
Debug.Log($"{type} 은(는) 유효한 타입이 아닙니다.");
}
}
[PunRPC]
public void RPC_SetWeapon(string type, string weapon)
{
SetWeapon(type, weapon);
}
public void RequestSetWeapon(string type = "None", string weapon = "Punch")
{
if(GameManager.Instance.isMultiPlaying)
{
photonView.RPC("RPC_SetWeapon", RpcTarget.AllBuffered, type, weapon);
}
else
{
SetWeapon(type, weapon);
}
}
-
기존에 SetWeapon()를 호출하고있던 클래스들은 이제 RequestSetWeapon()를 호출하게됩니다.
-
RequestSetWeapon()는 멀티여부를 확인하고, SetWeapon()를 호출합니다.
// - PlayerController
public Transform WeaponPoint { get; private set; } // 무기 생성 위치
private void Init()
{
// ...
WeaponPoint = GetComponentInChildren<WeaponPointMarker>().gameObject.transform;
}
- WeaponPointMarker는 마킹용 빈 스크립트입니다.
전투관련
플레이어가 몬스터를 공격할 때 몬스터의 피격함수를 RPC 호출합니다.
// - Punch, Sword, Staff..
// Raycast된 결과를 순회
foreach (RaycastHit hit in hits)
{
// 1. Monster 경우
if (hit.collider.CompareTag("Monster"))
{
Monster monster = hit.collider.GetComponent<Monster>();
if (monster != null)
{
if(GameManager.Instance.isMultiPlaying)
{
monster.photonView.RPC(nameof(monster.GetDamaged), Photon.Pun.RpcTarget.All,
DataManager.Instance.GetPlayerData().Damage * Random.Range(0.8f, 1f));
}
else
{
monster.GetDamaged(DataManager.Instance.GetPlayerData().Damage * Random.Range(0.8f, 1f));
}
}
// ...
else if()
}
}
반대로 몬스터가 플레이어를 공격할때입니다.
// - TurtleShell
// 공격판정
public override void Attack()
{
if (GameManager.Instance.isMultiPlaying && !PhotonNetwork.IsMasterClient)
return;
// Raycast할 위치, 방향
Vector3 origin = transform.position + new Vector3(0, 0.5f, 0);
Vector3 direction = transform.forward;
if (Physics.SphereCast(origin, 0.5f, direction, out RaycastHit hit, 1f, LayerMask.GetMask("Player")))
{
if (hit.collider.CompareTag("Player"))
{
// 멀티플레이
if (GameManager.Instance.isMultiPlaying)
{
// 플레이어 데미지
hit.collider.GetComponent<PhotonView>()?.RPC("GetDamaged", RpcTarget.All, damage);
}
// 싱글플레이
else
{
PlayerData playerData = DataManager.Instance.GetPlayerData();
playerData.GetDamaged(damage);
}
}
}
}
댓글남기기