본문 바로가기
IT

[Unity/Zepeto] 제페토 멀티 플레이어 환경 세팅 요약 정리

by 배애앰이 좋아 2023. 6. 7.
반응형

 

제페토 공식 멀티 플레이 환경 세팅 가이드 라인은 아래 링크에서 참고할 수 있으며 아래 글 내용은 유튜브 영상을 요약한 내용입니다. (https://docs.zepeto.me/studio-world/lang-ko/docs/multiplay_tutorial)

 

Multiplay Tutorial

Sample Project📘Multiplay Samplehttps://github.com/naverz/zepeto-multiplay-example Summary SummaryFrom creating a Multiplay server to a client, set up the environment needed to develop a Multiplay World.DifficultyIntermediateTime Required1 Hour Part 1. I

docs.zepeto.me

 

1. mutiplayer sever 와 zepetoplayers 추가

 

 

2. public open world seeting 에서 본인 world id 바꿔주기 (com.basicdevelop.test) 또한, 가로, 세로 방향 지정해주기

 

 

아래 이미지를 통해 가로 세로 방향 지정 참고

 

 

3. WorldMutiplay 와 zepeto world mutiplay script 추가 해주기

 

 

4. schema type 추가하기

 

 

5. index.ts 작성해주기 

 

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

export default class extends Sandbox {

    // room 이 생성될 때 1회 호출되면 room 에 대한 초기화 로직을 추가할 수 있다.
    async onCreate(options: SandboxOptions) { 
        // 리스너라서 계속해서 반응해줌
        this.onMessage("onChangedTransform", (client, message) => { 
            const player = this.state.players.get(client.sessionId);

            const transform = new Transform();
            transform.position = new Vector3();
            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;

            if(player){
                player.transform = transform;   
            }
        });

        this.onMessage("onChangedState", (client, message) => { // 서버로 전송된 메세지 값
            const player = this.state.players.get(client.sessionId);
            if(player){
                player.state = message.state;
            }
        });

    }

    // client가 room에 입장할 때 호출된다. 
    // client id, charater info 가 sandboxplayer 에 포함되어 있다.
    // charater 초기화
    async onJoin(client: SandboxPlayer) {

        console.log(`on join ${client.sessionId}, HashCode ${client.hashCode}, UserId ${client.userId},`)

        const player = new Player(); // player 정보 확인
        player.sessionId = client.sessionId;
        if(client.hashCode){
            player.zepetoHash = client.hashCode;
        }
        if(client.userId){
            player.zepetoUerId = client.userId;
        } // 및 설정

        player.stat.speed = 0;
        player.stat.jump = 0;
        const storage : DataStorage = client.loadDataStorage(); // 플레이어 정보를 서버에 저장
        let visit_cnt = await storage.get("VisitCount") as number; // 방문 횟수 카운트

        //함수를 선언 할 때 함수의 앞부분에 async 키워드 그리고 Promise 의 앞부분에 await 을 넣어주면 
        // 해당 프로미스가 끝날때까지 기다렸다가 다음 작업을 수행 할 수 있습니다.
        if(visit_cnt == null) visit_cnt = 0; // 초기값 없음

        console.log(`[Onjoin] ${client.sessionId}'s visiting count : ${visit_cnt}`);
        await storage.set("VisitCount", ++visit_cnt); // 값 갱신 및 저장

        let raw_speed = await storage.get("CharaterSpeed") as number;
        let raw_jump = await storage.get("CharaterJump") as number;
        if(raw_speed == null){
            raw_speed = 0;
        }
        if(raw_jump == null){
            raw_jump = 0;
        }
        player.stat.speed = raw_speed;
        player.stat.jump = raw_jump;
        console.log(`join CharaterSpeed : ${player.stat.speed}  CharaterJump : ${player.stat.jump}`);
        this.state.players.set(client.sessionId, player); // 플레이어 객체 저장
    }

    // client 가 room 에 퇴장할 때 호출된다
    // client가 연결 해체를 요청한 경우 consented 값이 true 이면 그렇지 않으면 false
    async onLeave(client: SandboxPlayer, consented?: boolean) {
        const storage : DataStorage = client.loadDataStorage();
        const player = this.state.players.get(client.sessionId);
        if(player){
            //console.log(`speed save : ${player.stat.speed}`);
            await storage.set("CharaterSpeed", player.stat.speed);
            await storage.set("CharaterJump", player.stat.jump);
            console.log(`CharaterSpeed : ${player.stat.speed}  CharaterJump : ${player.stat.jump}`);
        }
        // 플레이어별로 map 지워주기 delete
        this.state.players.delete(client.sessionId);
    }

    onPurchased(client : SandboxPlayer, receipt : IReceiptMessage){
        console.log(`Onpurchased`);
        const player = this.state.players.get(client.sessionId);
        if(player)
        {
            if(receipt.itemId == "potion.speed"){
                player.stat.speed += 3;
                console.log('player speed +');
            }
            if(receipt.itemId == "potion.jump"){
                player.stat.jump += 3;
                console.log('player jump +');
            }
        }
    }

    // sandboxoptions 에서 설정된 tickInterval 마다 반복적으로 호출되며 각종 Interval 이벤트 관리
    onTick(deltTime : number){
        //this.state.time += 0.1;
    }
}

 

5. ClientStarter 오브젝트 만들어주기

 

 

 

6. ClientStarter 오브젝트에 ClientStarter.ts 스크립트 추가

 

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

export default class ClientStarter extends ZepetoScriptBehaviour {

    public mutiplay : ZepetoWorldMultiplay;
    // room eventlistener list
    // roomcreated(room) room 생성되고 접속 가능할 때 호출
    // roomjoined(room) room 접속되면 호출
    public room : Room;
    private currentPlayers:Map<string,Player> = new Map<string, Player>();

    public noticePopup : Image;
    private player : Player;
    private zepetoPlayer : ZepetoPlayer;

    private static Instance : ClientStarter;

    public static GetInstance() : ClientStarter{
        if(!ClientStarter.Instance){
            const targetObj = UnityEngine.GameObject.Find("ClientStarter");
            if(targetObj){
                ClientStarter.Instance = targetObj.GetComponent<ClientStarter>();
            }
        }
        return ClientStarter.Instance;
    }

    private Awake(){
        this.noticePopup.gameObject.SetActive(false);
    }

    Start() {    
        this.mutiplay.RoomCreated += (room:Room) => {
            this.room = room;
        };
        this.mutiplay.RoomJoined += (room:Room) => {
            room.OnStateChange += this.OnStateChange;
        };
        this.StartCoroutine(this.SendMessageLoop(0.1));
    }

    private * SendMessageLoop(tick:number){
        while(true){
            yield new UnityEngine.WaitForSeconds(tick);
            if(this.room != null && this.room.IsConnected){
                const hasPlayer = ZepetoPlayers.instance.HasPlayer(this.room.SessionId);
                if(hasPlayer){
                    const myPlayer = ZepetoPlayers.instance.GetPlayer(this.room.SessionId);
                    // 캐릭터가 움직이고 있는 경우
                    if (myPlayer.character.CurrentState != CharacterState.Idle) {
                        this.SendTransform(myPlayer.character.transform);
                    }
                }
            }
        }
    }

    private SendTransform(transform : UnityEngine.Transform){
        const data = new 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());

        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());

        this.room.Send("onChangedTransform", data.GetObject()); // 서버로 데이터 보냄
    }

    private OnStateChange(state: State, isFirst:boolean){
        if(isFirst){ // 처음에만 등록하면 됨
            ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(()=> { // localplayer scene에 완전히 생성될 때 호출
                const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
                this.zepetoPlayer = myPlayer;
                myPlayer.character.tag = "Player";
                myPlayer.character.OnChangedState.AddListener((cur, prev) => {
                    this.SendState(cur); // 서버로 전송
                })
            });
            ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId : string) => {
                const isLocal = this.room.SessionId === sessionId;
                if(!isLocal){
                    const player : Player = this.currentPlayers.get(sessionId);
                    player.OnChange += (ChangedValue) => this.OnUpdatePlayer(sessionId, player);
                }
            }); 
        }
        let join = new Map<string, Player>();
        let leave = new Map<string, Player>(this.currentPlayers);
        state.players.ForEach((sessionId:string, player:Player) => {
            if(!this.currentPlayers.has(sessionId)) // true 일시 지금 입장한 플레이어
            {
                join.set(sessionId, player);
            }
            leave.delete(sessionId);
        });
        join.forEach((player:Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
        leave.forEach((player : Player, sessionId : string) => this.OnLeavePlayer(sessionId, player))

        if(this.zepetoPlayer != null){
            this.zepetoPlayer.character.additionalWalkSpeed = this.player.stat.speed;
            this.zepetoPlayer.character.additionalRunSpeed = this.player.stat.speed;
            this.zepetoPlayer.character.additionalJumpPower = this.player.stat.jump;
        }
    }

    private SendState(state:CharacterState){ // 서버로 전송
        const data = new RoomData();
        data.Add("state", state); // 현재 캐릭터의 state 추가
        this.room.Send("onChangedState", data.GetObject()); // 서버로 메세지를 송신하는 함수
    }

    private OnUpdatePlayer(sessionId:string, player : Player){
        // 월드 로직 작성하기 2 8분 28초
        // vector.distance 클라이언트 위치 , 서버 위치 2, 3 
        const position = this.ParseVector3(player.transform.position);
        const rotation = player.transform.rotation;
        const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);
        zepetoPlayer.character.MoveToPosition(position);
        //zepetoPlayer.character.tag = "Player";
        if(player.state === CharacterState.Jump || player.state == 104 ){ // || player.state == 106
            zepetoPlayer.character.Jump();
        }
        if (UnityEngine.Vector3.Distance(zepetoPlayer.character.transform.position, position) > 2) {
            zepetoPlayer.character.transform.position = position;
            zepetoPlayer.character.transform.rotation = UnityEngine.Quaternion.Euler(rotation.x, rotation.y, rotation.z);
        }

    }

    // 플레이어가 방에 들어올 때
    private OnJoinPlayer(sessionId : string, player : Player){
        //(`join sessionId : ${sessionId}`);
        this.currentPlayers.set(sessionId, player); // currentPlayers 등록
        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);

        const isLocal = this.room.SessionId === player.sessionId;
        if(isLocal){
            this.player = player;
        }
        ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUerId, spawnInfo, isLocal);
    }

    private OnJoinItem(itemIndex : string, player : Player){
        //console.log(`join itemIndex : ${itemIndex}`);

    }

    private OnLeavePlayer(sessionId : string, player:Player){
        //console.log(`leave player ${sessionId}`);
        this.currentPlayers.delete(sessionId);
        ZepetoPlayers.instance.RemovePlayer(sessionId);
    }

    private ParseVector3(vector3 : Vector3):UnityEngine.Vector3{
        return new UnityEngine.Vector3(
            vector3.x,
            vector3.y,
            vector3.z
        );
    }

    // 구매된 아이템 이름을 받아옴
    OnPurchaseComplete(item){
        console.log(`OnPurchaseComplete`);
        if(item.itemId == "potion.speed"){
            this.StartCoroutine(this.ShowNoticePopup());
        }
        if(item.itemId == "potion.jump"){
            this.StartCoroutine(this.ShowNoticePopup());
        }
    }

    private * ShowNoticePopup(){
        this.noticePopup.gameObject.SetActive(true);
        yield new UnityEngine.WaitForSeconds(0.7);
        this.noticePopup.gameObject.SetActive(false);
    }

}

 

