PUN Classic (v1), PUN 2, Bolt는 휴업 모드입니다. Unity2022에 대해서는 PUN 2에서 서포트하지만, 신기능의 추가는 없습니다. 현재 이용중인 고객님의 PUN 및 Bolt 프로젝트는 중단되지 않고, 퍼포먼스나 성능이 떨어지는 일도 없습니다. 앞으로의 새로운 프로젝트에는 Photon Fusion 또는 Quantum을 사용해 주십시오.

클릭하여 이동하기

이 샘플에서는 플레이어가 이동할 대상 위치를 클릭하여 이동하는 포인트 앤 클릭(Point-And-Click) 방법을 사용하여 캐릭터를 제어할 수 있는 탑-다운 게임을 만드는 방법을 보여 줍니다. 프로젝트에 Bolt가 설치되어 구성되어 있지 않은 경우, 시작하기 튜토리얼을 먼저 따라하십시오.

우리는 이 샘플에서 유니티의 표준 에셋ThirdPersonCharacter를 기반으로 사용할 것입니다. 이것은 이미 애니메이션으로 잘 제작되었고 네비게이션 시스템을 통해 제어할 수 있는 것 입니다. 이 튜토리얼은 (i)레벨 생성, (ii)캐릭터 설정 및 (iii)캐릭터를 관리하기 위한 Bolt 구성으로 나누어져 있습니다.

이 샘플의 최종 결과는 Bolt 샘플 리파지토리에서 확인할 수 있습니다.

레벨 생성

우리의 목표는 아래 그림과 비슷한 레벨을 만드는 것입니다. 캐릭터들이 스폰할 수 있는 장애물과 출발점이 있어야 합니다. 원하는 곳에 객체들을 배치할 수 있습니다. 창의적이기만 하면 됩니다.

Click to Move Level
클릭하여 이동하기 레벨.

다음은 레벨 생성에 사용되는 주요 단계입니다:

  1. 땅으로 사용될 플레인이 있어야 합니다.
  2. 땅위에 몇 개의 상자들을 놓으십시오.
  3. 플레이어로부터 이벤트를 받기 위해 EventSystem 객체를 추가하십시오.
  4. 씬 주변에 몇 개의 스폰 포인트를 추가하십시오:
    1. 포인트를 주기 위한 빈 객체를 생성합니다.
    2. 홀더에 또 하나의 빈 객체를 생성하여 Spawn 으로 이름을 부여하고 TagRespawn으로 설정합니다.
    3. 이제 이 객체를 복제하여 여러곳에 골고루 놓아주십시오.
    4. 씬에서 더 잘 보이기하기 위해 스폰 포인트에 아이콘 지정을 할 수 있습니다.
  5. 땅과 장애물에 일부 머터리얼을 추가하여 경기를 진행할 때 더 시각적인 효과를 볼 수 있습니다.
Click to Move Level
클릭하여 이동하기 레벨.

캐릭터가 가야할 목표 장소를 갱신하기 위해 GameObject를 설정할 것 입니다:

  1. GameObject를 생성하고 TargetPointer로 이름을 부여합니다.
  2. Player에게 보일 수 있도록, 이 객체의 자식에 Sphere 를 추가하여 투명한 Material을 생성합니다. 이것은 시각적인 참조로 동작을 하게 될 것 입니다.
  3. 새로운 스크립트를 생성하여 PlaceTarget로 이름을 부여하고 TargetPointer 객체에 추가합니다. 목표 위치를 제어하기 위해 이 스크립트를 사용할 것 입니다.
TargetPointer GameObject
`TargetPointer` GameObject.

다음은 PlaceTarget 스크립트입니다. 이 코드는 간단한데, Update 루프마다 플레이어의 Click 입력을 확인하고, 이 위치에서 레이를 추적하며, 히트 정보와 함께 TargetPointer의 위치를 변경하고 UpdateTarget 이벤트를 발생시킵니다. (나중에 플레이어가 구독하게 됩니다)

C#

public class PlaceTarget : MonoBehaviour
{
    public float surfaceOffset = 0.2f;
    public event Action<Transform> UpdateTarget;

    Vector3 lastPosition = Vector3.zero;

