---
title: ผู้เล่น ZEPETO
slug: world-sdk-guide-th/XjzG-zepeto
docTags: 
createdAt: 2024-09-02T08:34:13.135Z
---

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-rPUExlbxRfP5EpV-dcWfD-20240904-102423.png)

- **ZepetoPlayers** เป็นคลาสผู้จัดการ (Singleton) ที่ออกแบบมาเพื่อควบคุมทั้ง ZEPETO Player และ ZEPETO Character.
  - โดยการเพิ่ม ZepetoPlayers ลงในฉาก คุณสามารถสร้าง ลบ และจัดการ ZEPETO Player ได้.
- **ZepetoPlayer** แทนตัวอย่างเฉพาะของตัวละคร ZEPETO ที่ใช้ในการจัดการทั้งผู้เล่นที่คุณควบคุมโดยตรงในโลกหลายผู้เล่นและผู้เล่นคนอื่น ๆ.
  - ทุก ZepetoPlayer ที่สร้างขึ้นในโลกหลายผู้เล่นจะถูกกำหนดให้มี ID เซสชันที่ไม่ซ้ำกันและจะถูกจัดการโดยใช้ ID เซสชันนี้.
  - เนื่องจาก ZepetoPlayer มีคุณสมบัติของ ZepetoCharacter มันจึงสามารถควบคุมได้โดยใช้ฟังก์ชันที่เกี่ยวข้องกับ ZepetoCharacter.
  - มี ZepetoPlayer สามประเภทคือ:

| **ZepetoPlayer**                  | **คำอธิบาย**                                                                                                                                                                                                                                                                     |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ผู้เล่นท้องถิ่น                   | แสดงถึงอินสแตนซ์ตัวละคร ZEPETO ที่ถูกควบคุมโดยผู้ใช้ท้องถิ่นโดยตรง。<br /><br />- ติดตั้งด้วย Character Controller/Zepeto Camera components.                                                                                                                                      |
| ผู้เล่นเครือข่าย (ผู้เล่นระยะไกล) | อินสแตนซ์ตัวละคร ZEPETO ที่สามารถโหลดและใช้งานในเนื้อหาหลายผู้เล่น。<br /><br />- ไม่มีการติดตั้ง Character Controller/Zepeto Camera components.                                                                                                                                  |
| ผู้เล่นบอท                        | อินสแตนซ์ตัวละคร ZEPETO สำหรับเนื้อหาหลายผู้เล่น แต่ถูกควบคุมโดยบอทแทนที่จะเป็นผู้ใช้จริง。<br /><br />ใช้เป็นตัวแทนเมื่อเริ่มโลกหลายผู้เล่นที่มีผู้เล่นไม่เพียงพอหรือหากผู้เล่นออกจากเกมระหว่างเล่น。<br /><br />- ไม่มีการติดตั้ง Character Controller/Zepeto Camera components. |

:::hint{type="info"}
- โปรดดูคู่มือต่อไปนี้.
  - \[[ผู้เล่น ZEPETO](docId\:QEI1CLrlDK5LxXgpv1yQu)]
  - \[[ไกด์การสร้างผู้เล่นบอท](docId\:rHeAB2yZN4tlFO-R7RdpT)]
:::

- **ZepetoCharacter** เป็นหน่วยตัวอย่างพื้นฐานของตัวละคร ZEPETO ที่สามารถโหลดและควบคุมใน World Scene ได้.
  - ZepetoCharacter มีลักษณะเป็นอวตารที่สร้างขึ้นผ่านแอป ZEPETO.

:::hint{type="info"}
* กรุณาอ้างอิงไปยังคู่มือต่อไปนี้. \[[ตัวละคร ZEPETO](docId\:msDX6mZFJEuB38vBDzbjN)]
:::

## การเพิ่ม ZepetoPlayers

ในหน้าต่าง Hierarchy ให้เลือก ZEPETO → แท็บ ZepetoPlayers.

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-DJHFPqxeTgiSZHqx5hZPS-20240904-102423.png" size="52" width="321" height="473" position="center" showCaption="false"}