7. public 안 뜰 때 Ctrl + R 누르기 또는 Assets -> Refresh 또는 스크립트 일부 수정

 

 

8. 큐브 생성해서 플레이어 바닥 만들어주기

 

 

9. 중간에 생긴 오류

 

ystem.Exception: (node:12984) UnhandledPromiseRejectionWarning: Error: [tsl] ERROR
      TS2688: Cannot find type definition file for './index.d.ts'.

 

위와 같은 오류가 생겼을 시, 다음과 사진과 같이 World.multiplay 폴더 안에 MultiplayTypes 폴더 생성하고 MultiplayTypes 폴더로 index.d 파일 옮겨주기 

 

 

이후, World.multiplay 폴더에 있는 tsconfig.json 파일에 방금 추가한 폴더 추가

 

 

10. muti sever 키고 게임 실행하기 or QR 생성하기

 

 

위의 실행 결과 사진 : 잘 실행됨!

해당 내용을 더 자세히 알고 싶다면 참고하면 좋은 사이트 : https://velog.io/@jupiter6676/%EA%B3%B5%EB%AA%A8%EC%A0%84-%EC%9D%BC%EC%A7%8011-%EB%A9%80%ED%8B%B0%ED%94%8C%EB%A0%88%EC%9D%B4

 

공모전 일지11 - 멀티플레이

