3 분 소요

Intro

이번 포스팅에서는 두 플레이어가 협동하여 처치할 몬스터를 소환하고, 플레이어가 무기를 장착할 수 있도록 하는것을 추가하였습니다.

  • 몬스터 게임오브젝트 생성(네트워크 동기화)

  • 플레이어 무기생성 및 장착

  • 공격판정

image

중립오브젝트 생성

멀티플레이 환경에서 공유될 오브젝트를 생성할 때 주의할점

  • 마스터클라이언트가 중립 오브젝트를 생성하여 네트워크 동기화
    • PhotonNetwork.Instantiate() 로 오브젝트 생성
    • 생성할 오브젝트는 반드시 Resources 폴더 아래에 존재
  • 동기화될 오브젝트는 PhotonView 컴포넌트 필수

  • 체력이 깎이는등의 메서드는 RPC 호출

몬스터 오브젝트에 PhotonView, PhotonTransformView, PhotonAnimatorView를 추가합니다.

image

  • 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 객체 생성, 상태전환 조건처리
  • 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);
                }
            }
        }
    }

댓글남기기