คุณสามารถเพิ่มมันลงในฉากของคุณได้แล้ว

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-oz5MesLYjuesDEKW_eW6u-20240904-102423.png)

โปรดทราบว่าการเพิ่ม ZepetoPlayers เพียงอย่างเดียวจะไม่ทำให้ Zepeto Player เข้ามาในฉาก คุณต้องดำเนินการเขียนสคริปต์โดยใช้ API การสร้างตัวละครของ ZepetoPlayers

หากคุณต้องการสร้างและลองใช้ Local Player ในฉากอย่างรวดเร็ว โปรดดูคู่มือ:

:::hint{type="info"}
- โปรดดูคู่มือดังต่อไปนี้ \[[การสร้างตัวละคร Zepeto](docId\:MRnK9T5XnrdQCVMsgFgpk)]
:::



สำหรับตัวอย่างวิธีการแสดงชื่อและรูปโปรไฟล์ของผู้เล่น Zepeto โปรดดูที่คู่มือ:

:::hint{type="info"}
- โปรดดูที่คู่มือต่อไปนี้. \[[ข้อมูลผู้ใช้](docId\:CL33FgnsabaFjr7nprFYi)]
:::



## API ผู้เล่น ZEPETO

หากคุณสนใจใน API ของ ZepetoPlayers โปรดดูที่เอกสาร:

:::hint{type="info"}
- โปรดดูที่คู่มือต่อไปนี้. \[[API ZEPETO.Character.Controller](https://developer.zepeto.me/docs/character-controller/)]
:::



คู่มือนี้ครอบคลุมตัวอย่างการใช้ผู้เล่น ZEPETO ในสถานการณ์หลายผู้เล่นเป็นหลัก.



## การซิงโครไนซ์ตำแหน่งหลายผู้เล่นโดยใช้ผู้เล่น ZEPETO

ในโลกผู้เล่นคนเดียว จะต้องสร้างผู้เล่นท้องถิ่นเพียงคนเดียว และเนื่องจากมีเพียงผู้เล่นท้องถิ่นที่ปรากฏบนหน้าจอ การซิงโครไนซ์จึงไม่จำเป็น.

อย่างไรก็ตาม ในโลกหลายผู้เล่น ไม่เพียงแต่ผู้เล่นท้องถิ่นที่คุณควบคุมโดยตรง แต่ยังมีผู้เล่นอื่น ๆ ที่เรียกว่าผู้เล่นเครือข่าย ต้องแสดงบนหน้าจอด้วย.

ทุกการกระทำของผู้เล่นเครือข่าย - การเคลื่อนที่, การกระโดด, และการทำท่าทางเฉพาะ - ควรปรากฏเหมือนกันบนหน้าจอของคุณ.

กระบวนการนี้เรียกว่าการซิงโครไนซ์.

หากไม่ดำเนินการตามสคริปต์การซิงโครไนซ์ คุณจะไม่สามารถเห็นการปรากฏตัวหรือการเคลื่อนไหวของผู้เล่นในเครือข่ายได้

ในโลกที่มีการเล่นหลายคนโดยไม่มีการซิงโครไนซ์ วิธีเดียวที่จะรู้ว่าลูกค้าอื่นได้เข้ามาในห้องคือผ่านปุ่มโฮม

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-YxuI0_JNsH8BOnvarghc0-20240904-102424.gif "การปรากฏเมื่อมีการตั้งค่าการเล่นหลายคนเท่านั้นโดยไม่มีการสร้างตัวละครและสคริปต์การซิงโครไนซ์")



### ขั้นตอนที่ 1 : การตั้งค่าสภาพแวดล้อมการเล่นหลายคน

แนะนำให้เริ่มต้นด้วยการเข้าใจการตั้งค่าและแนวคิดพื้นฐานของการเล่นหลายคนผ่านวิดีโอสอนการเล่นหลายคน

:::hint{type="info"}
- กรุณาอ้างอิงไปยังคู่มือต่อไปนี้
  - \[[บทเรียนการเล่นหลายคน](docId\:bFzI_crxT7X2ujbo1ZNog) ]
  - \[[มัลติเพลย์](docId:-KQGjBFq_6Znt_AwBMNAc)]
:::



### ขั้นตอนที่ 2 : แสดงผู้เล่นคนอื่นบนหน้าจอของคุณ

ผู้เล่นท้องถิ่นของคุณจะถูกมองว่าเป็นผู้เล่นในเครือข่ายบนอุปกรณ์ของคนอื่น

หมายความว่าผู้เล่นท้องถิ่นของคุณต้องส่งข้อมูลไปยังเซิร์ฟเวอร์เพื่อการซิงโครไนซ์

ผู้เล่นทั้งหมดที่เชื่อมต่อกับโลกการเล่นหลายคนควรแชร์ข้อมูลของพวกเขา

ลูกค้าทั้งหมดที่เชื่อมต่อกับห้องเล่นหลายคนจะแชร์ข้อมูลสถานะของห้องเล่นหลายคน

ข้อมูลสถานะของห้องนี้จะปฏิบัติตามสคีมาที่กำหนดใน Schemas.json

(โปรดพิจารณา Schema เป็นโครงสร้างข้อมูล)

ในคู่มือนี้ คุณจะซิงโครไนซ์ตำแหน่งของผู้เล่นผ่านข้อมูลสถานะห้อง ดังนั้น ให้กำหนด Schema ใน Schemas.json ที่สามารถแสดงข้อมูลตำแหน่งสำหรับผู้เล่นแต่ละคนได้

:::CodeblockTabs
schema.json

```json
{
"State" : {"players" : {"map" : "Player"}},
"Player" : {"sessionId" : "string","zepetoUserId" : "string","transform" : "Transform","state" : "number","subState" : "number"},
"Transform" : {"position" : "Vector3","rotation" : "Vector3"},
"Vector3" : {"x" : "number","y" : "number","z" : "number"}
}
```
:::

:::hint{type="info"}
- โปรดดูคู่มือต่อไปนี้ \[[สถานะห้องเล่นหลายคน](docId\:blqQVdRFy2e3przAdo-DP) ]
:::



:::hint{type="success"}
**👍 เคล็ดลับ**

- เก็บข้อมูลทั้งหมดที่เซิร์ฟเวอร์และลูกค้าทั้งหมดควรแชร์ใน **สถานะห้องหลายผู้เล่น.**
- บันทึกข้อมูลเฉพาะสำหรับผู้เล่นแต่ละคน เช่น ระดับ, คะแนนประสบการณ์, คะแนน, ฯลฯ โดยใช้ **DataStorage.**
:::



สคริปต์เซิร์ฟเวอร์สามารถรับรู้เมื่อผู้เล่นคนอื่นเข้ามาในห้องและสามารถส่งข้อมูลนั้นไปยังไคลเอนต์เพื่อโหลดผู้เล่นคนนั้นเข้าสู่ฉากได้



### ขั้นตอน 2-1 : สคริปต์เซิร์ฟเวอร์พื้นฐาน

เมื่อผู้เล่นเข้ามาในห้องของโลกหลายผู้เล่น ฟังก์ชัน onJoin() จะถูกเรียกใช้

ในสคริปต์เซิร์ฟเวอร์ ให้เพิ่มข้อมูลของผู้เล่นที่เชื่อมต่อไปยังสถานะห้องของผู้เล่น



นอกจากนี้ เมื่อผู้เล่นออกจากห้อง ฟังก์ชัน onLeave() จะถูกเรียกใช้

สคริปต์เซิร์ฟเวอร์จะลบข้อมูลของผู้เล่นที่ออกจากผู้เล่นในสถานะห้อง.

```typescript
import {Sandbox, SandboxOptions, SandboxPlayer} from "ZEPETO.Multiplay";
import {Player} from "ZEPETO.Multiplay.Schema";
 
export default class extends Sandbox {
 
 
    onCreate(options: SandboxOptions) {
 
    }
 
 
    async onJoin(client: SandboxPlayer) {
        const player = new Player();
        player.sessionId = client.sessionId;
 
        if (client.userId) {
            player.zepetoUserId = client.userId;
        }
 
 
 
        // จัดการวัตถุผู้เล่นโดยใช้ sessionId ซึ่งเป็นค่าคีย์ที่ไม่ซ้ำกันของวัตถุไคลเอนต์
        // ไคลเอนต์สามารถตรวจสอบข้อมูลเกี่ยวกับวัตถุผู้เล่นที่เพิ่มโดยการตั้งค่าโดยการเพิ่มเหตุการณ์ add_OnAdd ไปยังวัตถุผู้เล่น
        this.state.players.set(client.sessionId, player);
    }
 
    async onLeave(client: SandboxPlayer, consented?: boolean) {
 
        this.state.players.delete(client.sessionId);
    }
}
```



### ขั้นตอน 2-2 : สคริปต์ไคลเอนต์พื้นฐาน

เมื่อสร้างโลกแบบหลายผู้เล่น จะต้องมีสคริปต์ไคลเอนต์เพื่อสื่อสารกับเซิร์ฟเวอร์.

ด้านล่างนี้คือตัวอย่างของสคริปต์ไคลเอนต์พื้นฐานสำหรับการเล่นแบบหลายผู้เล่น.

ในด้านไคลเอนต์ เราจัดการข้อมูลผู้เล่นที่จะถูกแสดงในไคลเอนต์ของเราโดยใช้ `currentPlayers` โครงสร้างข้อมูลแผนที่.



บรรทัดสำคัญของโค้ดแสดงด้านล่างนี้:

`ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal); `

เมื่อไคลเอนต์ได้รับข้อมูลผู้เล่นจากสถานะห้องของเซิร์ฟเวอร์และผู้เล่นใหม่เข้าร่วมห้อง จะมีการกำหนด ID เซสชัน หาก ID เซสชันของผู้เล่นที่กำลังสร้างตรงกับ ID เซสชันของเรา ผู้เล่นจะถือว่าเป็นผู้เล่นท้องถิ่น ในกรณีนี้ ผู้เล่นจะถูกสร้างขึ้นด้วย `isLocal = true`, ซึ่งบ่งชี้ว่านี่คือผู้เล่นท้องถิ่น.

จากนั้น ผู้เล่นที่ไม่ใช่ท้องถิ่นจะถูกสร้างขึ้นด้วย `isLocal = false`. สิ่งนี้ทำให้แน่ใจว่าการปรากฏตัวของผู้เล่นทั้งหมด ทั้งท้องถิ่นและไม่ท้องถิ่น จะถูกแสดงบนหน้าจอ

```typescript
import {ZepetoScriptBehaviour} from 'ZEPETO.Script'
import {ZepetoWorldMultiplay} from 'ZEPETO.World'
import {Room, RoomData} from 'ZEPETO.Multiplay'
import {Player, State, Vector3} from 'ZEPETO.Multiplay.Schema'
import {CharacterState, SpawnInfo, ZepetoPlayers, ZepetoPlayer, CharacterJumpState} from 'ZEPETO.Character.Controller'
import * as UnityEngine from "UnityEngine";
 
 
export default class MultiplayClientCode extends ZepetoScriptBehaviour {
 
    public multiplay: ZepetoWorldMultiplay;
 
    private room: Room;
    private currentPlayers: Map<string, Player> = new Map<string, Player>();
 
    private zepetoPlayer: ZepetoPlayer;
 
    private Start() {
 
        this.multiplay.RoomCreated += (room: Room) => {
            this.room = room;
        };
 
        this.multiplay.RoomJoined += (room: Room) => {
            room.OnStateChange += this.OnStateChange;
        };
 
    }
 
 
    private OnStateChange(state: State, isFirst: boolean) {
 
        // เมื่อได้รับเหตุการณ์ OnStateChange ครั้งแรก จะมีการบันทึกภาพรวมของสถานะทั้งหมด
        if (isFirst) {
 
            // [CharacterController] (ท้องถิ่น) ถูกเรียกเมื่ออินสแตนซ์ของผู้เล่นถูกโหลดอย่างสมบูรณ์ในฉาก
            ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
                const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
                this.zepetoPlayer = myPlayer;
            });
 
            // [CharacterController] (ท้องถิ่น) ถูกเรียกเมื่ออินสแตนซ์ของผู้เล่นถูกโหลดอย่างสมบูรณ์ในฉาก
            ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
                const isLocal = this.room.SessionId === sessionId;
                if (!isLocal) {
                    const player: Player = this.currentPlayers.get(sessionId);
 
                }
            });
        }
 
        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);
        });
 
        // [RoomState] สร้างอินสแตนซ์ของผู้เล่นสำหรับผู้เล่นที่เข้ามาในห้อง
        join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
 
        // [RoomState] ลบอินสแตนซ์ของผู้เล่นสำหรับผู้เล่นที่ออกจากห้อง
        leave.forEach((player: Player, sessionId: string) => this.OnLeavePlayer(sessionId, player));
    }
 
    private OnJoinPlayer(sessionId: string, player: Player) {
        console.log(`[OnJoinPlayer] ผู้เล่น - sessionId : ${sessionId}`);
        this.currentPlayers.set(sessionId, player);
         
        // สร้างผู้เล่น
        const isLocal = this.room.SessionId === player.sessionId;
        ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, new SpawnInfo(), isLocal);
    }
 
    private OnLeavePlayer(sessionId: string, player: Player) {
        console.log(`[OnRemove] ผู้เล่น - sessionId : ${sessionId}`);
        this.currentPlayers.delete(sessionId);
 
        ZepetoPlayers.instance.RemovePlayer(sessionId);
    }
}

```



ตอนนี้ เมื่อผู้เล่นเข้ามา คุณสามารถยืนยันได้ว่าตัวละคร ZEPETO ถูกสร้างขึ้นบนหน้าจอของคุณแล้ว.

แต่การเคลื่อนไหวของผู้เล่นยังไม่สะท้อนบนหน้าจอ.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-r5QeR4wNpnwDW3puaBFHJ-20240904-102428.gif "บนหน้าจอของฉัน มีเพียงผู้เล่นท้องถิ่นของฉันที่เคลื่อนไหว และผู้เล่นคนอื่นๆ ดูเหมือนจะนิ่งแม้ว่าพวกเขาจะเคลื่อนไหวจริงๆ.")



