상세 컨텐츠

본문 제목

[Unity] ZEPETO MultiPlay Guide - 월드 로직 작성하기 1

Unity

by 로니콜먼 2022. 6. 21. 14:11

본문

해당 글은

ZEPETO MultiPlay Guide - 월드 로직 작성하기 1 유튜브 영상을 정리한 내용 입니다.

https://youtu.be/tfkY_raboV0

 

  • 서버, 클라이언트 간 통신을 위해 필요한 Schema에 대해 알아보고, Schema Types와 Room State를 정의합니다.

 

Room State 작성하기 

* Room State :  Room에 접속중이 플레이어와 관련정보, Object 위치 등을 관리하기 위한 State Property로

미리 정의된 Schema 구조로 표현됩니다.

* Schema Types : 서버 / 클라이언트 통신용 Data Structure. 이를 편집하기 위해서는 Project 창에서 Assets > World.multiplay > schemas를 클릭 

World에서 사용할 Room State와 Schema Types는 Inspector 창에서 편집 가능 

 

Room State 정의하기(Schema Types 작성)

Schema Types에서 [+] 버튼을 클릭하여 작성 (Player, Transform, Vector3 등 추가)

Room State에서 [+] 버튼을 클릭하여 작성(Room의 Player 정보 추가)

작성 후 apply 클릭

 

Room 입장과 캐릭터 초기화

Room Lifecycle Event 4가지

- onCreate( options: SandboxOptions ) : Room이 생성될 때 1회 호출되며, Room에 대한 초기화 로직을 추가할 수 있습니다.

- onJoin( client: SandboxPlayer ) : Client가 Room에 입장할 때 호출됩니다. Client의 ID 및 캐릭터 정보는 SandboxPlayer 객체에 포함되어 있습니다.

- onLeave( client: SandboxPlayer, constend?: boolean ) : Client가 Room에서 퇴장할 때 호출됩니다. Client가 연결 해제를 요청한 경우, Consented 값이 true로 호출되며, 그렇지 않은 경우 false로 호출됩니다.

- onTick( deltaTime: number ) : SandboxOptions에서 설정된 tickInterval 마다 반복적으로 호출되며, 각종 Interval 이벤트를 관리할 수 있습니다.

 

Room 입장 시 이벤트 처리하기 

index.ts 파일을 열어 onJoin 먼저 작성 

onJoin 클라이언트가 룸에 처음 입장했을 때 실행하므로 캐릭터 초기화 등 처음에 수행할 일들을 정의

인자로 넘어온 SandboxPlayer 객체는 client와 관련된 정보를 담고있음

 

입장한 클라이언트 정보 출력

> console.log(`[OnJoin] sessionId : ${client.sessionId}, HashCode : ${client.hashCode}, userId : ${client.userId}`);

룸에 입장한 플레이어를 정의하기 위해 schemas에 정의했던 player 타입 사용

Player에 sessionId, zepetoHash, userId 초기 값 세팅

const player = new Player();
player.sessionId = client.sessionId;

if(client.hashCode){
	player.zepetoHash = client hashCode;
}
if(client.userId){
	player.zepetoUserId = client.userId;
}

 

Zepeto World에서 지원하는 Data Storage Component로 플레이어 데이터를 서버에 저장

Data Storage 객체 생성 후 Client의 Data Storage를 불러옴

//DataStorage 객체 생성
const storage: DataStorage = client.loadDataStorage();

//클라이언트 방문횟수 저장. await함수 사용 위해 onJoin메소드에 async 추가 필요
let visit_cnt = await storage.get("VisitCount") as number;
if(visit_cnt == null) visit_cnt = 0;

//플레이어 데이터 확인을 위한 로그 출력
console.log(`[OnJoin] ${client.sessionId}'s visiting count : ${visit_cnt}`);

//플레이어의 visit_cnt를 갱신해서 storage에 저장
await storage.set("VisitCount", ++visit_cnt);

//위에 설정한 플레이어 정보 저장
//scehmas의 Room State에 정의해두었던 player에 저장하려면 클라이언트 고유 키 값인 sessionId를 통해 player를 저장
this.state.players.set(client.sessionId, player);

플레이 > 플레이 종료 후 > 다시 플레이로 Visiting Count 갱신 확인 가능

 * Unity 환경의 로컬 서버에서는 서버 종료 후 다시 실행했을 때 데이터가 유지되지 않습니다.

월드 배포(앱 등록) 후에는 ZEPETO DB에 저장되어서 데이터가 유지됩니다.

 

Room 설정하기 (클라이언트 - Start)

Assets ->  Create -> ZEPETO -> TypeScript

ClientStater 파일 생성, ClientStarter 빈 오브젝트 생성 후 스크립트 추가 

초반 룸 설정, 캐릭터 초기화 코드 작성

 

multiplay에 필요한 클래스 import

> import { ZepetoWorldMultiplay } from 'ZEPETO.World'

 

> public multiplay : ZepetoWorldMultiplay;

public이므로 Component를  Unity에서 직접 연결필요

WorldMultiplay 오브젝트를 multiplay에 할당

해당 컴포넌트는 클라이언트의 roomCreate roomJoined와 같은 서버 룸 이벤트를 연동하는 인터페이스 제공

 

* ZepetoWorldMultiplay : ZEPETO Multiplay 서버의 Room(Game session) Event를 클라이언트에서 연동할 수 있는 인터페이스를 제공합니다.

* Room EventListener 목록 

- RoomCreated(Room) : Room이 생성되고, 접속 가능할 때 호출됩니다. Room을 인자로 전달됩니다.

- RoomJoined(Room) : 해당 Room에 접속되면 호출됩니다. Room을 인자로 전달합니다.