    private void Update()
    {
        if (!Input.GetMouseButtonDown(0))
        {
            return;
        }

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (!Physics.Raycast(ray, out hit))
        {
            return;
        }

        transform.position = hit.point + hit.normal * surfaceOffset;

        if (lastPosition != transform.position)
        {
            lastPosition = transform.position;

            if (UpdateTarget != null)
            {
                UpdateTarget(transform);
            }
        }
    }
}

스크립트가 잘 작성되었다면 게임을 실행시킨 후 객체를 클릭하면 TargetPointer 클릭한 노멀 벡트의 올바른 위치로 이동해야 할 것 입니다.

이제 캐릭터에게 네비게이션 메쉬를 생성하기 위해 네비게이션 시스템을 설정해야 합니다. 하는 것은 정말 간단합니다:

  1. Window/Navigation 탭의 Navigation을 오픈합니다. 유니티의 네비게이션에 익숙하지 않다면, 이 링크를 확인하십시오.
  2. 네비게이션 영역을 구성합니다:
    1. Object의 최근 탭으로 이동하면 캐릭터가 걸어서 갈수 있는 곳과 없는 곳을 구성할 것 입니다.
    2. Ground 객체를 선택하여 Navigation Static으로 체크하고 Walkable로 설정합니다.
    3. 씬의 모든 장애물을 선택하고 Navigation Static으로 체크하고 Not Walkable로 설정합니다.
  3. 모든 객체들이 구성되었다면 NavMesh를 생성해야합니다. Bake 텝으로 이동하고 Bake 버튼을 클릭합니다. 이렇게 하면 폴더가 생성되고 씬 위치 바로 옆에 NavMesh 파일이 생성될 것 입니다.

이러한 단계를 수행한 후에는 아래 표시된 것과 유사한 NavMesh가 있어야 합니다. 파란색 영역은 모두 Walkable으로, 씬의 장애물에 의해 메쉬가 어떻게 제한되는지를 주목하면, 우리의 캐릭터가 경로를 계산하면서 이러한 영역을 피할 수 있게 됩니다.

Level NavMesh
레벨 *NavMesh*.

이제 레벨이 플레이 할 준비가 되었습니다. 원하시면 보행 가능 구역이나 장애물을 조금 더 조정할 수 있지만, 변경 후에는 반드시 리베이크를 해야합니다(Navigation 윈도우의 Bake 버튼을 다시 클릭하는 것을 잊지 마세요).

캐릭터 설정

이 샘플에서는 ThirdPersonCharacter를 사용하고 있으며 특별히 AIThirdPersonController 버전(표준 에셋에서 다운로드 가능)을 메인 캐릭터로서 사용하고 있습니다. 이 캐릭터는 선글라스😎를 낀 멋진 남자 Ethan 이라는 모델에서 만들어졌습니다. 자 그럼 패키지를 다운로드하여 유니티 프로젝트로 가져옵니다.

다음과 같이 AIThirdPersonController 프리팹을 찾을 수 있는 유사한 구조가 되어야 합니다. 이 프리팹을 복사하여 다른 폴더로 옮기십시오. 원하시는 대로 폴더의 이름을 부여하세요. 여기서는 EthanClickToMove를 사용할 것입니다. 원래 개체의 일부 구성 요소를 변경할 예정이므로 항상 백업을 수행하는 것이 좋습니다.

Game Character
게임 캐릭터.

먼저, TargetPointer의 변경 사항을 리슨하고 플레이어를 해당 위치로 보내는 간단한 스크립트를 작성합니다. 이 스크립트는 캐릭터에 부착되어야 합니다. Start() 메소드에서 TargetPointer 개체를 찾고, PlaceTarget 컴포넌트에 대한 레퍼런스를 얻은 후 UpdateTarget 이벤트를 구독합니다. 이벤트가 실행되면 SetTarget 콜을 리슨하는 게임오브젝트의 다른 스크립트에 메시지를 보냅니다. 이 경우 AICharacterControl 스크립트가 이 데이터를 수신하고 그에 따라 동작합니다.

C#

public class ClickToMoveController : MonoBehaviour
{
    void Start()
    {
        var placeTarget = GameObject.Find("TargetPointer").GetComponent<PlaceTarget>();

        placeTarget.UpdateTarget += (newTarget) =>
        {
            gameObject.SendMessage("SetTarget", newTarget);
        };
    }
}