เรามาดำเนินการซิงโครไนซ์ตำแหน่งกันต่อไป.



### ขั้นตอนที่ 3 : การซิงโครไนซ์โดยการดึงข้อมูลของผู้เล่นคนอื่น

สำหรับการซิงโครไนซ์ ทุกครั้งที่ผู้เล่นเคลื่อนไหวหรือทำการใดๆ พวกเขาต้องส่งการเปลี่ยนแปลงสถานะของพวกเขาไปยังเซิร์ฟเวอร์.

การส่งข้อความที่มีการเปลี่ยนแปลงสถานะของพวกเขาไปยังเซิร์ฟเวอร์จะทำผ่านการสื่อสารข้อความในห้อง.

เมื่อเซิร์ฟเวอร์ได้รับข้อความเกี่ยวกับการเปลี่ยนแปลงสถานะของผู้เล่น มันจะอัปเดตสถานะห้อง.

:::hint{type="info"}
- กรุณาอ้างอิงไปยังคู่มือต่อไปนี้. \[[ข้อความในห้องเล่นหลายคน](docId:80Px4FCTALLbpG9V1PnCX)]
:::



ตัวอย่างเช่น สมมติว่าผู้เล่นท้องถิ่นของคุณชื่อ B.

เมื่อผู้เล่นเครือข่าย A เข้าร่วมห้อง พวกเขาจะถูกสร้างขึ้นที่พิกัด x:0, y:0, z:0.

