Bolt 튜토리얼 - 챕터 3

<< 이전 챕터

이 챕터에서는 엔티티들의 제어 추적과 세계를 돌아다니게 하는 것을 다룰 것 입니다. 이를 통해 Bolt에서 어떻게 제어 하는지에 대한 개념와 신뢰성 있는 움직임 처리 방법에 대해서 익숙해질 것 입니다.

서버와 클라이언트의 차이점 없애기

엔티티 제어 추적을 하기전에 앞서 우리는 Bolt와 일반적인 멀티플레이어 게임에서 많이 부딪히게 되는 것에 대해서 설명하고 다룰 것 입니다.

문제: 서버를 게임내에서 다른 플레이어와 같이 만들기를 원한다면, 연결 자체에는 서버가 존재하지 않는 다는 사실을 어떻게 다룰까요?

서버에 연결하는 각 클라이언트는 BoltConnection 객체로 나타내어지며 각 클라이언트에서 서버는 하나의 BoltConnection 객체로 표현됩니다. 서버자체에서 "서버 플레이어"에게 무엇인가 하길 원할 때, 서버를 쉽게 참조할 방법이 없습니다. 왜냐하면 서버 자체를 나타내는 객체가 없기 때문입니다.

이에 대한 답은 간단한 추상화를 생성하는 것으로 특정 연결대신에 Player 객체로 다루어지는 것을 생성할 필요가 있습니다. 이 플레이어 객체내에서 연결이 되었는지에 관계없이 감출것이므로 나머지 코딩에서는 이에 대해 전혀 고려할 필요가 없습니다.

TutorialPlayerObject.csTutorialPlayerObjectRegistry.cs 두 개의 C# 파일을 생성합니다. TutorialPlayerObject 클래스 부터 시작해봅니다.

public class TutorialPlayerObject {
  public BoltEntity character;
  public BoltConnection connection;
}

이 코드는 표준 C# 클래스입니다. 유니티의 MonoBehaviour 클래스에서 상속받지 않았습니다. 이 사항은 매우 중요합니다. characterconnection 두 개의 필드가 있습니다. character 필드에는 세계에서 플레이어의 캐릭터를 나타내는 인스턴스화한 객체를 포함할 것 입니다. connection 필드는 이 플레이어에게 있는 경우 연결을 포함합니다. 이 필드는 서버 플레이어 개체에 대해 서버에서는 null 이 될 것 입니다.

두 개의 속성을 추가할 것이고 이를 통해 connection 필드를 직접 다루지 않고 클라이언트인지 서버 플레이어인지 체크할 수 있습니다.

public class TutorialPlayerObject {
  public BoltEntity character;
  public BoltConnection connection;

  public bool isServer {
    get { return connection == null; }
  }

  public bool isClient {
    get { return connection != null; }
  }
}

isServerisClient connection이 null 여부를 간단하게 체크하는 것으로, 플레이어가 서버인지 클라이언트인지를 나타내주는 것 입니다. TutorialPlayerObject 에 기능을 더 추가하기 전에 TutorialPlayerObjectRegistry 클래스를 열겠습니다. 이 클래스는 TutorialPlayerObject 클래스 인스턴스를 관리하는데 사용되는 클래스입니다.

전체 클래스 코드중에서 표준 C# 이 아닌 코드는 BoltConnection 클래스에서 userToken 에 접근하는 곳 입니다. 이 속성은 단순히 연결과 쌍을 이루고 싶길 원하는 다른 유형의 객체/데이터를 가질 수 있는 곳입니다. 이 경우 우리가 만든 TutorialPlayerObject 와 그것이 속한 연결(만약 그것이 속한 경우)을 쌍으로 만들 것 입니다.

이 클래스의 나머지 부분은 Bolt에 관련된 아주 적은 부분입니다. 코드와 주석을 쭉 읽어보셔도 되지만 이에 대한 상세한 설명을 하지는 않을 것 입니다.

using System.Collections.Generic;
using System.Linq;

public static class TutorialPlayerObjectRegistry {
  // keeps a list of all the players
  static List<TutorialPlayerObject> players = new List<TutorialPlayerObject>();

