클릭하여 이동하기
이 샘플에서는 플레이어가 이동할 대상 위치를 클릭하여 이동하는 포인트 앤 클릭(Point-And-Click) 방법을 사용하여 캐릭터를 제어할 수 있는 탑-다운 게임을 만드는 방법을 보여 줍니다. 프로젝트에 Bolt
가 설치되어 구성되어 있지 않은 경우, 시작하기 튜토리얼을 먼저 따라하십시오.
우리는 이 샘플에서 유니티의 표준 에셋의 ThirdPersonCharacter
를 기반으로 사용할 것입니다. 이것은 이미 애니메이션으로 잘 제작되었고 네비게이션 시스템을 통해 제어할 수 있는 것 입니다. 이 튜토리얼은 (i)레벨 생성, (ii)캐릭터 설정 및 (iii)캐릭터를 관리하기 위한 Bolt 구성으로 나누어져 있습니다.
이 샘플의 최종 결과는 Bolt 샘플 리파지토리에서 확인할 수 있습니다.
레벨 생성
우리의 목표는 아래 그림과 비슷한 레벨을 만드는 것입니다. 캐릭터들이 스폰할 수 있는 장애물과 출발점이 있어야 합니다. 원하는 곳에 객체들을 배치할 수 있습니다. 창의적이기만 하면 됩니다.
다음은 레벨 생성에 사용되는 주요 단계입니다:
- 땅으로 사용될 플레인이 있어야 합니다.
- 땅위에 몇 개의 상자들을 놓으십시오.
- 플레이어로부터 이벤트를 받기 위해
EventSystem
객체를 추가하십시오. - 씬 주변에 몇 개의 스폰 포인트를 추가하십시오:
- 땅과 장애물에 일부 머터리얼을 추가하여 경기를 진행할 때 더 시각적인 효과를 볼 수 있습니다.
캐릭터가 가야할 목표 장소를 갱신하기 위해 GameObject
를 설정할 것 입니다:
- 빈
GameObject
를 생성하고TargetPointer
로 이름을 부여합니다. Player
에게 보일 수 있도록, 이 객체의 자식에Sphere
를 추가하여 투명한Material
을 생성합니다. 이것은 시각적인 참조로 동작을 하게 될 것 입니다.- 새로운 스크립트를 생성하여
PlaceTarget
로 이름을 부여하고TargetPointer
객체에 추가합니다. 목표 위치를 제어하기 위해 이 스크립트를 사용할 것 입니다.
다음은 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
클릭한 노멀 벡트의 올바른 위치로 이동해야 할 것 입니다.
이제 캐릭터에게 네비게이션 메쉬를 생성하기 위해 네비게이션 시스템을 설정해야 합니다. 하는 것은 정말 간단합니다:
Window/Navigation
탭의Navigation
을 오픈합니다. 유니티의 네비게이션에 익숙하지 않다면, 이 링크를 확인하십시오.- 네비게이션 영역을 구성합니다:
Object
의 최근 탭으로 이동하면 캐릭터가 걸어서 갈수 있는 곳과 없는 곳을 구성할 것 입니다.Ground
객체를 선택하여Navigation Static
으로 체크하고Walkable
로 설정합니다.- 씬의 모든 장애물을 선택하고
Navigation Static
으로 체크하고Not Walkable
로 설정합니다.
- 모든 객체들이 구성되었다면
NavMesh
를 생성해야합니다.Bake
텝으로 이동하고Bake
버튼을 클릭합니다. 이렇게 하면 폴더가 생성되고 씬 위치 바로 옆에NavMesh
파일이 생성될 것 입니다.
이러한 단계를 수행한 후에는 아래 표시된 것과 유사한 NavMesh
가 있어야 합니다. 파란색 영역은 모두 Walkable
으로, 씬의 장애물에 의해 메쉬가 어떻게 제한되는지를 주목하면, 우리의 캐릭터가 경로를 계산하면서 이러한 영역을 피할 수 있게 됩니다.
이제 레벨이 플레이 할 준비가 되었습니다. 원하시면 보행 가능 구역이나 장애물을 조금 더 조정할 수 있지만, 변경 후에는 반드시 리베이크를 해야합니다(Navigation
윈도우의 Bake
버튼을 다시 클릭하는 것을 잊지 마세요).
캐릭터 설정
이 샘플에서는 ThirdPersonCharacter
를 사용하고 있으며 특별히 AIThirdPersonController
버전(표준 에셋에서 다운로드 가능)을 메인 캐릭터로서 사용하고 있습니다. 이 캐릭터는 선글라스😎를 낀 멋진 남자 Ethan
이라는 모델에서 만들어졌습니다. 자 그럼 패키지를 다운로드하여 유니티 프로젝트로 가져옵니다.
다음과 같이 AIThirdPersonController
프리팹을 찾을 수 있는 유사한 구조가 되어야 합니다. 이 프리팹을 복사하여 다른 폴더로 옮기십시오. 원하시는 대로 폴더의 이름을 부여하세요. 여기서는 EthanClickToMove
를 사용할 것입니다. 원래 개체의 일부 구성 요소를 변경할 예정이므로 항상 백업을 수행하는 것이 좋습니다.
먼저, 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
를 게임에 통합하여 멀티플레이어로 만들 수 있습니다.
Bolt 통합
캐릭터가 걷고 목표 지점을 향해 따라가는 게임이 이제 동작을하므로, 프로젝트에 Bolt
통합 시작을 할 준비가 되었습니다. 전에 말했던것처럼 프로젝트에 Bolt
가 아직 설정되지 않았다면 먼저 시작하기 튜토리얼을 따라 해 주십시오.
에셋 설정
Bolt
에서는 모든 엔티티들이 States
를 사용하여 데이터를 동기화합니다. 이 예제도 다르지 않습니다. Window/Bolt/Assets
으로 이동하여 State
를 생성하고 ClickToMoveState
로 이름을 부여하여 다음 단계를 따라하세요:
- 새로운 속성
Transform
을 생성하고, 타입을Transform
로 설정합니다. - 네트워크를 통해 동기화를 하기 위해 플레이어가 사용하는 애니메이션으로 부터 속성을 임포트해야합니다.
Import Mecanim Parameters
에서ThirdPersonAnimatorController
에셋을 선택합니다.Import
버튼을 클릭합니다.- 모든 파라미터들이 올바르게 임포트되어야 합니다.
아래에 보인 것과 유사하게 구성된 State
가 있어야 합니다.
클라이언트
에서 서버
로 데이터를 전송할 수 있는 방법도 필요하며, 이는 Bolt
의 Commands
를 사용하여 이루어집니다. 커맨드는 플레이어에서 캐릭터를 제어하는 입력을 나타내지만, 서버
에서는 클라이언트
에 변경 사항을 보내 게임의 무결성을 유지하는 데 사용됩니다.
방금 ClickToMoveState
를 생성한 동일한 윈도우에서 우측 클릭을하고 새로운 Command
를 생성하여 ClickToMoveCommand
라고 이름을 부여해줍니다.
- 새로운
Input
을 생성하고click
으로 이름을 부여하여 타입을Vector
로 설정합니다. 이것은 플레이어가 설정한 목표 위치를 전송하는데 사용될 것 입니다. - 새로운
Result
를 생성하여position
라고 이름을 부여하고 타입을Vector
로 설정합니다. 이것은Server
상에 캐릭터가 어디있는지를 설정하게 될 것 입니다.
이제 Bolt Assets
를 닫을 수 있고 Assets/Bolt/Compile Assembly
의 커맨드를 실행하여 Bolt
를 컴파일 할 수 있습니다. 콘솔에 BoltCompiler: Success!
메시지가 나타났으면 계속할 준비가 되었다는 것 입니다.
Bolt
를 사용하여 플레이어를 복제하려면 Bolt Entity
컴포넌트를 추가하고 최근에 만든 State
를 사용하도록 구성해야 합니다. 다음에 표시된 그림과 같습니다.
EthanClickToMove
프리팹을 선택합니다.- 새로운 컴포넌트인
Bolt Entity
컴포넌트를 추가합니다. State
드롭다운 메뉴에서IClickToMoveState
를 선택합니다.- 라이브러리가 이 엔티티(
Id
에 의해 시그널된)를 알리기 위해Bolt
(Assets/Bolt/Compile Assembly
에서)를 컴파일합니다..
네트워크 스크립트
이번 세션에서는 일부 스크립트를 추가하여 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
(이 경우 해당 씬의 이름과 일치하도록 변경)으로 변경됩니다 SceneLoadLocalDone
과SceneLoadRemoteDone
콜백을 이용하며, 둘다 게임 씬이 로딩이 완료되었을 때 호출됩니다. 이 순간을 사용하여 임의의 위치에서EthanClickToMove
프리팹 사본을 인스턴스화합니다.(이전에 생성된 Respawn 포인트를 이용)- 이 작업은 게임 호스트(서버)에서만 실행되므로 씬이 로컬로 로드되면 플레이어가 엔티티의 (
player.TakeControl()
)를 제어하고 씬이 클라이언트에 로드되면 호스트는 해당 원격 플레이어에게 제어 권한을 부여(player.AssignControl(connection)
)합니다.
이제 Bolt
를 사용하여 명령을 전송하도록 컨트롤러 스크립트를 업데이트해야 합니다. ClickToMoveController
스크립트를 열고 몇 가지 내용을 변경합니다.
Bolt
에서 이벤트 콜백을 받으려면EntityEventListener
를 확장해야 합니다. 이렇게 하면 라이브러리에서 여러 유틸리티에 접근할 수 있습니다. 지네릭 아규먼트로서IClickToMoveState
를 포함한 것을 확인해 보세요. 이거은 특정State
의 데이터만 취급한다는 것을 보장해 줍니다.
C#
public class ClickToMoveController : Bolt.EntityEventListener<IClickToMoveState>
{
// ...
}
Attached()
콜백은 캐릭터 데이터를Bolt
에 붙일 수 있는 링크를 제공해줍니다. 이 경우에는Transform
그리고Animator
정보를 동기화할 것 입니다.
C#
// ...
public override void Attached()
{
state.SetTransforms(state.Transform, transform);
state.SetAnimator(GetComponentInChildren<Animator>());
}
// ...
- 나중에
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가 게임 호스트로 실행됩니다. 잘했습니다!
Have fun !
축하합니다. 튜토리얼을 끝냈습니다!
Back to top