หากผู้เล่น A ย้ายไปที่ตำแหน่ง x: -10, y: 0, z: 0, B ไม่มีวิธีการที่แท้จริงในการรู้ว่า A กำลังเคลื่อนที่อยู่.

นี่เป็นเพราะทั้งสองคนกำลังทำงานในท้องถิ่นบนอุปกรณ์ที่แยกจากกัน ดังนั้นจึงอยู่บนลูกค้าที่แยกจากกัน.

ดังนั้น บุคคลที่ควบคุม A จำเป็นต้องสื่อสารการเคลื่อนไหวของพวกเขาไปยังเซิร์ฟเวอร์ผ่านข้อความในห้อง.

เมื่อเซิร์ฟเวอร์ได้รับข้อมูลนี้ มันจะแจ้งให้ทุกคนที่อยู่ในห้องทราบเกี่ยวกับตำแหน่งจริงของ A แบบเรียลไทม์ ด้วยวิธีนี้ B จะทราบว่า A กำลังเคลื่อนที่.

เพื่อให้เซิร์ฟเวอร์แจ้งผู้เล่นคนอื่น มีสองวิธีคือ:

- ใช้การกระจายข้อความในห้อง.
- อัปเดตสถานะห้อง จากนั้นให้ลูกค้าดึงและใช้สถานะห้อง.