만약 캐릭터 프리팹 사본을 씬에 넣고 게임을 실행한다면, 아래 그림과 같이 주변을 클릭해서 플레이어를 그 위치로 이동시킬 수 있을 것입니다. 이제 다음 단계로 넘어갈 준비가 되었습니다. Bolt를 게임에 통합하여 멀티플레이어로 만들 수 있습니다.

Game Character running by Click
Game Character running by Click.

Bolt 통합

캐릭터가 걷고 목표 지점을 향해 따라가는 게임이 이제 동작을하므로, 프로젝트에 Bolt 통합 시작을 할 준비가 되었습니다. 전에 말했던것처럼 프로젝트에 Bolt가 아직 설정되지 않았다면 먼저 시작하기 튜토리얼을 따라 해 주십시오.

에셋 설정

Bolt에서는 모든 엔티티들이 States를 사용하여 데이터를 동기화합니다. 이 예제도 다르지 않습니다. Window/Bolt/Assets으로 이동하여 State를 생성하고 ClickToMoveState로 이름을 부여하여 다음 단계를 따라하세요:

  1. 새로운 속성 Transform을 생성하고, 타입을 Transform로 설정합니다.
  2. 네트워크를 통해 동기화를 하기 위해 플레이어가 사용하는 애니메이션으로 부터 속성을 임포트해야합니다.
    1. Import Mecanim Parameters에서 ThirdPersonAnimatorController 에셋을 선택합니다.
    2. Import 버튼을 클릭합니다.
    3. 모든 파라미터들이 올바르게 임포트되어야 합니다.

아래에 보인 것과 유사하게 구성된 State가 있어야 합니다.

Bolt State Creation
Bolt State 생성.

클라이언트에서 서버로 데이터를 전송할 수 있는 방법도 필요하며, 이는 BoltCommands를 사용하여 이루어집니다. 커맨드는 플레이어에서 캐릭터를 제어하는 입력을 나타내지만, 서버에서는 클라이언트에 변경 사항을 보내 게임의 무결성을 유지하는 데 사용됩니다.

방금 ClickToMoveState를 생성한 동일한 윈도우에서 우측 클릭을하고 새로운 Command를 생성하여 ClickToMoveCommand라고 이름을 부여해줍니다.

  1. 새로운 Input을 생성하고 click으로 이름을 부여하여 타입을 Vector로 설정합니다. 이것은 플레이어가 설정한 목표 위치를 전송하는데 사용될 것 입니다.
  2. 새로운 Result를 생성하여 position라고 이름을 부여하고 타입을 Vector로 설정합니다. 이것은 Server상에 캐릭터가 어디있는지를 설정하게 될 것 입니다.
Bolt State on Character
Bolt State on Character.

이제 Bolt Assets를 닫을 수 있고 Assets/Bolt/Compile Assembly의 커맨드를 실행하여 Bolt를 컴파일 할 수 있습니다. 콘솔BoltCompiler: Success!메시지가 나타났으면 계속할 준비가 되었다는 것 입니다.

Bolt를 사용하여 플레이어를 복제하려면 Bolt Entity 컴포넌트를 추가하고 최근에 만든 State를 사용하도록 구성해야 합니다. 다음에 표시된 그림과 같습니다.

  1. EthanClickToMove 프리팹을 선택합니다.
  2. 새로운 컴포넌트인 Bolt Entity 컴포넌트를 추가합니다.
  3. State 드롭다운 메뉴에서 IClickToMoveState를 선택합니다.
  4. 라이브러리가 이 엔티티(Id에 의해 시그널된)를 알리기 위해 Bolt (Assets/Bolt/Compile Assembly에서)를 컴파일합니다..
Bolt State on Character
Character의 Bolt State.

네트워크 스크립트

이번 세션에서는 일부 스크립트를 추가하여 Bolt가 에셋을 인식하고 플레이어를 생성하고 필요한 모든 데이터를 동기화하도록 할 것입니다.

우선, 네트워크 게임 관리자 스크립트를 만들 것입니다. 이 스크립트는 서버와 클라이언트에서 게임 프리팹를 인스턴스화하는 역할을 할 것입니다. 아래와 같이 새 스크립트 ClickToMoveNetworkCallbacks를 작성합니다.

C#

[BoltGlobalBehaviour(BoltNetworkModes.Server, "ClickToMoveGameScene")]
public class ClickToMoveNetworkCallbacks : Bolt.GlobalEventListener
{
    public override void SceneLoadLocalDone(string scene)
    {
        var player = InstantiateEntity();
        player.TakeControl();
    }

