Photon BOLT 유니티 멀티게임 만들기 Webinar 5탄

HostMigrationManager.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Bolt;
using Bolt.Matchmaking;
using UdpKit;
using System.Linq;

public class HostMigrationManager : GlobalEventListener
{
[SerializeField] GameObject titlePanel;
[SerializeField] GameObject hostMigrationPlayerPrefab;

[SerializeField] BoltEntity myEntity;
[SerializeField] Vector3 myEntityPos;
[SerializeField] bool nextHost;
int serverPort = 1000;

#region 참여와 셧다운

void Start()
{
titlePanel.SetActive(true);
}

public void StartServer() 
{
serverPort = Random.Range(1000, 25000);
BoltLauncher.StartServer(new UdpEndPoint(UdpIPv4Address.Any, (ushort)serverPort)); 
}

public void StartClient() => BoltLauncher.StartClient(UdpEndPoint.Any);

public override void BoltStartDone()
{
myEntity = null;
nextHost = false;

if (BoltNetwork.IsServer)
BoltMatchmaking.CreateSession(sessionID: "room");
else
BoltMatchmaking.JoinSession("room");
}

public override void BoltShutdownBegin(AddCallback registerDoneCallback, UdpConnectionDisconnectReason disconnectReason)
{
registerDoneCallback(BoltShutdownCallback);
}

void BoltShutdownCallback()
{
if (nextHost)
StartServer();
else
StartClient();
}

#endregion

public override void SceneLoadLocalDone(string scene, IProtocolToken token)
{
titlePanel.SetActive(false);

Vector3 spawnPos = new Vector3(Random.Range(-3f, 3f), 0, 0);
if (myEntityPos != Vector3.zero)
spawnPos = myEntityPos;

myEntity = BoltNetwork.Instantiate(hostMigrationPlayerPrefab, spawnPos, Quaternion.identity);
myEntity.TakeControl();

if (BoltNetwork.IsServer) 
StartCoroutine(NextHostCheckCo());
}

IEnumerator NextHostCheckCo() 
{
// 서버에서 다음 호스트를 체크함
while (true)
{
yield return new WaitForSeconds(5);

var entities = BoltNetwork.Entities.ToList();

bool isSetNextHost = false;
foreach (var entity in entities)
{
bool nextHost = entity.NetworkId != myEntity.NetworkId && !isSetNextHost;
if (nextHost)
isSetNextHost = true;

var evnt = HostMigrationNextHostEvent.Create();
evnt.networkId = entity.NetworkId;
evnt.nextHost = nextHost;
evnt.nextPort = serverPort;
evnt.Send();
}
}
}


public override void OnEvent(HostMigrationNextHostEvent evnt)
{
if (evnt.networkId == myEntity.NetworkId)
{
nextHost = evnt.nextHost;
serverPort = evnt.nextPort;
}
}

void Update()
{
if (myEntity != null)
myEntityPos = myEntity.transform.position;
}






HostMigrationPlayer.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;

public class HostMigrationPlayer : EntityBehaviour<IHostMigrationPlayerState>
{
[SerializeField] float speed;


void Update()
{
if (entity.IsOwner) 
{
transform.Translate(new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0)
* Time.deltaTime * speed);

state.position = transform.position;
}
else
{
transform.position = Vector3.Lerp(transform.position, state.position, 0.3f);
}
}
}






NetworkManager.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Bolt.Matchmaking;
using Bolt;

public class NetworkManager : GlobalEventListener
{
public static NetworkManager Inst { get; private set; }
void Awake() => Inst = this;

[SerializeField] GameObject playerPrefab;
[SerializeField] GameObject titlePanel;
[SerializeField] InputField nickInput;
public string myNickname => nickInput.text;

void Start()
{
titlePanel.SetActive(true);
}

public void StartServer() => BoltLauncher.StartServer();

public void StartClient() => BoltLauncher.StartClient();

public override void BoltStartDone()
{
if (BoltNetwork.IsServer)
BoltMatchmaking.CreateSession(sessionID: "room");
else
BoltMatchmaking.JoinSession("room");
}

public override void SceneLoadLocalDone(string scene, IProtocolToken token)
{
titlePanel.SetActive(false);

if (playerPrefab == null)
return;

Vector3 spawnPos = new Vector3(Random.Range(-3f, 3f), 0, 0);
BoltEntity entity = BoltNetwork.Instantiate(playerPrefab, spawnPos, Quaternion.identity);
entity.TakeControl();
}


}







InputSyncPlayer.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;

