Server 정보

  • World.multiplay (Zepeto Multiplay Server)

DB 정보

  • Schema (Zepeto Multiplay Schema Definition)

순서 1. Schemas 정의하기

Room State : Room의 현재 상태를 나타내는 Property. Schema 구조는 미리 정의되어 있다.

Schema Types : 서버 / 클라이언트 통신용 Data Structure이다. 이 데이터 구조로 클라이언트의 정보를 서버로 반대로 서버의 정보를 클라이언트에게로 뿌릴 수 있다.

멀티 환경에서 플레이어 간의 위치와 방향값을 저장하는 데이터 구조를 만들기 위해 먼저 Vector3로 필드값을 만들어주고, 이 Vector3로 Transform 구조를 정의한다. 다음으로 플레이어 정보 구조를 작성하는데, 플레이어의 작업을 식별하는 sessionID와 zepetoHash 필드를 문자열로 만들어주고, zepetoUserID는 플레이어 객체로 생성한다. 마지막으로 플레이어의 상태값을 저장하기 위해, state를 number로 정의한다.

Schemas 정의는 아래와 같이 한다.

image-20220713220902138

image-20220713223319097

순서 2. Zepeto Multiplay Server가 제공하는 Room Lifecycle Event

  1. onCreate ( options : SandboxOptions) : Room이 생성될 때 1회 호출 되며, Room에 대한 초기화 로직을 추가할 수 있다.
  2. onJoin ( client : SandboxPlayer ) : Client가 Room에 입장할 때 호출된다. Client의 ID 및 캐릭터 정보는 SandboxPlayer 객체에 포함된다.
  3. onLeaver ( client : SandboxPlayer, constend? : boolean) : Client가 Room에서 퇴장할 때 호출된다. 클라이언트가 연결 해제를 요청한 경우, Constented 값이 true로 호출되며 그렇지 않은 경우 false로 호출된다.
  4. onTick ( deltaTime : number ) : SandboxOptions에서 설정된 tickInterval마다 반복적으로 호출되며, 각종 Interval 이벤트를 관리할 수 있다.

[ WorldMultiplay 오브젝트’s index.ts ]

import { Sandbox, SandboxOptions, SandboxPlayer } from "ZEPETO.Multiplay";
import { DataStorage } from "ZEPETO.Multiplay.DataStorage";
// Schema에서 Player 임포트
import { Player } from "ZEPETO.Multiplay.Schema";


export default class extends Sandbox {

    // Room이 생성되었을 때
    onCreate(options: SandboxOptions) {
        
    }