    public override void SceneLoadRemoteDone(BoltConnection connection)
    {
        var player = InstantiateEntity();
        player.AssignControl(connection);
    }

    private BoltEntity InstantiateEntity()
    {
        GameObject[] respawnPoints = GameObject.FindGameObjectsWithTag("Respawn");

        var respawn = respawnPoints[Random.Range(0, respawnPoints.Length)];
        return BoltNetwork.Instantiate(BoltPrefabs.EthanClickToMove, respawn.transform.position, Quaternion.identity);
    }
}

이 코드에 대한 설명:

  • 서버에서만 실행되며, 게임 씬이 로드되면 ClickToMoveGameScene(이 경우 해당 씬의 이름과 일치하도록 변경)으로 변경됩니다
  • SceneLoadLocalDoneSceneLoadRemoteDone 콜백을 이용하며, 둘다 게임 씬이 로딩이 완료되었을 때 호출됩니다. 이 순간을 사용하여 임의의 위치에서 EthanClickToMove 프리팹 사본을 인스턴스화합니다.(이전에 생성된 Respawn 포인트를 이용)
  • 이 작업은 게임 호스트(서버)에서만 실행되므로 씬이 로컬로 로드되면 플레이어가 엔티티의 (player.TakeControl())를 제어하고 씬이 클라이언트에 로드되면 호스트는 해당 원격 플레이어에게 제어 권한을 부여(player.AssignControl(connection))합니다.

이제 Bolt를 사용하여 명령을 전송하도록 컨트롤러 스크립트를 업데이트해야 합니다. ClickToMoveController 스크립트를 열고 몇 가지 내용을 변경합니다.

  1. Bolt에서 이벤트 콜백을 받으려면 EntityEventListener를 확장해야 합니다. 이렇게 하면 라이브러리에서 여러 유틸리티에 접근할 수 있습니다. 지네릭 아규먼트로서 IClickToMoveState 를 포함한 것을 확인해 보세요. 이거은 특정 State의 데이터만 취급한다는 것을 보장해 줍니다.

C#

public class ClickToMoveController : Bolt.EntityEventListener<IClickToMoveState>
{
    // ...
}
  1. Attached() 콜백은 캐릭터 데이터를 Bolt에 붙일 수 있는 링크를 제공해줍니다. 이 경우에는 Transform 그리고 Animator 정보를 동기화할 것 입니다.

C#

// ...
public override void Attached()
{
    state.SetTransforms(state.Transform, transform);
    state.SetAnimator(GetComponentInChildren<Animator>());
}
// ...
  1. 나중에 Bolt Entity에 대한 제어권을 로컬 플레이어에 부여하여 명령을 전송할 수 있도록 할 것입니다. 이를 활용하기 위해 다음 3가지 콜백을 사용합니다.
  • ControlGained 메소드는 플레이어가 엔티티를 제어할 때 실행됩니다. 로컬 TargetPointer를 찾고 이전처럼 UpdateTarget 콜백에 연결하지만, 여기서는 나중에 사용할 타겟 트랜스폼을 저장합니다.

C#

// ...
public Transform destination;

public override void ControlGained()
{
    var placeTarget = GameObject.Find("TargetPointer").GetComponent<PlaceTarget>();

    placeTarget.UpdateTarget += (newTarget) =>
    {
        destination = newTarget;
    };
}
// ...
  • SimulateController 콜백에서만 Server로 새로운 명령을 전송할 수 있습니다. 여기서는 새로운 ClickToMoveCommand를 작성하여 목적지(이 경우 click 파라미터)를 설정하고 entity.QueueInput를 호출하여 큐에 입력합니다. 커맨드에는 위치 벡터만 전송한다는 점에 주목해주세요.

C#

// ...
public override void SimulateController()
{
    if (destination != null)
    {
        IClickToMoveCommandInput input = ClickToMoveCommand.Create();
        input.click = destination.position;
        entity.QueueInput(input);
    }
}
// ...
  • ExecuteCommand에서는 모든 마법이 일어납니다. 이 콜백에서는 플레이어가 보낸 모든 명령을 처리하고 클라이언트와 서버 모두 명령을 실행하여 click 파라미터를 읽은 다음 메시지를 통해 AICharacterControl로 전송합니다. 그 외에도, Server 는 모든 플레이어들에게 동일한 위치에 있도록 클라이언트에게 수정 사항을 다시 보낼 것입니다.