  // create a player for a connection
  // note: connection can be null
  static TutorialPlayerObject CreatePlayer(BoltConnection connection) {
    TutorialPlayerObject p;

    // create a new player object, assign the connection property
    // of the object to the connection was passed in
    p = new TutorialPlayerObject();
    p.connection = connection;

    // if we have a connection, assign this player 
    // as the user data for the connection so that we
    // always have an easy way to get the player object 
    // for a connection
    if (p.connection != null) {
      p.connection.UserData = p;
    }

    // add to list of all players
    players.Add(p);

    return p;
  }

  // this simply returns the 'players' list cast to 
  // an IEnumerable<T> so that we hide the ability 
  // to modify the player list from the outside.
  public static IEnumerable<TutorialPlayerObject> allPlayers {
    get { return players; }
  }

  // finds the server player by checking the 
  // .isServer property for every player object.
  public static TutorialPlayerObject serverPlayer {
    get { return players.First(x => x.isServer); }
  }

  // utility function which creates a server player
  public static TutorialPlayerObject CreateServerPlayer() {
    return CreatePlayer(null);
  }

  // utility that creates a client player object.
  public static TutorialPlayerObject CreateClientPlayer(BoltConnection connection) {
    return CreatePlayer(connection);
  }

  // utility function which lets us pass in a 
  // BoltConnection object (even a null) and have 
  // it return the proper player object for it.
  public static TutorialPlayerObject GetTutorialPlayer(BoltConnection connection) {
    if (connection == null) {
      return serverPlayer;
    }

    return (TutorialPlayerObject)connection.UserData;
  }
}

TutorialServerCallbacks.cs 파일을 열고 두 개의 BoltNetwork.Instantiate 호출을 제거하여 클래스를 변경해주세요.

유니티의 Awake 함수를 구현하고 이 함수 내부에서 TutorialPlayerObjectRegistry.CreateServerPlayer 를 호출합니다. 이는 이 콜백 오브젝트가 활성화 될 때마다 서버 플레이어가 생성됩니다.

TutorialServerCallbacks 오버라이딩 메소드에서 Bolt.GlobalEventListener 로 부터 상속된 Connected 메소드를 오버라이드 합니다.

Connected 메소드 안에서 TutorialPlayerObjectRegistry.CreateClientPlayer 을 호출하고 connection 아규먼트를 전달합니다.

using UnityEngine;

[BoltGlobalBehaviour(BoltNetworkModes.Server, "Level2")]
public class TutorialServerCallbacks : Bolt.GlobalEventListener {
  void Awake() {
    TutorialPlayerObjectRegistry.CreateServerPlayer();
  }

  public override void Connected(BoltConnection arg) {
    TutorialPlayerObjectRegistry.CreateClientPlayer(arg);
  }

  public override void SceneLoadLocalDone(string map) {
  }

  public override void SceneLoadRemoteDone(BoltConnection connection) {
  }
}

이제 각 플레이어들에 대해 캐릭터를 스폰하고 이 캐릭터를 올바르게 제어를 지정하는 것이 마지막으로 남았습니다. TutorialPlayerObject 클래스를 다시 열고 두 개의 새로운 메소드를 추가하겠습니다:SpawnRandomPosition. Spawn은 캐릭터를 스폰하고 RandomPosition 은 스폰할 위치를 무작위로 선택하는 메소드 입니다.

using UnityEngine;
using System.Collections.Generic;

public class TutorialPlayerObject {
  public BoltEntity character;
  public BoltConnection connection;

  public bool isServer {
    get { return connection == null; }
  }

  public bool isClient {
    get { return connection != null; }
  }

  public void Spawn() {
    if (!character) {
      character = BoltNetwork.Instantiate(BoltPrefabs.TutorialPlayer);

      if (isServer) {
        character.TakeControl();
      } else {
        character.AssignControl(connection);
      }
    }

    // teleport entity to a random spawn position
    character.transform.position = RandomPosition();
  }

  Vector3 RandomPosition() {
    float x = Random.Range(-32f, +32f);
    float z = Random.Range(-32f, +32f);
    return new Vector3(x, 32f, z);
  }
}

RandomPosition 는 세계에서 무작위 벡터(위치)를 간단히 되돌려주는 것으로 설명하지는 않을 것이지만, Spawn 함수에 대해서는 좀 자세히 살펴 보도록 하겠습니다. Spawn 에서는 캐릭터가 있는지를 확인하고 * 없으면* BoltNetwork.Instantiate 를 호출하여 생성합니다. 그리고 나서 서버인지를 체크하여 제어를 줄지 받을지에 대한 적장한 메소드를 호출합니다.