230501

velog.io

 

함수별로 설명을 잘 적어서 이해하기 더 좋은 것 같습니다!

 

+ 다른 플레이어 왔다갔다 거리는 버그 수정 스크립트

 

import { GameObject } from 'UnityEngine';
import * as UnityEngine from 'UnityEngine'
import { WaitForSeconds } from 'UnityEngine';
import { CharacterJumpState, SpawnInfo, ZepetoCharacter } from 'ZEPETO.Character.Controller';
import { CharacterState, LocalPlayer, ZepetoPlayer, ZepetoPlayers } from 'ZEPETO.Character.Controller';
import { Room, RoomData } from 'ZEPETO.Multiplay';
import { Player, State, Vector3 } from 'ZEPETO.Multiplay.Schema';
import { ZepetoScriptBehaviour } from 'ZEPETO.Script'
import { ZepetoWorldMultiplay } from 'ZEPETO.World';
import { MapSchema } from '@colyseus/schema';

export default class ClientStarter2 extends ZepetoScriptBehaviour {

    // Multiplay 모듈
    @SerializeField()
    private multiplay : ZepetoWorldMultiplay;

    // const values
    private SEND_MESSAGE_LOOP_DURATION : number = 0.04;

    // values
    private room : Room = null;
    @NonSerialized()
    public currentPlayers:Map<string, Player> = new Map<string, Player>();
    @NonSerialized()
    public currentZepetoCharacterRef:Map<string, ZepetoCharacter> = new Map<string, ZepetoCharacter>();
    @NonSerialized()
    public player : Player;
    private zepetoPlayer : ZepetoPlayer;
    // 캐릭터 위치 차이가 이 값을 초과하면 텔레포트
    private allowablePosDiff : number = 3;

