---
title: Tutorial Bermain Ganda
slug: world-sdk-guide-id/tutorial-bermain-ganda
docTags: 
createdAt: 2024-09-02T08:34:13.138Z
---

## Proyek Contoh

:::hint{type="info"}
**📘 Contoh Multiplay**
[https://github.com/naverz/zepeto-multiplay-example](https://github.com/naverz/zepeto-multiplay-example)
:::



## Ringkasan

| Ringkasan             | Dari membuat server Multiplay hingga klien, siapkan lingkungan yang diperlukan untuk mengembangkan Dunia Multiplay. |
| --------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Kesulitan             | Menengah                                                                                                            |
| Waktu yang Diperlukan | 1 Jam                                                                                                               |



## Bagian 1. Menginstal Multiplay

- Dari membuat server Multiplay hingga klien, siapkan lingkungan yang diperlukan untuk mengembangkan Dunia Multiplay.

::embed[[https://www.youtube.com/watch?v=S68B1TMa-A8](https://www.youtube.com/watch?v=S68B1TMa-A8)]{url="https://www.youtube.com/watch?v=S68B1TMa-A8"}



## Bagian 2. Menulis Logika Dunia

- Pelajari tentang Skema yang diperlukan untuk komunikasi antara server dan klien, dan definisikan Jenis Skema dan Status Ruangan.

::embed[[https://www.youtube.com/watch?v=tfkY_raboV0](https://www.youtube.com/watch?v=tfkY_raboV0)]{url="https://www.youtube.com/watch?v=tfkY_raboV0"}

:::hint{type="danger"}
- Harap dicatat bahwa skrip server yang digunakan dalam video, yang mencakup konten terkait hashCode, tidak lagi didukung.
- Oleh karena itu, kecualikan kode berikut saat menulis.

```typescript
if (client.hashCode) {  
	player.zepetoHash = client.hashCode;  
}
```
:::



## Bagian 3. Menulis Logika Dunia 2

- Tulis logika Dunia yang diperlukan untuk menyinkronkan dari lokasi pemain ke keluaran pemain.

::embed[[https://www.youtube.com/watch?v=H92Gd2G9DhM](https://www.youtube.com/watch?v=H92Gd2G9DhM)]{url="https://www.youtube.com/watch?v=H92Gd2G9DhM"}



## Bagian 4. Menjalankan Server dan Menghubungkan ke Multiplay

- Jalankan server dan hubungkan ke Multiplay.

::embed[[https://www.youtube.com/watch?v=FmK6JNlSjJA](https://www.youtube.com/watch?v=FmK6JNlSjJA)]{url="https://www.youtube.com/watch?v=FmK6JNlSjJA"}



## Bagian 5. Skrip

### Schemas.json

:::CodeblockTabs
schemas.json

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



### Kode Server

:::CodeblockTabs
index.ts

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

export default class extends Sandbox {


    storageMap:Map<string,DataStorage> = new Map<string, DataStorage>();
    
    constructor() {
        super();
    }

    onCreate(options: SandboxOptions) {
      	// Dipanggil saat objek Ruang dibuat.
        // Tangani status atau inisialisasi data objek Ruang.

        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; // Pengontrol Karakter V2
            }
        });
    }
    
   
    
    async onJoin(client: SandboxPlayer) {

        // Buat objek pemain yang didefinisikan dalam schemas.json dan atur nilai awal.
        console.log(`[OnJoin] sessionId : ${client.sessionId}, userId : ${client.userId}`)

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

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

        // [DataStorage] Memuat DataStorage Pemain yang masuk
        const storage: DataStorage = client.loadDataStorage();

        this.storageMap.set(client.sessionId,storage);

        let visit_cnt = await storage.get("VisitCount") as number;
        if (visit_cnt == null) visit_cnt = 0;

        console.log(`[OnJoin] jumlah kunjungan ${client.sessionId} : ${visit_cnt}`)

        // [DataStorage] Perbarui jumlah kunjungan Pemain dan kemudian Simpan Storage
        await storage.set("VisitCount", ++visit_cnt);

        // Kelola objek Pemain menggunakan sessionId, nilai kunci unik dari objek klien.
        // Klien dapat memeriksa informasi tentang objek pemain yang ditambahkan dengan menambahkan acara add_OnAdd ke objek pemain.
        this.state.players.set(client.sessionId, player);
    }

    onTick(deltaTime: number): void {
        //  Dipanggil berulang kali pada setiap waktu yang ditetapkan di server, dan acara interval tertentu dapat dikelola menggunakan deltaTime.
    }

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

				// Dengan mengatur allowReconnection, dimungkinkan untuk mempertahankan koneksi untuk sirkuit, tetapi segera membersihkan dalam panduan dasar.
        // Klien dapat memeriksa informasi tentang objek pemain yang dihapus dengan menambahkan acara add_OnRemove ke objek pemain.
        this.state.players.delete(client.sessionId);
    }
}
```
:::



### Kode Klien

:::CodeblockTabs
ClientStarter.ts

```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));
    }

    // Kirim transformasi karakter lokal ke server pada Interval Waktu yang dijadwalkan.
    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) {

        // Ketika event OnStateChange pertama diterima, snapshot status penuh dicatat.
        if (isFirst) {

            // [CharacterController] (Lokal) Dipanggil ketika instance Pemain sepenuhnya dimuat di Scene
            ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
                const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
                this.zepetoPlayer = myPlayer;
            });

            // [CharacterController] (Lokal) Dipanggil ketika instance Pemain sepenuhnya dimuat di Scene
            ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
                const isLocal = this.room.SessionId === sessionId;
                if (!isLocal) {
                    const player: Player = this.currentPlayers.get(sessionId);

                    // [RoomState] Dipanggil setiap kali status instance pemain diperbarui. 
                    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] Buat instance pemain untuk pemain yang masuk ke Ruangan
        join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));

        // [RoomState] Hapus instance pemain untuk pemain yang keluar dari ruangan
        leave.forEach((player: Player, sessionId: string) => this.OnLeavePlayer(sessionId, player));
    }

    private OnJoinPlayer(sessionId: string, player: Player) {
        console.log(`[OnJoinPlayer] pemain - 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] pemain - 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
        );
    }
}
```
:::