플레이어를 움직이는 캐릭터 객체의 transform.position 속성을 세계에서 무작위 위치로 설정했습니다.

게임을 시작하기 전에, 처리할 두 가지가 더 있습니다. 첫 번째는 tutorial/Scripts/Callbacks 에 있는 TutorialPlayerCallbacks 클래스를 엽니다. 인티티의 제어를 얻을 때 알림을 주는 ControlOfEntityGained 콜백을 오버라이드 할 것 입니다.

using UnityEngine;

[BoltGlobalBehaviour("Level2")]
public class TutorialPlayerCallbacks : Bolt.GlobalEventListener {
  public override void SceneLoadLocalDone(string map) {
    // this just instantiates our player camera, 
    // the Instantiate() method is supplied by the BoltSingletonPrefab<T> class
    PlayerCamera.Instantiate();
  }

  public override void ControlOfEntityGained(BoltEntity arg) {
    // this tells the player camera to look at the entity we are controlling
    PlayerCamera.instance.SetTarget(arg);
  }
}

이제 마지막으로 해야할 것은 이 행동이 서버에서 존재하기 때문에 TutorialServerCallbacks 로 돌아가서 Spawn 씬 로딩이 완료되었을 때 Spawn 메소드를 호출합니다([BoltGlobalBehaviour(BoltNetworkModes.Server, "Level2")] 속성의 제공). 서버 자체의 SceneLoadLocalDone와 클라이언트의 SceneLoadRemoteDone 을 검토해야 합니다.

using UnityEngine;

[BoltGlobalBehaviour(BoltNetworkModes.Server, "Level2")]
public class TutorialServerCallbacks : Bolt.GlobalEventListener {
  void Awake() {
    TutorialPlayerObjectRegistry.CreateServerPlayer();
  }

  public override void Connected(BoltConnection arg) {
    TutorialPlayerObjectRegistry.CreateClientPlayer(arg);
  }

  public override void SceneLoadLocalDone(string map) {
    TutorialPlayerObjectRegistry.serverPlayer.Spawn();
  }

  public override void SceneLoadRemoteDone(BoltConnection connection) {
    TutorialPlayerObjectRegistry.GetTutorialPlayer(connection).Spawn();
  }
}

Bolt Scenes 윈도우로 가서 Play As Server 를 클릭하면 아래 그림과 같은 화면을 볼 수 있을 것 입니다.

캐릭터 스폰과 이 캐릭터에 제어를 할당했고 카메라가 캐릭터를 바라보고 있습니다. 다음은 캐릭터를 주변으로 움직이고 제어하는 것 입니다. 별도로 클라이언트를 빌드하여 에디터 내에서 시작하고 있는 서버로 연결할 수 있습니다. 이 클라이언트가 서버와 같이 캐릭터를 올바르게 스폰하고 캐릴터를 지정하는 것을 볼 수 있을 것 입니다.

노트: 카메라 코드 동작방식으로 인하여 캐릭터 주위로 회전시킬 수는 없습니다. 캐릭터가 움직이지 않는다면 완전하게 정적입니다. 일부러 이렇게 만들었습니다

Back To Top

이동

이 튜토리얼의 이 섹션에서는 많은 분들이 질문하시는 것에 대한 것을 다룹니다: 서버가 아직 제어중이고 검증하고 있는 움직임을 클라이언트에서 즉시 움직임을 예측하는 신뢰성 있는 움직임입니다. 이것은 이동 코드 관점에서 클라이언트서버 간의 차이점을 제거하여 완벽하게 투명성있도록 해줍니다.

새로운 Command 를 생성하는 것으로 시작하겠습니다. Bolt Assets 윈도우에서 우클릭하여 New Command를 선택합니다.

command를 클릭하여 선택하고 TutorialPlayerCommand 로 이름을 부여 합니다.

왼쪽 상단에서 New Property 버튼 상태 대신에 New InputNew Result 라고하는 두 개의 버튼을 볼 수 있을 것 입니다. 커맨드에 데이터 추가를 시작하기전에 InputResult 가 정확히 무엇인지에 대해서 상세히 알아보도록 하겠습니다.

Input 의 일반적인 의미는 하나의 플레이어로 부터 입력받는 인풋을 캡슐화 하는 것 입니다. 이동에 대한 "Forward""Backward" 또는 마우스 회전에 대한 "YRotation""XRotation" 과 같은 것 입니다. 하지만 "SelectedWeapon" 과 같은 훨씬 더 추상적인 것이 될 수 도 있습니다.