    // zepeto values
    private mLocalPlayer : LocalPlayer = null;
    public get LocalPlayer() { return this.mLocalPlayer; }
    private mTouchController : GameObject = null;
    public get TouchController() { return this.mTouchController; }

    private isHost : boolean = false;

    // timers
    private waitForSendMessageLoop : WaitForSeconds = new WaitForSeconds(this.SEND_MESSAGE_LOOP_DURATION);

    /* Singleton */
    private static Instance: ClientStarter2;
    public static GetInstance(): ClientStarter2 {
        if (!ClientStarter2.Instance) {
            const targetObj = GameObject.Find("ClientStarter");
            if (targetObj)
            ClientStarter2.Instance = targetObj.GetComponent<ClientStarter2>();
        }
        return ClientStarter2.Instance;
    }

    Start() {    
        this.multiplay.RoomCreated += (room:Room) => {
            this.room = room;
            // request trash game start tutorial saw
            room.AddMessageHandler<boolean>("Init", message => {
                this.isHost = message;
            });
        }
        this.multiplay.RoomJoined += (room:Room) => {
            // on state change
            room.OnStateChange += this.OnStateChange;
        }
        this.StartCoroutine(this.SendMessageLoop());
    }

    // Player 상태를 지속적으로 서버로 전송 1, 0.04초
    private * SendMessageLoop() {
        while(true){
            yield this.waitForSendMessageLoop;
            if(this.room != null && this.room.IsConnected) {
                const hasPlayer = ZepetoPlayers.instance.HasPlayer(this.room.SessionId);
                if(hasPlayer) {
                    const mPlayer = ZepetoPlayers.instance.GetPlayer(this.room.SessionId);
                    this.SendTransform(mPlayer.character.transform);
                    this.SendState(mPlayer.character.CurrentState);
                }
            }
        }
    }