public class InputSyncPlayer : EntityBehaviour<IInputSyncPlayerState>
{
[SerializeField] float speed;

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

public override void SimulateController()
{
IInputSyncCommandInput input = InputSyncCommand.Create();
input.up = Input.GetKey(KeyCode.W);
input.down = Input.GetKey(KeyCode.S);
input.left = Input.GetKey(KeyCode.A);
input.right = Input.GetKey(KeyCode.D);
entity.QueueInput(input);
}

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

if (resetState)
{
transform.position = cmd.Result.position;
}
else
{
Vector3 dir = Vector3.zero;

if (cmd.Input.left ^ cmd.Input.right)
dir.x += cmd.Input.right ? 1 : -1;

if (cmd.Input.up ^ cmd.Input.down)
dir.y += cmd.Input.up ? 1 : -1;

transform.Translate(dir.normalized * BoltNetwork.FrameDeltaTime * speed);
cmd.Result.position = transform.position;
}
}
}









PhysicsPrecisionPlayer.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;

public class PhysicsPrecisionPlayer : EntityBehaviour<IPhysicsPrecisionPlayerState>
{
[SerializeField] float addforcePower;
Rigidbody rbody;

public override void Attached()
{
rbody = GetComponent<Rigidbody>();
state.SetTransforms(state.transform, transform);
}

public override void SimulateController()
{
IPhysicsPrecisionCommandInput input = PhysicsPrecisionCommand.Create();
input.up = Input.GetKey(KeyCode.W);
input.down = Input.GetKey(KeyCode.S);
input.left = Input.GetKey(KeyCode.A);
input.right = Input.GetKey(KeyCode.D);
entity.QueueInput(input);
}

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

if (resetState)
{
rbody.velocity = cmd.Result.velocity;
rbody.angularVelocity = cmd.Result.angularVelocity;
}
else
{
if (cmd.Input.up)
rbody.AddForce(Vector3.forward * BoltNetwork.FrameDeltaTime * addforcePower);
else if (cmd.Input.down)
rbody.AddForce(Vector3.back * BoltNetwork.FrameDeltaTime * addforcePower);
if (cmd.Input.right)
rbody.AddForce(Vector3.right * BoltNetwork.FrameDeltaTime * addforcePower);
else if (cmd.Input.left)
rbody.AddForce(Vector3.left * BoltNetwork.FrameDeltaTime * addforcePower);

cmd.Result.velocity = rbody.velocity;
cmd.Result.angularVelocity = rbody.angularVelocity;
}
}
}








ManyAgentSpawner.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using Bolt;
using UnityEngine;

public class ManyAgentSpawner : GlobalEventListener
{
    [SerializeField] GameObject agentPrefab;
    [SerializeField] int agentCount;


public override void SceneLoadLocalDone(string scene, IProtocolToken token)
{
        if (BoltNetwork.IsServer)
        {
            for (int i = 0; i < agentCount; i++)
                BoltNetwork.Instantiate(agentPrefab, new Vector3(0, 2, 0), Quaternion.identity);
        }
    }
}








ManyAgent.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class ManyAgent : Bolt.EntityEventListener<IManyAgentState>
{
    [SerializeField] NavMeshAgent agent;
    NavMeshHit navMeshHit;
    Vector3 curPos;

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

        if (BoltNetwork.IsServer)
            StartCoroutine(Navigation());
    }

    IEnumerator Navigation()
    {
        while (true)
        {
            while (true)
            {
                Vector3 randPos = Random.insideUnitCircle;
                randPos = new Vector3(randPos.x, 0, randPos.y);
                randPos = randPos * 30f + transform.position;

                NavMesh.SamplePosition(randPos, out navMeshHit, 10f, NavMesh.AllAreas);

                if (navMeshHit.position.x != Mathf.Infinity)
                    break;
            }

            agent.SetDestination(navMeshHit.position);
            yield return new WaitForSeconds(5f);
        }
    }

void Update()
{
        if (!entity.IsOwner)
        {
            transform.position = Vector3.Lerp(curPos, transform.position, 0.1f);
            curPos = transform.position;
        }
    }

}










Tips.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Bolt;
using System.Linq;