- RoomLeave(RoomLeaveEvent) : 해당 Room의 접속을 해제할 때 호출됩니다. RoomLeaveEvent(상태 코드 정보)를 인자로 전달합니다. 

- RoomReconnected(Room) : 해당 Room에 재연결되었을 때 호출됩니다. Room을 인자로 전달합니다.

- RoomError(RoomErrorEvent) : 해당 Room에서 오류가 발생했을 때 호출됩니다. RoomErrorEvent(에러 코드 정보)를 인자로 전달합니다.

- RoomWeakConnection : 해당 Room 객체와의 연결이 불안정할 때 호출됩니다.

 

이벤트 리스너 등록

import { CharacterState, SpawnInfo, ZepetoPlayers } from 'ZEPETO.Character.Controller';
import { Room, RoomData } from 'ZEPETO.Multiplay';
import { Player, State } from 'ZEPETO.Multiplay.Schema';
import { ZepetoScriptBehaviour } from 'ZEPETO.Script'
import { ZepetoWorldMultiplay } from 'ZEPETO.World'
import * as UnityEngine from 'UnityEngine';

export default class ClientStarter extends ZepetoScriptBehaviour {

    public multiplay: ZepetoWorldMultiplay;
    private room: Room;

    //접속된 플레이어 관리
    private currentPlayers:Map<string, Player> = new Map<string, Player>();

    //Room 설정하기(Client - Start) :  초반 룸 설정, 캐릭터 초기화 코드 작성
    Start() {    

        this.multiplay.RoomCreated += (room : Room) => {
        	this.room = room;
        };
        
        this.multiplay.RoomJoined += (room : Room) => {
            //state가 변할 때마다 호출되는 onStateChange 함수 등록
            room.OnStateChange += this.OnStateChange;
        };

    }

    //Player의 join에 관련된 처리 진행
    private OnStateChange(state: State, isFirst: boolean){

        //Zepeto Players의 이벤트리스너 등록
        if(isFirst){    //처음에만 등록하면 됨
            ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
                const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
                myPlayer.character.OnChangedState.AddListener((cur, prev)=>{
                    this.SendState(cur);
                });
            })
        }

        let join = new Map<string, Player>();
        state.players.ForEach((sessionId: string, player:Player) =>{
            //currentPlayers에 sessionId가 없는 경우 지금 입장한 Player로 판단
            if(!this.currentPlayers.has(sessionId)){
                join.set(sessionId, player);
            }
        });

        join.forEach((player: Player, sessionId: string)=> this.OnJoinPlayer(sessionId, player));
    }

    private SendState(state: CharacterState){
        const data = new RoomData();
        data.Add("state", state);
        this.room.Send("onChangeState", data.GetObject());
    }

    private OnJoinPlayer(sessionId: string, player: Player){
        console.log(`[OnJoinPlayer] players - sessionId : ${sessionId}`);
        //룸에 입장한 플레이어를 관리하기 위해 지금 입장한 플레이어를 currentPlayers에 등록
        this.currentPlayers.set(sessionId, player);

        //Player 인스턴스의 초기 transform 설정을위해 spawnInfo 객체 생성
        const spawnInfo = new SpawnInfo();
        const position = new UnityEngine.Vector3(0, 0, 0);
        const rotation = new UnityEngine.Vector3(0, 0, 0);
        
        spawnInfo.position = position;
        spawnInfo.rotation = UnityEngine.Quaternion.Euler(rotation);
        
        //room.sessionId와 player.sessionId가 같으면 local player 라고 판단
        const isLocal = this.room.SessionId === player.sessionId;
        ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);

    }



}

 

클라이언트로부터 수신된 메시지 확인

import { Sandbox, SandboxOptions, SandboxPlayer } from "ZEPETO.Multiplay";
import { DataStorage } from "ZEPETO.Multiplay.DataStorage";
import { Player } from "ZEPETO.Multiplay.Schema";

export default class extends Sandbox {

    onCreate(options: SandboxOptions) {
        //클라이언트로부터 수신된 메시지 확인
        this.onMessage("onChangedState", (client, message) => {
            //sessionId를 통해 메시지를 보낸 플레이어의 정보를 가져옴
            const player = this.state.players.get(client.sessionId);
            player.state = message.state;
        })
        
    }

    async onJoin(client: SandboxPlayer) {
        //입장한 클라이언트 정보 출력
        console.log(`[OnJoin] sessionId : ${client.sessionId}, HashCode : ${client.hashCode}, userId : ${client.userId}`);

        const player = new Player();
        player.sessionId = client.sessionId;

        if(client.hashCode){
            player.zepetoHash = client.hashCode;
        }
        if(client.userId){
            player.zepetoUserId = client.userId;
        }

        //DataStorage 객체 생성
        const storage: DataStorage = client.loadDataStorage();

        //클라이언트 방문횟수 저장. await함수 사용 위해 onJoin메소드에 async 추가 필요
        let visit_cnt = await storage.get("VisitCount") as number;
        if(visit_cnt == null) visit_cnt = 0;

        //플레이어 데이터 확인을 위한 로그 출력
        console.log(`[OnJoin] ${client.sessionId}'s visiting count : ${visit_cnt}`);

        //플레이어의 visit_cnt를 갱신해서 storage에 저장
        await storage.set("VisitCount", ++visit_cnt);

        //위에 설정한 플레이어 정보 저장
        //scehmas의 Room State에 정의해두었던 player에 저장하려면 클라이언트 고유 키 값인 sessionId를 통해 player를 저장
        this.state.players.set(client.sessionId, player);
    }

    onLeave(client: SandboxPlayer, consented?: boolean) {
        
    }
}

 

 

관련글 더보기