    // 실제로 Player 위치 정보를 서버로 보내는 함수
    private SendTransform(transform:UnityEngine.Transform) {
        const data = new 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());
        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());
        this.room.Send("OnChangedTransform", data.GetObject());
    }

    // Player 상태를 실제로 서버에 전송하는 함수
    // 주로 점프 및 제스쳐
    private SendState(state:CharacterState) {
        const data = new RoomData();
        // 기본 state
        data.Add("state", state);
        if(state === CharacterState.Jump) { 
            if(typeof this.zepetoPlayer.character.MotionV2.CurrentJumpState === "number") {
                data.Add("subState", this.zepetoPlayer.character.MotionV2.CurrentJumpState);
            }
            else {
                data.Add("subState", 0);
            }
        }
        // 메세지 보냄
        this.room.Send("OnChangedState", data.GetObject());
    }

    // 서버에 처음 접속 시에도 한번 호출
    // 그 이후 서버 State가 변경될 경우 호출
    private OnStateChange(state:State, isFirst:boolean) {
        if(isFirst) {
            // LocalPlayer 추가
            ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
                const mPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
                this.zepetoPlayer = mPlayer;
                // set zepeto values
                this.mLocalPlayer = ZepetoPlayers.instance.LocalPlayer;
                this.mTouchController = ZepetoPlayers.instance.gameObject.transform.Find("UIZepetoPlayerControl").gameObject;
                this.currentZepetoCharacterRef.set(this.room.SessionId, mPlayer.character);
            });
            // 다른 Player 추가
            ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId:string) => {
                const isLocal = this.room.SessionId === sessionId;
                if(!isLocal) {
                    const player : Player = this.currentPlayers.get(sessionId);
                    const mPlayer : ZepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);
                    player.OnChange += (ChangeValues) => this.OnUpdatePlayer(sessionId, player);
                    this.currentZepetoCharacterRef.set(sessionId, mPlayer.character);
                }
            });
        }

        let join = new Map<string, Player>();
        let leave = new Map<string, Player>(this.currentPlayers);

        state.players.ForEach((sessionId:string, player:Player) => {
            if(!this.currentPlayers.has(sessionId)) {
                join.set(sessionId, player);
            }
            leave.delete(sessionId);
        });

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

    private OnUpdatePlayer(sessionId:string, player:Player) {
        const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);
        const position = this.ParseVector3(player.transform.position);
        const rotation = player.transform.rotation;
        
        //zepetoPlayer.character.transform.rotation = UnityEngine.Quaternion.Euler(rotation.x, rotation.y, rotation.z);
        var moveDir = UnityEngine.Vector3.op_Subtraction(position, zepetoPlayer.character.transform.position);
        // 기본
        moveDir = new UnityEngine.Vector3(moveDir.x, 0, moveDir.z);
        if (moveDir.magnitude < 0.05) {
            if (player.state === CharacterState.MoveTurn)
                return;
            zepetoPlayer.character.StopMoving();
        } else {
            zepetoPlayer.character.MoveContinuously(moveDir);
        }
        // 기본 state
        if (player.state === CharacterState.Jump) {
            if (zepetoPlayer.character.CurrentState !== CharacterState.Jump) {
                zepetoPlayer.character.Jump();
            }

            if (player.subState === CharacterJumpState.JumpDouble) {
                zepetoPlayer.character.DoubleJump();
            }
        }
        // Scene에서의 캐릭터의 위치와 서버에서의 캐릭터 위치가 허용값 보다 많이 차이날 경우 Teleport
        if (UnityEngine.Vector3.Distance(zepetoPlayer.character.transform.position, position) > this.allowablePosDiff) {
            zepetoPlayer.character.transform.position = position;
            zepetoPlayer.character.transform.rotation = UnityEngine.Quaternion.Euler(rotation.x, rotation.y, rotation.z);
        }
    }

    private ParseVector3(vector3:Vector3):UnityEngine.Vector3 {
        return new UnityEngine.Vector3(
            vector3.x,
            vector3.y,
            vector3.z
        );
    }

    private OnJoinPlayer(sessionId:string, player:Player) {
        console.log(`[OnJoinPlayer] players - sessionId : ${sessionId}`);
        this.currentPlayers.set(sessionId, player);
        const spawnInfo:SpawnInfo = new SpawnInfo();
        const rotation:UnityEngine.Quaternion = this.gameObject.transform.rotation;
        const isLocal = this.room.SessionId === player.sessionId;
        spawnInfo.position = this.gameObject.transform.position;
        spawnInfo.rotation = rotation;
        if(isLocal) {
            this.player = player;
        }
        ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUerId, spawnInfo, isLocal);
    }

    private OnLeavePlayer(sessionId:string, player:Player) {
        console.log(`[OnRemove] players = sessionId : ${sessionId}`);
        this.currentPlayers.delete(sessionId);
        this.currentZepetoCharacterRef.delete(sessionId);
        ZepetoPlayers.instance.RemovePlayer(sessionId);
    }
}
반응형

댓글