public class Tips : GlobalEventListener
{
[SerializeField] GameObject spawnPrefab;

void Start()
{
// API문서 참조 : https://doc-api.photonengine.com/en/bolt/current/index.html

// 셧다운
BoltLauncher.Shutdown();

// 싱글플레이로 실행, CCU 포함 안됨
BoltLauncher.StartSinglePlayer();

// OverlapSphereAll 원을 그려 충돌한 모든 엔티티 가져오기
if (GetComponent<BoltEntity>().IsOwner)
{
Vector3 origin = Vector3.zero;
float radius = 2f;
var hits = BoltNetwork.OverlapSphereAll(origin, radius);
for (int i = 0; i < hits.count; i++)
{
var hit = hits.GetHit(i);
var targetEntity = hit.body.GetComponent<BoltEntity>();
}
}

// RaycastAll 레이를 쏴 충돌한 모든 엔티티 가져오기
if (GetComponent<BoltEntity>().IsOwner)
{
var hits = BoltNetwork.RaycastAll(new Ray(transform.position, Vector3.down));
var hit = hits.GetHit(0);
var targetEntity = hit.body.GetComponent<BoltEntity>();
}

// 접속한 모든 클라이언트들 가져오기
{
IEnumerable<BoltConnection> clients = BoltNetwork.Clients;
List<BoltConnection> clientList = clients.ToList();
print(clientList[0].ConnectionId);
print(clientList[0].PingNetwork);
clientList[0].Disconnect();
}

// 현재 씬에 로드된 모든 엔티티 가져오기
IEnumerable<BoltEntity> entities = BoltNetwork.Entities;

// Time.fixedDeltaTime, Time.time과 같다
float frameDeltaTime = BoltNetwork.FrameDeltaTime;
float time = BoltNetwork.Time;

// bool
bool isServer = BoltNetwork.IsServer;
bool isClient = BoltNetwork.IsClient;
bool isSinglePlayer = BoltNetwork.IsSinglePlayer;
bool isDebugMode = BoltNetwork.IsDebugMode;
bool isRunning = BoltNetwork.IsRunning;

// 하나이상 연결이 되었는지, 서버 혼자 열면 연결된게 아니라, 
// 최소한 하나의 통신로가 존재해야 하므로 하나의 클라가 있어야함
bool isConnected = BoltNetwork.IsConnected;

// 서버 프레임, 시간 가져오기
int serverFrame = BoltNetwork.ServerFrame;
float serverTime = BoltNetwork.ServerTime;

// Photon Bolt 어셈블리 버전 번호
System.Version version = BoltNetwork.Version;
version.ToString();

// Bolt탭 - Settings - Miscellaneous - Log Targets를 Unity, Console 체크 - 
// Show Debug Info 체크 - Tab으로 디버그 보이게 할수 있다
BoltLog.Info("Info");
BoltLog.Warn("Warn");
BoltLog.Error("Error");
}

public override void SceneLoadLocalDone(string scene, IProtocolToken token)
{
if (BoltNetwork.IsServer)
{
BoltEntity entity = BoltNetwork.Instantiate(spawnPrefab);
// 생성도 서버가 하고 서버가 조종하도록 컨트롤을 달아줌
entity.TakeControl();
}
}


// 리모트 씬이 불러와짐
public override void SceneLoadRemoteDone(BoltConnection connection, IProtocolToken token)
{
if (BoltNetwork.IsServer) 
{
BoltEntity entity = BoltNetwork.Instantiate(spawnPrefab);
// 생성은 서버가 했지만 연결된 클라이언트가 제어하도록 컨트롤을 달아줌, 서버에서 연산에 유리
entity.AssignControl(connection);

// 엔티티 판단
bool hasControl = entity.HasControl;
bool isOwner = entity.IsOwner;
bool isControllerOrOwner = entity.IsControllerOrOwner;
NetworkId networkId = entity.NetworkId;
print(networkId.PackedValue);
}
}
}











FallGuysManager.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Bolt;
using Bolt.Matchmaking;

public class FallGuysManager : GlobalEventListener
{
public static FallGuysManager Inst { get; private set; }
void Awake() => Inst = this;

[SerializeField] GameObject playerPrefab;
[SerializeField] GameObject titlePanel;
[SerializeField] InputField nickInput;
public string myNickname => nickInput.text;


void Start()
{
titlePanel.SetActive(true);
}

public void StartServer() => BoltLauncher.StartServer();

public void StartClient() => BoltLauncher.StartClient();

public override void BoltStartDone()
{
if (BoltNetwork.IsServer)
BoltMatchmaking.CreateSession(sessionID: "room");
else
BoltMatchmaking.JoinSession("room");
}

public override void SceneLoadLocalDone(string scene, IProtocolToken token)
{
titlePanel.SetActive(false);

Vector3 spawnPos = new Vector3(Random.Range(-5f, 5f), 0, Random.Range(-5f, 5f));
BoltEntity entity = BoltNetwork.Instantiate(playerPrefab, spawnPos, Quaternion.identity);
entity.TakeControl();
}
}