ResultInput 을 객체에 적용한 결과 상태를 캡슐화합니다. 여기에 있는 일반적인 속성들에는 positionvelocity 에 대한 속성들의 값일 뿐 만 아니라 isGrounded 와 같은 상태의 다른 유형의 플래그도 될 수 있습니다.

위 사항을 염두에두고 커맨드에 몇가지 입력 추가를 시작해보겠습니다. input 속성에는 다음 사항을 추가합니다.

  • Forward - Bool. 전진 키를 누르고 있으면.
  • Backward - Bool. 후진 키를 누르고 있으면.
  • Left - Bool. 왼쪽키를 누르고 있으면.
  • Right - Bool. 우측키를 누르고 있으면.
  • Jump - Bool. 점프 키를 눌렀으면.
  • Yaw - Float. Y 축의 현재 회전.
  • Pitch - Float. X 축의 현재 회전.

입력에 적용하려는 result 에는 다음 4개의 속성을 나타냅니다.

  • Position - Vector3.
  • Velocity - Vector3.
  • IsGrounded - Bool. 땅에 닿았는지 여부.
  • JumpFrames - Integer. 이것은 약간 이상한 것으로 점프력을 적용할 때 남긴 프레임수를 나타내는 숫자입니다. 우리가 사용할 캐릭터 모터의 특정 세부사항입니다.

명령은 완료되었으며, 나중에 몇 가지를 추가할 것이지만 지금은 움직임이 동작하는 것이 필요합니다. Assets/Compile Bolt Assets (All) 로 이동하여 Bolt를 다시 컴파일합니다. 컴파일해주면 Bolt는 우리가 생성한 새로운 커맨드로 내부 데이터를 갱신할 것 입니다.

다음에 설정할 필요가 있는 것은 캐릭터 모터입니다. Bolt에는 우리가 필요한 모든 것을 지원하는 캐릭터 모터 작동이 내장되어 있습니다. bolt_tutorial/Scripts/Player/PlayerMotor.cs 에서 찾아볼 수 있습니다. 스크립트와 TutorialPlayer 프리팹을 찾고 프리팹에 모터의 사본을 붙여줍니다.

자동으로 추가된 Player Motor 컴포넌트와 Character Controller 컴포넌트의 2가지 설정을 조정할 필요가 있습니다.

  1. Step Offset 을 0.5 로 설정합니다
  2. Center 를 (0, 1, 0) 로 설정합니다
  3. Height 르 2.2 로 설정합니다
  4. 세계에 Layer Mask 를 설정합니다

tutorial/Scripts/Player 폴더에 새로운 TutorialPlayerController.cs 스크립트를 생성합니다.

새로운 클래스는 Bolt.EntityBehaviour<ITutorialPlayerState> 클래스에서 상속하여 TutorialPlayerState 에셋의 데이터에 직접적이고 정적인 접근을 할 수 있도록 합니다.

using UnityEngine;

public class TutorialPlayerController : Bolt.EntityBehaviour<ITutorialPlayerState> {

}

이제 우리는 꽤 양이 많은 코딩을 할 것이고 여러 작은 부분으로 나누어서 진행할 것 입니다. 우선 입력, 모터와 TutorialPlayerController 클래스에 대한 상수 필드를 추가합니다.

using UnityEngine;

public class TutorialPlayerController : BoltEntityBehaviour<ITutorialPlayerState> {
  const float MOUSE_SENSITIVITY = 2f;


  bool _forward;
  bool _backward;
  bool _left;
  bool _right;
  bool _jump;

  float _yaw;
  float _pitch;

  PlayerMotor _motor; 

  // ...

그리고 표준 유니티 Awake 메소드를 정의하여 내부에서 모터의 참조를 취득합니다.

  // ... 

  void Awake() {
    _motor = GetComponent<PlayerMotor>();
  }

  // ...

우리는 Bolt가 엔티티의 트랜스폼을 알 수 있도록 해야 합니다. Bolt.EntityBehaviour 베이스 클래스에서 제공하는 'Attached' 메소드를 오버라이드합니다. 이 메소드 내에서 state.Transform 에 접근하고 SetTransforms 메소드를 호출 하여 우리 게임 오브젝트의 트랜스폼에 제공합니다.