คู่มือนี้ใช้วิธีที่สองในการอัปเดตสถานะห้อง.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-jdhwvhngCfWBwEj36JbCZ-20240904-102423.png)



เนื่องจากผู้เล่น Zepeto ที่โหลดเข้าสู่ฉากมีคุณสมบัติของตัวละคร Zepeto คุณสามารถใช้ฟังก์ชันตัวละคร Zepeto เพื่อสั่งการเคลื่อนไหวไปยังตำแหน่งเฉพาะหรือเริ่มกระโดดได้

เพื่อให้ลูกค้าของ B มองเห็นการเคลื่อนไหวของ A ไปที่ x:-10, y:0, z:0 วิธีที่เข้าใจได้ง่ายที่สุดคือการใช้ MoveToPosition() ฟังก์ชันนี้จะย้ายผู้เล่นไปยังตำแหน่งล่าสุดของ A ตามที่ได้รับจากเซิร์ฟเวอร์

ไม่เพียงแต่การเปลี่ยนตำแหน่ง แต่ยังรวมถึงท่าทาง การใช้ทักษะ การเก็บไอเทม และการเปลี่ยนแปลงสถานะทั้งหมดต้องการการสื่อสารระหว่างเซิร์ฟเวอร์และลูกค้าเพื่อการซิงโครไนซ์

คุณจะต้องดำเนินการซิงโครไนซ์เพื่อให้ทุกการกระทำอยู่ในความกลมกลืนกันทั่วทั้งเครือข่าย

:::hint{type="success"}
**👍 สรุปแนวคิดการซิงโครไนซ์**

- เมื่อผู้เล่นท้องถิ่นมีการเปลี่ยนแปลงสถานะ พวกเขาจะส่งไปยังเซิร์ฟเวอร์โดยใช้ Room Message
- เซิร์ฟเวอร์จะแจ้งให้ผู้เล่นคนอื่น ๆ ทั้งหมดทราบเกี่ยวกับการเปลี่ยนแปลงสถานะ ยกเว้นผู้เล่นท้องถิ่น
- เมื่อได้รับข้อความการเปลี่ยนแปลงสถานะ โค้ดของลูกค้าจะอัปเดตสถานะของผู้เล่นที่ส่งข้อความ
:::



### ขั้นตอนที่ 3-1 : สคริปต์เซิร์ฟเวอร์ที่มีการซิงโครไนซ์ตำแหน่งที่เสร็จสมบูรณ์

ในสคริปต์เซิร์ฟเวอร์พื้นฐาน จำเป็นต้องมีการดำเนินการเพิ่มเติมเพื่ออัปเดตสถานะห้องทุกครั้งที่ได้รับข้อความเกี่ยวกับการเปลี่ยนแปลงสถานะจากไคลเอนต์ของผู้เล่นท้องถิ่น