FallGuysCameraArm.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FallGuysCameraArm : MonoBehaviour
{
[SerializeField] float minY;
[SerializeField] float maxY;
[SerializeField] Transform cmCamera;

public Vector3 camForward;
public Transform target;

void Update()
{
if (target == null)
return;

transform.position = target.position + Vector3.up * 1.66f;

Vector2 mouseDelta = new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y"));
Vector3 camAngle = transform.rotation.eulerAngles;
transform.rotation = Quaternion.Euler(Mathf.Clamp(camAngle.x + mouseDelta.y, minY, maxY), camAngle.y + mouseDelta.x, camAngle.z);
camForward = transform.position - cmCamera.position;
camForward.y = 0;
camForward.Normalize();
}
}










FallGuysPlayer.cs 소스입니다


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Bolt;

public class FallGuysPlayer : EntityBehaviour<IFallGuysPlayerState>
{
    [SerializeField] float speed;
    [SerializeField] float rotationLerp;
    [SerializeField] float jumpPower;
    [SerializeField] float jumpCoolTime;
    [SerializeField] Text nickText;
    [SerializeField] Transform playerCanvas;

    Animator animator;
    Rigidbody rbody;
    FallGuysCameraArm cameraArm;
    bool isGround;
    bool jumpable = true;

    public override void Attached() 
    {
        rbody = GetComponent<Rigidbody>();
        animator = GetComponent<Animator>();

        state.SetTransforms(state.transform, transform);
        state.SetAnimator(animator);
        
        state.nickname = FallGuysManager.Inst.myNickname;
        state.AddCallback("nickname", NicknameCallback);
    }

    void NicknameCallback() 
    {
        nickText.text = state.nickname;
    }

    void Start()
    {
        cameraArm = FindObjectOfType<FallGuysCameraArm>();

        if (entity.IsOwner)
            cameraArm.target = transform;
    }

    public override void SimulateController()
    {
        IFallGuysPlayerCommandInput input = FallGuysPlayerCommand.Create();
        input.up = Input.GetKey(KeyCode.W);
        input.down = Input.GetKey(KeyCode.S);
        input.left = Input.GetKey(KeyCode.A);
        input.right = Input.GetKey(KeyCode.D);
        input.jump = Input.GetKey(KeyCode.Space);
        entity.QueueInput(input);
    }

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

        if (resetState)
        {
            rbody.velocity = cmd.Result.velocity;
            rbody.angularVelocity = cmd.Result.angularVelocity;
        }
        else 
        {
            Vector3 dir = Vector3.zero;

            // WASD 방향벡터와 부드러운 회전
            if (cmd.Input.up)
            {
                dir += cameraArm.camForward;
                transform.rotation = Quaternion.Lerp(transform.rotation,
                    Quaternion.LookRotation(cameraArm.camForward), rotationLerp);
            }

            else if (cmd.Input.down)
            {
                dir += -cameraArm.camForward;
                transform.rotation = Quaternion.Lerp(transform.rotation,
                    Quaternion.LookRotation(-cameraArm.camForward), rotationLerp);
            }

            if (cmd.Input.right)
            {
                Vector3 camRight = Quaternion.Euler(0, 90, 0) * cameraArm.camForward;
                dir += camRight;
                transform.rotation = Quaternion.Lerp(transform.rotation,
                    Quaternion.LookRotation(camRight), rotationLerp);
            }

            else if (cmd.Input.left)
            {
                Vector3 camLeft = Quaternion.Euler(0, -90, 0) * cameraArm.camForward;
                dir += camLeft;
                transform.rotation = Quaternion.Lerp(transform.rotation,
                    Quaternion.LookRotation(camLeft), rotationLerp);
            }

            // 걷기 애니메이션
            state.isWalk = dir != Vector3.zero;
            animator.SetBool("isWalk", state.isWalk);

            rbody.velocity = dir.normalized * speed + rbody.velocity.y * Vector3.up;
            isGround = Physics.Raycast(transform.position + Vector3.up, Vector3.down, 1.1f);

            // 점프
            if (cmd.Input.jump && jumpable && isGround)
            {
                state.jumpTrigger();
                rbody.velocity = Vector3.zero;
                rbody.AddForce(Vector3.up * jumpPower);
                Invoke(nameof(JumpCoolTimeDelay), jumpCoolTime);
                jumpable = false;
            }

            cmd.Result.velocity = rbody.velocity;
            cmd.Result.angularVelocity = rbody.angularVelocity;
        }
    }

    void JumpCoolTimeDelay() => jumpable = true;

void LateUpdate() => playerCanvas.rotation = Camera.main.transform.rotation;

void Update()
    {
        // Y만 따로 동기화
        if (entity.IsOwner)
            state.Y = transform.position.y;
        else 
        {
            Vector3 pos = transform.position;
            pos.y = state.Y;
            transform.position = pos;
        }
}

}