  public override void Attached() {
    state.SetTransforms(state.Transform, transform);
  }

다음은 로컬 플레이어의 입력 데이터 버퍼링에 사용되는 PollKeys 입니다. 여기에는 모든 버튼과 마우스 이동이 포함되어있습니다.

  // ...

  void PollKeys(bool mouse) {
    _forward = Input.GetKey(KeyCode.W);
    _backward = Input.GetKey(KeyCode.S);
    _left = Input.GetKey(KeyCode.A);
    _right = Input.GetKey(KeyCode.D);
    _jump = Input.GetKeyDown(KeyCode.Space);

    if (mouse) {
      _yaw += (Input.GetAxisRaw("Mouse X") * MOUSE_SENSITIVITY);
      _yaw %= 360f;

      _pitch += (-Input.GetAxisRaw("Mouse Y") * MOUSE_SENSITIVITY);
      _pitch = Mathf.Clamp(_pitch, -85f, +85f);
    }
  }

  // ...

표준 유니티 입력 코드와 거의 유사하니 이에 대해서 논하지 않도록 하겠습니다 - 정말 흥미로운 유일한 것은 bool mouse 파라미터로 마우스 입력을 폴(poll) 해야 되는지를 알려주는 것 으로 나중에 자세히 보겠습니다.

유니티가 Update 안에서 Input 클래스의 상태를 갱신하기 때문에 다음과 같이 PollKeys 함수를 호출하는 간단한 Update 함수를 정의 하겠습니다. PollKeys 함수에 true를 전달하여 마우스 움직임도 읽을 수 있도록 했습니다.

  // ...

  void Update() {
    PollKeys(true);
  }

  // ...

이제 Bolt에 관련된 사항을 다루어 보겠습니다. SimulateController 라고 하는 메소드를 오버라이드 할 것이며, 이 메소드는 엔티티의 control 이 할당된 컴퓨터에서만 호출 됩니다. 첫 번째로 우리가 해야할 것은 다시 PollKeys 를 호출하는 것이지만 false 를 전달하여 마우스 데이터를 읽을 필요가 없다고 알려줍니다. 이렇게 하는 이유는 여기에서 마우스 움직임을 읽는다는 것은 이중으로 읽는 것이기 때문입니다.

다음은 Bolt가 TutorialPlayerCommand 에셋에서 컴파일을 해 놓은 명령 입력의 인스턴스 생성을 위해서 TutorialPlayerCommand.Create() 를 호출하는 것입니다. 이제 모든 입력 데이터를 로컬 변수에서 입력으로 복사하여 전달 해주는 것 입니다.

마지막으로 엔티티에서 QueueInput 를 호출하는 것으로, 이것은 처리를 위해서 입력을 서버와 클라이언트로 전송하고 Bolt가 서버에서는 신뢰성에 대해서 여전히 유지하고 있지만 클라이언트 예측을 하도록 합니다.

  // ..


  public override void SimulateController() {
    PollKeys(false);

    ITutorialPlayerCommandInput input = TutorialPlayerCommand.Create();

    input.Forward = _forward;
    input.Backward = _backward;
    input.Left = _left;
    input.Right = _right;
    input.Jump = _jump;
    input.Yaw = _yaw;
    input.Pitch = _pitch;

    entity.QueueInput(input);
  }

  // ..

휴 ...거의 다되었고 하나의 메소드만 남아 있습니다. 이 메소드가 Bolt 중에서도 가장 중요하다는 점에 동의 합니다. 이것이 신뢰성있는 움직임과 제어에 관해 마법이 일어나는 곳입니다. ExecuteCommand 와 만나 엔티티의 controllerowner 둘 모두 실행합니다.

이 함수의 첫 번째 파라미터는 항상 커맨드로써 SimulateController 로 부터 QueueInput 으로 전송된 입력이 포함되어 있습니다. 두 번째 파라미터는 resetState 이며 controller 에서만 true 라는 것이 중요합니다. 이 값은 컨트롤러(일반적으로 클라이언트) 소유자(일반적으로 서버) 보정을 위해 전송하고 모터의 상태를 재설정해야 합니다.

함수의 코드안에서 resetState 이 true 인지를 체크하여 true이면 명령을 "실행"하지 않고 모터의 로컬 상태를 단순히 재설정만 해줍니다. true가 아니면 명령어의 입력을 Move 를 호출하여 모터에 적용시키는데 전달된 명령에서 Result 속성을 복제하여 새로운 상태를 리턴합니다.

전달된 명령으로 모터의 상태를 지정하는 것은 매우 중요하며 Bolt가 소유자로부터 컨트롤러에게 명령의 결과 보정을 적용할 수 있도록 해 줍니다.

