위치 동기화 원리

접속한 모든 클라이언트는 서버의 state에 자신의 위치 정보를 전송한다.

이렇게 state에 저장된 클라이언트들의 위치 정보는 OnState 를 통해 동기화되게 된다.

image-20220715233839896

순서 1. StartCoroutine으로 클라이언트 위치 정보를 정기적으로 전달

[ClientStarter.ts]

Start() {
	...
	// StartCoroutine정기적으로 클라이언트의 위치정보 전달
	this.StartCoroutine(this.SendMessageLoop(0.1));
}

SendMessageLoop() 함수 정의

    private * SendMessageLoop(tick: number) {
        while (true) {
            yield new UnityEngine.WaitForSeconds(tick);

            if (this.room != null && this.room.IsConnected) {
                // 로컬 플레이어의 인스턴스 존재여부를 판단 HasPlayer
                const hasPlayer = ZepetoPlayers.instance.HasPlayer(this.room.SessionId);
                // 로컬 플레이어가 있다면 myPlayer에 로컬 플레이어 인스턴스를 저장
                if (hasPlayer) {
                    const myPlayer = ZepetoPlayers.instance.GetPlayer(this.room.SessionId);
                    // 만약 myPlayer가 움직이면, 내 위치 정보를 전송
                    // SendTransform 함수로 정의하여 전송
                    if (myPlayer.character.CurrentState != CharacterState.Idle) {
                        this.SendTransform(myPlayer.character.transform);
                    }
                }
            }
        }
    }

SendTransform() 함수로 서버에 전달할 Transform 함수

// 내 위치 정보를 전송하는 함수
private SendTransform(transform: UnityEngine.Transform) {
    // transform을 전송하기 위해 RoomData 객체 생성
    const data = new RoomData();

    // position을 넣어줄 RoomData 객체 생성
    const pos = new RoomData();
    pos.Add("x", transform.localPosition.x);
    pos.Add("y", transform.localPosition.y);
    pos.Add("z", transform.localPosition.z);
    data.Add("position", pos.GetObject());

    // rotation을 넣어줄 RoomData 객체 생성
    const rot = new RoomData();
    rot.Add("x", transform.localEulerAngles.x);
    rot.Add("y", transform.localEulerAngles.y);
    rot.Add("z", transform.localEulerAngles.z);
    data.Add("rotation", rot.GetObject());

    // onChanged 타입으로 메세지를 전송
    this.room.Send("onChangedTransform", data.GetObject);
}

순서 2. Room 상태 동기화하기

[index.ts]

onCreate() 함수에서 개별 클라이언트들의 위치를 수신하여 player에 전달

onCreate(options: SandboxOptions) {

    // 개별 클라이언트들의 위치를 수신받을 수 있도록
    // onChangedTransform 메시지 리스너
    this.onMessage("onChangedTransform", (client, message) => {
        const player = this.state.players.get(client.sessionId);

        // 받아온 위치 정보를 받기 위한 transform 객체
        const transform = new Transform();
        transform.position.x = message.position.x;
        transform.position.y = message.position.y;
        transform.position.z = message.position.z;

        transform.rotation = new Vector3();
        transform.rotation.x = message.rotation.x;
        transform.rotation.y = message.rotation.y;
        transform.rotation.z = message.rotation.z;

        // message에서 전달받은 플레이어 위치 정보를 player에 저장
        player.transform = transform;
    });

순서 3. 다른 클라이언트 위치 수신받기

[ClientStarter.ts > OnStateChange()]

local player가 아닌 player의 위치정보를 업데이트하기 위한 리스너 추가

// 캐릭터 정보를 전송받기 위해 OnAddedPlayer에 새로운 리스너를 추가
 ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
 	// local player가 아닌 경우에만 업데이트하도록
	// local player인지 판단하는 isLocal 상수를 생성
	const isLocal = this.room.SessionId === sessionId;

	// Local player가 아니라면
	if (!isLocal) {
		// current players에 sessionId에 해당하는 플레이어를 불러옴
		const player: Player = this.currentPlayers.get(sessionId);

		// player의 위치 정보를 업데이트 하는 함수 선언
		player.OnChange += (ChangeValues) => this.OnUpdatePlayer(seesionId, player);
		}
	});