```typescript
import {Sandbox, SandboxOptions, SandboxPlayer} from "ZEPETO.Multiplay";

import {Player, Transform, Vector3} from "ZEPETO.Multiplay.Schema";
 
export default class extends Sandbox {
 
     
    constructor() {
        super();
    }
 
    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;
                player.subState = message.subState; // ตัวควบคุมตัวละคร V2
            }
        });
    }
     
    
     
    async onJoin(client: SandboxPlayer) {
 
        // สร้างวัตถุผู้เล่นที่กำหนดใน schemas.json และตั้งค่าค่าตั้งต้น
        console.log(`[OnJoin] sessionId : ${client.sessionId}, userId : ${client.userId}`)
 
        const player = new Player();
        player.sessionId = client.sessionId;
 
        if (client.userId) {
            player.zepetoUserId = client.userId;
        }
         // จัดการวัตถุผู้เล่นโดยใช้ sessionId ซึ่งเป็นค่าคีย์ที่ไม่ซ้ำกันของวัตถุไคลเอนต์
        // ไคลเอนต์สามารถตรวจสอบข้อมูลเกี่ยวกับวัตถุผู้เล่นที่เพิ่มโดยการตั้งค่า add_OnAdd event ไปยังวัตถุผู้เล่น
        this.state.players.set(client.sessionId, player);
    }
 
    async onLeave(client: SandboxPlayer, consented?: boolean) {
 
        // โดยการตั้งค่า allowReconnection จะสามารถรักษาการเชื่อมต่อสำหรับวงจรได้ แต่จะทำความสะอาดทันทีในคู่มือพื้นฐาน
        // ไคลเอนต์สามารถตรวจสอบข้อมูลเกี่ยวกับวัตถุผู้เล่นที่ถูกลบโดยการตั้งค่า add_OnRemove event ไปยังวัตถุผู้เล่น
        this.state.players.delete(client.sessionId);
    }
}


```



### ขั้นตอนที่ 3-2 : สคริปต์ไคลเอนต์ที่มีการซิงโครไนซ์ตำแหน่งที่เสร็จสมบูรณ์

การดำเนินการหลักในสคริปต์ไคลเอนต์พื้นฐานคือ:

- เรียกใช้ OnStateChange โดยอัตโนมัติเมื่อสถานะห้องของเซิร์ฟเวอร์เปลี่ยนแปลง
- ใช้ `SendMessageLoop(0.04)` ฟังก์ชัน ส่งตำแหน่งของผู้เล่นท้องถิ่นและข้อมูลสถานะการกระโดดของตัวละครไปยังเซิร์ฟเวอร์ทุก ๆ 0.04 วินาที