    // Player가 Room에 입장했을 때
    async onJoin(client: SandboxPlayer) {
        console.log(`clientID: ${client.userId} / sessionID: ${client.sessionId} / hashCode: ${client.hashCode}`);

        // 플레이어를 const로 생성
        const player = new Player();

        // 세션 ID 받아오기
        player.sessionId = client.sessionId;

        // hashcode 받아오기
        if (client.hashCode) {
            player.zepetoHash = client.hashCode;
        }

        // user ID 받아오기
        if (client.userId) {
            player.zepetoUserId = client.userId;
        }

        // 플레이어의 DataStorage 받아오기
        const storage: DataStorage = client.loadDataStorage();

        // 클라이언트의 방문횟수를 storage에 저장
        // storage 에서 await 키워드로 VisitCount를 number 명시하기
        let visit_cnt = await storage.get("VisitCount") as number;

        // 방문횟수가 없으면 visit_cnt를 0으로 저장하기
        if (visit_cnt == null) visit_cnt = 0;

        console.log(`[OnJoin] ${player.sessionId}'s visit count : ${visit_cnt}.`);

        // 플레이어의 방문횟수를 갱신하여 sotrage에 저장
        // 방문할 때마다 방문횟수를 갱신할 수 있음
        await storage.set("VisitCount", ++visit_cnt);
    }

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

첫 실행

clientID: 62c924565b43c2d6334e48a3 / sessionID: USER_1deqVbeuRD1 / hashCode: 1VSMNY
[OnJoin] USER_1deqVbeuRD1's visit count : 0.

두번째 실행

clientID: 62c924565b43c2d6334e48a3 / sessionID: USER_sS_HywpceEw / hashCode: 1VSMNY
[OnJoin] USER_sS_HywpceEw's visit count : 1.

순서 3. 클라이언트 코드 작성하기

클라이언트를 관리할 ClientStarter 오브젝트를 생성하고 아래 코드와 같이, 멀티 플레이에 필요한 ZEPETO.World 클래스를 ZepetoWorldMultiplay로 임포트한다.

ZepetoWorldMultiplay는 Zepeto Multiplay 서버의 Room(Game Session) Event를 클라이언트에서 연동할 수 있는 인터페이스를 제공한다.

Room EventListener 목록은 아래와 같다.

image-20220713235011462

그리고 위에서 생성했던 Room 정보(index.ts)를 변수 multiplay에 받아온다.

[ ClientStarter 오브젝트’s ClientStarter.ts ]

// 멀티플레이에 Room 임포트
import { SpawnInfo, ZepetoPlayer, ZepetoPlayers } from 'ZEPETO.Character.Controller';
import { Room } 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 {
    // ZepetoWorldMultiplay 변수를 선언
    // 하이어라키에 생성해둔 WorldMultiPlay 오브젝트를 참조
    public multiplay: ZepetoWorldMultiplay;

    // RoomCreated() 이벤트 리스너를 등록
    private room: Room;

    // 접속된 player 관리를 위해 map형태의 currentPlayers를 정의
    // key값으로 player ID를 저장하기 위해 string과 value값은 Player 객체로 정의
    private currentPlayers: Map<string, Player> = new Map<string, Player>();

    Start() {
        // 전달받은 Room 인자를 this.room에 할당
        this.multiplay.RoomCreated += (room: Room) => {
            this.room = room;
        };
        // RoomJoined() 이벤트 리스너를 등록
        // 해당 Room에 접속되면 Room 인자를 전달
        this.multiplay.RoomJoined += (room: Room) => {
            room.OnStateChange += this.OnStateChange;
        };
    }

    // OnStateChange() 는 RoomJoined() 되었을 때, 처음에만 true이고 이후부터는 False를 유지
    private OnStateChange(state: State, isFirst: boolean) {
        // join 한 플레이어를 관리하기 위해 맵을 생성하여 새로운 변수 join에 할당
        let join = new Map<string, Player>();

        // schema에 저장된 room state의 값을 foreach로 하나씩 조회 
        state.players.ForEach((sessionId: string, player: Player) => {
            if (this.currentPlayers.has(sessionId)) {
                join.set(sessionId, player);
            }
        });

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

    // Room 입장 시 플레이어 이벤트 처리하기
    private OnJoinPlayer(sessionId: string, player: Player) {
        // join한 플레이어의 정보를 출력
        console.log(`[OnJoinPlayer] players - sessionId : ${sessionId}`);

        // Room에 입장한 플레이어를 관리하기 위해 현재 입장한  플레이어를 currentPlayers로 저장
        this.currentPlayers.set(sessionId, player);

        // 입장한 플레이어의 초기 trasnform 설정을 위해 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);

        // 플레이어 인스턴스 생성함수를 호출하기 전에 isLocal 상수를 생성
        // 룸의 세션 ID와 Player의 세션 ID가 일치하면 Local 플레이어로 판별
        const isLocal = this.room.SessionId === player.sessionId;

        // ZepetoPlayers.instance.CreatePlayerWithUser()로 플레이어 인스턴스를 생성
        ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
    }
}

순서 4. ZepetoPlayers 오브젝트 생성 후, 서버로 player state 전송하기

// 멀티플레이에 Room 임포트
import { CharacterState, SpawnInfo, ZepetoPlayer, 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 {
    // ZepetoWorldMultiplay 변수를 선언
    // 하이어라키에 생성해둔 WorldMultiPlay 오브젝트를 참조
    public multiplay: ZepetoWorldMultiplay;

    // RoomCreated() 이벤트 리스너를 등록
    private room: Room;

    // 접속된 player 관리를 위해 map형태의 currentPlayers를 정의
    // key값으로 player ID를 저장하기 위해 string과 value값은 Player 객체로 정의
    private currentPlayers: Map<string, Player> = new Map<string, Player>();

    Start() {
        // 전달받은 Room 인자를 this.room에 할당
        this.multiplay.RoomCreated += (room: Room) => {
            this.room = room;
        };
        // RoomJoined() 이벤트 리스너를 등록
        // 해당 Room에 접속되면 Room 인자를 전달
        this.multiplay.RoomJoined += (room: Room) => {
            room.OnStateChange += this.OnStateChange;
        };
    }

    // OnStateChange() 는 RoomJoined() 되었을 때, 처음에만 true이고 이후부터는 False를 유지
    private OnStateChange(state: State, isFirst: boolean) {
        // Room에 처음 접속했을 때
        if (isFirst) {
            // ZepetoPlayers의 인스턴스에 접근하여 AddListner()를 등록
            // 이 event는 localplayer 인스턴스가 씬에 완전히 로드되었을 때 호출
            ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
                // 상수 myPlayer에 ZepetoPlayers의 인스턴스를 저장
                const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
                // myPlayer의 캐릭터에 접근하면 OnChangedState.AddListner()를 호출
                // 서버 상태가 변경되면 캐릭터 스테이트를 전송하는 함수(.SendState()) 호출
                myPlayer.character.OnChangedState.AddListener((cur, prev) => {
                    this.SendState(cur);
                })
            });
        }

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

        // schema에 저장된 room state의 값을 foreach로 하나씩 조회 
        state.players.ForEach((sessionId: string, player: Player) => {
            if (this.currentPlayers.has(sessionId)) {
                join.set(sessionId, player);
            }
        });

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

    // 서버로 state 전송하기
    private SendState(state: CharacterState) {
        // 서버에 전송할 데이터 타입 RoomData
        const data = new RoomData();
        // 현재 캐릭에 state를 저장
        data.Add("state", state);
        // 서버로 메세지를 전송
        this.room.Send("OnChangedState", data.GetObject());
    }


    // Room 입장 시 플레이어 이벤트 처리하기
    private OnJoinPlayer(sessionId: string, player: Player) {
        // join한 플레이어의 정보를 출력
        console.log(`[OnJoinPlayer] players - sessionId : ${sessionId}`);

        // Room에 입장한 플레이어를 관리하기 위해 현재 입장한  플레이어를 currentPlayers로 저장
        this.currentPlayers.set(sessionId, player);

        // 입장한 플레이어의 초기 trasnform 설정을 위해 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);

        // 플레이어 인스턴스 생성함수를 호출하기 전에 isLocal 상수를 생성
        // 룸의 세션 ID와 Player의 세션 ID가 일치하면 Local 플레이어로 판별
        const isLocal = this.room.SessionId === player.sessionId;

        // ZepetoPlayers.instance.CreatePlayerWithUser()로 플레이어 인스턴스를 생성
        ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
    }
}

순서 5. client 코드로부터 수신된 state를 서버 state에 저장

import { Sandbox, SandboxOptions, SandboxPlayer } from "ZEPETO.Multiplay";
import { DataStorage } from "ZEPETO.Multiplay.DataStorage";
// Schema에서 Player 임포트
import { Player } from "ZEPETO.Multiplay.Schema";


export default class extends Sandbox {

    // Room이 생성되었을 때
    // Client 코드로부터 수신된 메시지를 확인하기 위해
    onCreate(options: SandboxOptions) {
        this.onMessage("onChangedState", (client, message) => {
            const player = this.state.players.get(client.sessionId);
            player.state = message.state;
        })
    }

    // Player가 Room에 입장했을 때
    async onJoin(client: SandboxPlayer) {
        console.log(`clientID: ${client.userId} / sessionID: ${client.sessionId} / hashCode: ${client.hashCode}`);

        // 플레이어를 const로 생성
        const player = new Player();

        // 세션 ID 받아오기
        player.sessionId = client.sessionId;

        // hashcode 받아오기
        if (client.hashCode) {
            player.zepetoHash = client.hashCode;
        }

        // user ID 받아오기
        if (client.userId) {
            player.zepetoUserId = client.userId;
        }

        // 플레이어의 DataStorage 받아오기
        const storage: DataStorage = client.loadDataStorage();

        // 클라이언트의 방문횟수를 storage에 저장
        // storage 에서 await 키워드로 VisitCount를 number 명시하기
        let visit_cnt = await storage.get("VisitCount") as number;

        // 방문횟수가 없으면 visit_cnt를 0으로 저장하기
        if (visit_cnt == null) visit_cnt = 0;

        console.log(`[OnJoin] ${player.sessionId}'s visit count : ${visit_cnt}.`);

        // 플레이어의 방문횟수를 갱신하여 sotrage에 저장
        // 방문할 때마다 방문횟수를 갱신할 수 있음
        await storage.set("VisitCount", ++visit_cnt);

        // 지금까지 플레이어의 정보를 저장
        // 플레이어의 고유 정보(sessionID)와 player객체를 Room state에 저장
        this.state.players.set(client.sessionId, player);
    }

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

댓글남기기