순서 4. 다른 클라이언트 위치 수신받기

[ClientStarter.ts > OnUpdatePlayer() 신규 생성]

player의 위치 정보를 받기 위한 함수 선언

private OnUpdatePlayer(sessoinId: string, player: Player) {
        
	const position = this.ParseVector3(player.transform.position);
}

[ClientStarter.ts > ParseVector3() 신규 생성]

ParseVector3에서 인자로 포함된 Schema 타입의 Vector3를 유니티엔진의 Vector3 로 변환

// ParseVector3 에서 인자로 포함된 Schema 타입의 Vector3를 유니티엔진의 Vector3로 변환
private ParseVector3(vector3: Vector3): UnityEngine.Vector3 {
	return new UnityEngine.Vector3(
		vector3.x,
		vector3.y,
		vector3.z
	);
}

위에서 Vetor3 타입을 받아오지 못해, 에러가 발생하는데 Schema로부터 Vector3 클래스를 받아와야한다.

import { Player, State, Vector3 } from 'ZEPETO.Multiplay.Schema';

player가 점프 상태라면 캐릭터를 점프시킴

private OnUpdatePlayer(sessionId: string, player: Player) {
        
	...

	// 위치를 업데이트할 player를 받아옴
	const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);

	// MoveToPosition() 함수로 플레이어를 이동
	zepetoPlayer.character.MoveToPosition(position);

	// 플레이어 상태가 점프인 경우, 점프 상태를 호출
	if (player.state === CharacterState.JumpIdle || player.state === CharacterState.JumpMove)
		zepetoPlayer.character.Jump();
}

순서 5. Room 퇴장 로직 작성

[index.ts > onLeave()]

Room 퇴장시 클라이언트의 sessionId를 삭제

// Room 퇴장시 클라이언트의 sessionId 삭제
onLeave(client: SandboxPlayer, consented?: boolean) {
    this.state.players.delete(client.sessionId);
}

[ClientStarter.tst > OnStateChange() ]

OnStateChange()에서 leave를 처리

...
// leave 한 플레이어를 관리하기 위해 맵을 생성하여 새로운 변수 leave에 할당
let leave = new Map<string, Player>(this.currentPlayers);

// schema에 저장된 room state의 값을 foreach로 하나씩 조회 
state.players.ForEach((sessionId: string, player: Player) => {
	if (this.currentPlayers.has(sessionId)) {
		join.set(sessionId, player);
		}
		// Room에 존재하는 player는 모두 제거하고, 퇴장한 player만 leave에 추가
		leave.delete(sessionId);
	});

	// room에 플레이어가 입장할 때 event를 받을 수 있게 플레이어 객체에 .OnJoinPlayer()를 연결
	join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));

	// Room에서 퇴장한 모든 player 인스턴스를 제거하기 위해 OnLeavePlayer() 호출
	leave.forEach((player: Player, sessionId: string) => this.OnLeavePlayer(sessionId, player));
}

OnLeavePlayer() 함수 정의

private OnLeavePlayer(sessionId: string, player: Player) {
	console.log(`[OnRemove] players - sessionId : ${sessionId}`);
	this.currentPlayers.delete(sessionId);

	ZepetoPlayers.instance.RemovePlayer(sessionId);
}

이것을 모든 월드 로직 작성은 끝났다.

그런데 실행을 하니, 플레이어 인스턴스와 모바일 인풋이 보이지 않는다.

아무래도 받아온 player 정보를 멀티플레이 월드에서 생성해야 되는 코드가 빠진 것 같다.

image-20220716121858468

댓글남기기