```typescript
import {ZepetoScriptBehaviour} from 'ZEPETO.Script'
import {ZepetoWorldMultiplay} from 'ZEPETO.World'
import {Room, RoomData} from 'ZEPETO.Multiplay'
import {Player, State, Vector3} from 'ZEPETO.Multiplay.Schema'
import {CharacterState, SpawnInfo, ZepetoPlayers, ZepetoPlayer, CharacterJumpState} from 'ZEPETO.Character.Controller'
import * as UnityEngine from "UnityEngine";
 
 
export default class ClientStarterV2 extends ZepetoScriptBehaviour {
 
    public multiplay: ZepetoWorldMultiplay;
 
    private room: Room;
    private currentPlayers: Map<string, Player> = new Map<string, Player>();
 
    private zepetoPlayer: ZepetoPlayer;
 
    private Start() {
      			this.multiplay.RoomCreated += (room: Room) => {
            this.room = room;
        };
 
        this.multiplay.RoomJoined += (room: Room) => {
            room.OnStateChange += this.OnStateChange;
        };
 
        this.StartCoroutine(this.SendMessageLoop(0.04));
    }
 
    // ส่งการแปลงตัวละครท้องถิ่นไปยังเซิร์ฟเวอร์ที่เวลาที่กำหนด
    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 character = ZepetoPlayers.instance.GetPlayer(this.room.SessionId).character;                                 
                    this.SendTransform(character.transform);
                    this.SendState(character.CurrentState);
                }
            }
        }
    }
 
    private OnStateChange(state: State, isFirst: boolean) {
 
        // เมื่อได้รับเหตุการณ์ OnStateChange ครั้งแรก จะมีการบันทึกภาพรวมของสถานะทั้งหมด
        if (isFirst) {
 
            // [CharacterController] (ท้องถิ่น) ถูกเรียกเมื่ออินสแตนซ์ผู้เล่นถูกโหลดอย่างสมบูรณ์ในฉาก
            ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
                const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
                this.zepetoPlayer = myPlayer;
            });
 
            // [CharacterController] (ท้องถิ่น) ถูกเรียกเมื่ออินสแตนซ์ผู้เล่นถูกโหลดอย่างสมบูรณ์ในฉาก
            ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
                const isLocal = this.room.SessionId === sessionId;
                if (!isLocal) {
                    const player: Player = this.currentPlayers.get(sessionId);
 
                    // [RoomState] ถูกเรียกเมื่อใดก็ตามที่สถานะของอินสแตนซ์ผู้เล่นถูกอัปเดต
                    player.OnChange += (changeValues) => 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)) {
                join.set(sessionId, player);
            }
            leave.delete(sessionId);
        });
 
        // [RoomState] สร้างอินสแตนซ์ผู้เล่นสำหรับผู้เล่นที่เข้าห้อง
        join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
 
        // [RoomState] ลบอินสแตนซ์ผู้เล่นสำหรับผู้เล่นที่ออกจากห้อง
        leave.forEach((player: Player, sessionId: string) => this.OnLeavePlayer(sessionId, player));
    }
 
    private OnJoinPlayer(sessionId: string, player: Player) {
        console.log(`[OnJoinPlayer] players - sessionId : ${sessionId}`);
        this.currentPlayers.set(sessionId, player);
 
        const spawnInfo = new SpawnInfo();
        const position = this.ParseVector3(player.transform.position);
        const rotation = this.ParseVector3(player.transform.rotation);
        spawnInfo.position = position;
        spawnInfo.rotation = UnityEngine.Quaternion.Euler(rotation);
 
        const isLocal = this.room.SessionId === player.sessionId;
        ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
    }
 
    private OnLeavePlayer(sessionId: string, player: Player) {
        console.log(`[OnRemove] players - sessionId : ${sessionId}`);
        this.currentPlayers.delete(sessionId);
 
        ZepetoPlayers.instance.RemovePlayer(sessionId);
    }
 
    private OnUpdatePlayer(sessionId: string, player: Player) {
 
        const position = this.ParseVector3(player.transform.position);
 
        const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);
 
        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);
        }
 
        if (player.state === CharacterState.Jump) {
            if (zepetoPlayer.character.CurrentState !== CharacterState.Jump) {
                zepetoPlayer.character.Jump();
            }
 
            if (player.subState === CharacterJumpState.JumpDouble) {
                zepetoPlayer.character.DoubleJump();
            }
        }
    }
 
    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 SendState(state: CharacterState) {
        const data = new RoomData();
        data.Add("state", state);
        if(state === CharacterState.Jump) {
            data.Add("subState", this.zepetoPlayer.character.MotionV2.CurrentJumpState);
        }
        this.room.Send("onChangedState", data.GetObject());
    }
 
    private ParseVector3(vector3: Vector3): UnityEngine.Vector3 {
        return new UnityEngine.Vector3
        (
            vector3.x,
            vector3.y,
            vector3.z
        );
    }
}
```



![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-Gp1oU1cN9Y-neGhxo7HUY-20240904-102430.gif "เมื่อใช้รหัสการซิงโครไนซ์ตำแหน่ง ไม่เพียงแต่ผู้เล่นในท้องถิ่นของฉัน แต่การเคลื่อนไหวของผู้เล่นคนอื่น ๆ ก็จะแสดงด้วยเช่นกัน.")



:::hint{type="success"}
**👍 เคล็ดลับ**

- คู่มือนี้ใช้การซิงโครไนซ์ตำแหน่งเท่านั้น การซิงโครไนซ์ท่าทาง การซิงโครไนซ์วัตถุ ฯลฯ ยังไม่ได้ดำเนินการ
- หลักการเหมือนกันสำหรับทุกอย่าง แต่กระบวนการส่งและรับข้อความในห้องในทุกช่วงเวลาที่จำเป็นนั้นเป็นสิ่งจำเป็น
- หากคุณต้องการดำเนินการซิงโครไนซ์ได้สะดวกยิ่งขึ้น ให้พิจารณาใช้โมดูลซิงโครไนซ์แบบหลายผู้เล่น
:::