  // ..

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

    if (resetState) {
      // we got a correction from the server, reset (this only runs on the client)
      _motor.SetState(cmd.Result.Position, cmd.Result.Velocity, cmd.Result.IsGrounded, cmd.Result.JumpFrames);
    }
    else {
      // apply movement (this runs on both server and client)
      PlayerMotor.State motorState = _motor.Move(cmd.Input.Forward, cmd.Input.Backward, cmd.Input.Left, cmd.Input.Right, cmd.Input.Jump, cmd.Input.Yaw);

      // copy the motor state to the commands result (this gets sent back to the client)
      cmd.Result.Position = motorState.position;
      cmd.Result.Velocity = motorState.velocity;
      cmd.Result.IsGrounded = motorState.isGrounded;
      cmd.Result.JumpFrames = motorState.jumpFrames;
    }
  }

  // ...

TutorialPlayerController 사본을 TutorialPlayer 에 붙이고 지금 Play As Server 를 눌렀다면 세계(애니메이션 없는)를 돌아다닐 수 있으며 캐릭터를 움직일 수 있어야 합니다. TutorialPlayerController 에 전체 코드를 참고하시기 바랍니다.

using UnityEngine;

public class TutorialPlayerController : Bolt.EntityBehaviour<ITutorialPlayerState> {
  const float MOUSE_SENSITIVITY = 2f;

  bool _forward;
  bool _backward;
  bool _left;
  bool _right;
  bool _jump;

  float _yaw;
  float _pitch;

  PlayerMotor _motor;

  void Awake() {
    _motor = GetComponent<PlayerMotor>();
  }

  public override void Attached() {
    state.SetTransforms(state.Transform, transform);
  }

  void PollKeys(bool mouse) {
    _forward = Input.GetKey(KeyCode.W);
    _backward = Input.GetKey(KeyCode.S);
    _left = Input.GetKey(KeyCode.A);
    _right = Input.GetKey(KeyCode.D);
    _jump = Input.GetKeyDown(KeyCode.Space);

    if (mouse) {
      _yaw += (Input.GetAxisRaw("Mouse X") * MOUSE_SENSITIVITY);
      _yaw %= 360f;

      _pitch += (-Input.GetAxisRaw("Mouse Y") * MOUSE_SENSITIVITY);
      _pitch = Mathf.Clamp(_pitch, -85f, +85f);
    }
  }

  void Update() {
    PollKeys(true);
  }

  public override void SimulateController() {
    PollKeys(false);

    ITutorialPlayerCommandInput input = TutorialPlayerCommand.Create();

    input.Forward = _forward;
    input.Backward = _backward;
    input.Left = _left;
    input.Right = _right;
    input.Jump = _jump;
    input.Yaw = _yaw;
    input.Pitch = _pitch;

    entity.QueueInput(input);
  }

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

    if (resetState) {
      // we got a correction from the server, reset (this only runs on the client)
      _motor.SetState(cmd.Result.Position, cmd.Result.Velocity, cmd.Result.IsGrounded, cmd.Result.JumpFrames);
    }
    else {
      // apply movement (this runs on both server and client)
      PlayerMotor.State motorState = _motor.Move(cmd.Input.Forward, cmd.Input.Backward, cmd.Input.Left, cmd.Input.Right, cmd.Input.Jump, cmd.Input.Yaw);

      // copy the motor state to the commands result (this gets sent back to the client)
      cmd.Result.Position = motorState.position;
      cmd.Result.Velocity = motorState.velocity;
      cmd.Result.IsGrounded = motorState.isGrounded;
      cmd.Result.JumpFrames = motorState.jumpFrames;
    }
  }
}

두개의 클라이언트가 연결되어 있는 서버의 스크린샷 (에디터 내) 입니다(Bolt 이전 버전이기 때문에 UI 가 약간 달라보입니다).

100% 신뢰성있는 움직임을 설명하는 동영상입니다(Bolt 이전 버전이기 때문에 UI 가 약간 달라보입니다).

다음 챕터 >>

기술문서 TOP으로 돌아가기