C#

// ...
public override void ExecuteCommand(Command command, bool resetState)
{
    ClickToMoveCommand cmd = (ClickToMoveCommand)command;

    if (resetState)
    {
        // owner has sent a correction to the controller
        transform.position = cmd.Result.position;
    }
    else
    {
        if (cmd.Input.click != Vector3.zero)
        {
            gameObject.SendMessage("SetTarget", cmd.Input.click);
        }

        cmd.Result.position = transform.position;
    }
}
// ...

이러한 모든 변경 사항이 적용됨에 따라 게임을 실행하고 플레이어를 씬에서 실행할 준비가 거의 완료되었습니다. 다음은 ClickToMoveController의 전체 스크립트입니다.

C#

public class ClickToMoveController : Bolt.EntityEventListener<IClickToMoveState>
{
    public Transform destination;

    public override void Attached()
    {
        state.SetTransforms(state.Transform, transform);
        state.SetAnimator(GetComponentInChildren<Animator>());
    }

    public override void ControlGained()
    {
        var placeTarget = GameObject.Find("TargetPointer").GetComponent<PlaceTarget>();

        placeTarget.UpdateTarget += (newTarget) =>
        {
            destination = newTarget;
        };
    }

    public override void SimulateController()
    {
        if (destination != null)
        {
            IClickToMoveCommandInput input = ClickToMoveCommand.Create();
            input.click = destination.position;
            entity.QueueInput(input);
        }
    }

    public override void ExecuteCommand(Command command, bool resetState)
    {
        ClickToMoveCommand cmd = (ClickToMoveCommand)command;

        if (resetState)
        {
            //owner has sent a correction to the controller
            transform.position = cmd.Result.position;
        }
        else
        {
            if (cmd.Input.click != Vector3.zero)
            {
                gameObject.SendMessage("SetTarget", cmd.Input.click);
            }

            cmd.Result.position = transform.position;
        }
    }
}

수정해야 할 것이 하나 더 있습니다. AICharacterControl.SetTarget 메소드에서 아규먼트로 Vector3가 아닌 Transform을 수신한다는 것을 알아채셨는지요. 현재 Bolt는 하나의 파라미터로 transform을 완전히 보내도로 지원 하지는 않지만(그러나 위치와 회전을 두 개의 Vector3(Vector3) 변수로 보내면 됩니다) 목표 위치에만 관심이 있기 때문에 문제가 되지 않습니다. AICharacterControl 스크립트를 열고 아래와 같이 수정합니다.

C#

public class AICharacterControl : MonoBehaviour
{
    // Add this new field
    public Vector3 targetPosition;

    // ...

    // Modify the Update method
    private void Update()
    {
        if (target != null)
            agent.SetDestination(target.position);

        // Here we pass our new Vector3 target
        if (targetPosition != Vector3.zero)
            agent.SetDestination(targetPosition);

        if (agent.remainingDistance > agent.stoppingDistance)
            character.Move(agent.desiredVelocity, false, false);
        else
            character.Move(Vector3.zero, false, false);
    }

    // ...

    // Add this new overload
    public void SetTarget(Vector3 target)
    {
        this.targetPosition = target;
    }
}

게임 실행

좋습니다! 이 샘플이 작동하도록 하기 위한 모든 단계가 끝났습니다. 이제 샘플이 작동하는 것을 볼 차례입니다. 이를 실행하는 가장 간단한 방법은 Bolt Debug 유틸리티를 사용하는 것입니다. 먼저 게임 씬을 Build Settings/Scenes In Build에 추가하고 Bolt (Assets/Bolt/Compile Assembly)를 컴파일하여 해당 씬을 인식시켜 주어야 합니다.

이렇게 하면 Window/Bolt/Scenes에서 Bolt Debug 창을 열어 편집기를 Server로 설정하고 클라이언트 수를 구성한 다음 Debug Start를 클릭합니다. 잠시 후 클라이언트가 자동으로 시작되고 Unity Editor가 게임 호스트로 실행됩니다. 잘했습니다!

Debug Start the Game
Debug Start the Game.

Have fun !

Game Running
Game Running.

축하합니다. 튜토리얼을 끝냈습니다!

Back to top