---
title: ท่าทาง
slug: world-sdk-guide-th/nFDfKjTe1mv1TrGatIhdb
docTags: 
createdAt: 2024-09-02T08:34:13.134Z
---

ZepetoWorldContent API ช่วยให้คุณตั้งภาพขนาดย่อสำหรับหมวดหมู่ท่าทาง/ท่าทางที่ต้องการและเปิดใช้งานท่าทาง/ท่าทางเฉพาะเมื่อคลิกที่ภาพขนาดย่อ.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-UnpfP_uZECoMnF5rgTIym-20240904-102427.gif)



ในการใช้ ZepetoWorldContent API คุณต้องเขียนคำสั่งนำเข้า ดังนี้.

```typescript
import { OfficialContentType, WorldService, ZepetoWorldContent, Content } from 'ZEPETO.World';
```



ข้อมูลตัวแปรและฟังก์ชันของคลาส Content ที่มีข้อมูลการเคลื่อนไหว/ท่าทางมีดังนี้:

| **API**                                                  | **คำอธิบาย**                                                                                                                                                                                                                                                           |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| public get Id(): string                                  | รหัสเฉพาะเนื้อหา                                                                                                                                                                                                                                                       |
| public get Title(): string                               | ข้อความชื่อท่าทาง<br /><br />- ภาษา จะถูกแปลโดยอัตโนมัติตามภาษาของอุปกรณ์                                                                                                                                                                                              |
| public get Thumbnail(): UnityEngine.Texture2D            | ภาพขนาดย่อ 2D                                                                                                                                                                                                                                                          |
| public get AnimationClip(): UnityEngine.AnimationClip    | คลิปอนิเมชันท่าทาง                                                                                                                                                                                                                                                     |
| public get IsDownloadedThumbnail(): boolean              | ฟังก์ชันเพื่อตรวจสอบว่าคุณได้ดาวน์โหลดภาพขนาดย่อนี้แล้วหรือไม่                                                                                                                                                                                                         |
| public get IsDownloadedAnimation(): boolean              | ฟังก์ชันเพื่อตรวจสอบว่าคุณได้ดาวน์โหลดคลิปอนิเมชันนี้แล้วหรือไม่                                                                                                                                                                                                       |
| public DownloadAnimation($complete: System.Action)\:void | ฟังก์ชันดาวน์โหลดคลิปอนิเมชันที่รับการเรียกกลับเมื่อเสร็จสิ้น<br /><br />- หาก IsDownloadedAnimation() เป็น false ให้เรียกใช้ DownloadAnimation()                                                                                                                      |
| public DownloadThumbnail($complete: System.Action)\:void | ฟังก์ชันดาวน์โหลดภาพขนาดย่อ<br /><br />- หาก IsDownloadedThumbnail() เป็น false ให้เรียกใช้ DownloadThumbnail()                                                                                                                                                        |
| OfficialContentType : enum                               | ประเภทของเนื้อหา (World 1.9.0 ขึ้นไป)<br /><br />- ท่าทาง = 2<br />- ท่าทาง = 4<br />- เซลฟี่ = 8<br />- ท่าทางทักทาย = 16<br />- ท่าทาง = 32<br />- ท่าทางยืนยัน = 64<br />- ท่าทางเต้น = 128<br />- ท่าทางปฏิเสธ = 256<br />- ท่าทางอื่น ๆ = 512<br />- ทั้งหมด = 14 |

:::hint{type="danger"}
- คุณสามารถใช้ฟังก์ชันที่มีอยู่แล้ว `public DownloadThumbnail($character: ZEPETO_Character_Controller.ZepetoCharacter, $complete: System.Action):void`, โดยไม่มีปัญหาเกี่ยวกับฟังก์ชันการทำงาน อย่างไรก็ตาม เนื่องจากมันไม่รับตัวละคร Zepeto เป็นอาร์กิวเมนต์อีกต่อไป กรุณาใช้ฟังก์ชันที่แก้ไขใหม่ `public DownloadThumbnail($complete: System.Action):void` แทน.
:::



## ขั้นตอนที่ 1 : ตั้งค่า UI



## ขั้นตอนที่ 1-1 : สร้างปุ่มท่าทาง

1\) เพิ่ม Hierarchy > UI > Canvas และตั้งค่า Sort Order เป็น 2 เพื่อหลีกเลี่ยงการถูกบดบังโดย UI อื่น.

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



2\) เพิ่มลำดับชั้น > UI > ปุ่ม.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-23HjYw463uI38rM3540vu-20240904-102427.png "ตัวอย่างปุ่มท่าทาง")



## ขั้นตอน 1-2 : จัดระเบียบแผงท่าทาง

1\) เพิ่มลำดับชั้น > สร้างวัตถุว่างและเปลี่ยนชื่อเป็น PanelParent.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-kA-CWlNNslUUr_9KBHlFJ-20240904-102427.png)



2\) เพิ่มลำดับชั้น > UI > แผงเป็นลูกของ PanelParent.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-WHpk5yBybkZb05MFvmovg-20240904-102427.png "ตัวอย่างแผงท่าทาง")



3\) ปุ่มปิด: หลังจากเพิ่ม UI > ปุ่ม ให้เพิ่มเหตุการณ์ onClick เพื่อปิดแผงท่าทาง.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-HR4FNZGvT1UZlDe3IuXtF-20240904-102427.png "ตัวอย่างการตั้งค่าปุ่มปิด")



4\) ปุ่มเปิด: กรุณาเพิ่มเหตุการณ์ onClick ที่เปิดแผงท่าทางไปยังปุ่มเปิดที่สร้างขึ้นข้างต้น.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-MKqJTnRH4QzrlXIJOyehI-20240904-102427.png "ตัวอย่างการตั้งค่าปุ่มเปิด")



5\) เพิ่มภาพเพื่อใช้เป็นพื้นที่ชื่อเรื่อง.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-0xjmwA4pNtH_rlxgjL4S9-20240904-102427.png "ตัวอย่างภาพชื่อเรื่อง")



6\) ตั้งค่ามุมมองเลื่อนเพื่อแสดงภาพขนาดย่อของท่าทาง.

- เพิ่ม Hierarchy > UI > Scroll View.
- ตรวจสอบ Horizontal และปิดการใช้งานภาพแถบเลื่อนเนื่องจากคุณจะใช้เฉพาะการเลื่อนแนวตั้งและไม่ต้องการการเลื่อนแนวนอน.
- เพิ่มเลย์เอาต์แบบกริดไปยังเนื้อหาใน Scroll View เพื่อจัดเรียงภาพขนาดย่อในรูปแบบกริด.
- เพิ่ม Content Size Fitter เพื่อทำให้ขนาดของวัตถุเหมาะสมกับขนาดของเนื้อหา.
- เมื่อคุณดำเนินการสคริปต์ คุณต้องตั้งค่าเนื้อหาใน Scroll View เป็นผู้ปกครองของภาพขนาดย่อของท่าทาง (เพื่อให้พื้นที่ทั้งหมดถูกจดจำและเลื่อน)

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-NTHIAOgZfjHaBM6_m7vF3-20240904-102427.png "ตัวอย่างการตั้งค่ามุมมองเลื่อน")



7\) ตั้งค่าแท็บตามประเภทท่าทาง.

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

:::hint{type="success"}
**👍&#x20;**&#xE43;นการกำหนดค่าแท็บเพิ่มเติม ให้เพิ่มลำดับชั้น > UI > Scroll View และตรวจสอบแนวนอนในตัวเลือก Scroll View.
:::

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N--8G4Wi0_yKQCjS5EKh3Z6-20240904-102427.png "ตัวอย่างการกำหนดค่า GestureTitle")



8\) เพิ่มข้อความที่จะใช้เป็นปุ่มสลับเป็นลูกของ GestureTitle และแทนที่ด้วย All.

- ตั้งค่าสีของข้อความเป็นสีเทา.
- เพิ่มข้อความที่เน้นซึ่งจะแสดงเมื่อถูกเลือกเป็นลูกของข้อความ.
  - ตั้งค่าขนาดและความหนาของฟอนต์ให้เหมือนกัน และตั้งค่าสีเป็นสีดำ.
- เพิ่มส่วนประกอบ Toggle.
  - ระบุวัตถุหลักในกลุ่ม.
  - เพิ่มข้อความที่เน้นซึ่งคุณได้เพิ่มเป็นลูกให้กับ Graphic
  - ตรวจสอบ isOn เฉพาะสำหรับส่วนประกอบ Toggle ทั้งหมดที่จะถูกแสดงก่อน.
- สร้างปุ่มสลับ Gesture และ Pose ทั้งสองในลักษณะเดียวกัน.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-QzGzbIJmv1keZcc5bWn8x-20240904-102427.png "ตัวอย่างการตั้งค่าปุ่มสลับ")



## ขั้นตอน 1-3 : สร้างพรีแฟบภาพขนาดย่อ

ใช้วิธีการสร้างปุ่มภาพขนาดย่อเป็นพรีแฟบและจากนั้นสร้างเป็นอินสแตนซ์ในสคริปต์

1\) เพิ่ม UI > ปุ่ม เป็นลูกของเนื้อหาใน Scroll View และเปลี่ยนชื่อเป็น preThumb

2\) กรุณาเปลี่ยนชื่อเป็น Thumb หลังจากเพิ่ม Raw Image

- ภาพนี้จะเป็นภาพขนาดย่อ ปรับขนาดให้เหมาะสม

3\) เพิ่มข้อความ

- ตั้งตำแหน่งให้กึ่งกลางด้านล่างของภาพ
- ปรับขนาดและความหนาของการเขียน และเพิ่ม Content Size Fitter
  - Horizontal Fit : ขนาดที่ต้องการ
  - Vertical Fit : ขนาดที่ต้องการ

4\) หากการตั้งค่าเสร็จสิ้น กรุณาทำให้เป็น Prefab และใส่ในโฟลเดอร์ Resources

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-HEjBUarUzOHukKwXOjYmO-20240904-102427.png "ตัวอย่างการตั้งค่าธรรมชาติของภาพขนาดย่อ")

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N--8qFrc8v8chrUoQTFkeLH-20240904-102427.png "ตัวอย่างที่ปุ่มขนาดย่อถูกตั้งค่าในแผงการเคลื่อนไหวหลังจากการดำเนินการ")



## ขั้นตอนที่ 1-4 : วิดีโอแนะนำการตั้งค่า UI

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

:::hint{type="success"}
**👍&#x20;**&#xE04;่าขนาดและตำแหน่งของ UI ที่แสดงในวิดีโอเป็นค่าที่แนะนำ แต่คุณสามารถปรับเปลี่ยนให้เป็นค่าที่คุณต้องการได้!
:::

เมื่อการตั้งค่า UI เสร็จสิ้น ให้ดำเนินการไปที่การเขียนสคริปต์.



## ขั้นตอนที่ 2 : เขียนสคริปต์

สคริปต์นี้อิงจากการเล่นแบบเดี่ยว.



## ขั้นตอนที่ 2-1 : รูปขนาดย่อ

- โปรเจกต์ > สร้าง > ZEPETO > TypeScript และเปลี่ยนชื่อเป็น รูปขนาดย่อ.
- เขียนสคริปต์ตัวอย่างตามด้านล่าง.
  - นี่คือสคริปต์ที่จัดระเบียบข้อมูลเนื้อหาท่าทาง (ชื่อ, รูปภาพ) ลงใน UI.

:::CodeblockTabs
รูปขนาดย่อ

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Content } from 'ZEPETO.World';
import { RawImage, Text } from 'UnityEngine.UI';
import { Texture2D } from 'UnityEngine';
 
export default class Thumbnail extends ZepetoScriptBehaviour {
 
    @HideInInspector() public content: Content;
     
    Start() {
        this.GetComponentInChildren<Text>().text = this.content.Title;
        this.GetComponentInChildren<RawImage>().texture = this.content.Thumbnail as Texture2D;
    }
 
}
```
:::

- หลังจากสร้างสคริปต์แล้ว ให้เปิด prefab preThumb และเพิ่มสคริปต์เข้าไป.

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-mMxqLpUWz04xT28luxwWO-20240904-102427.png" size="86" width="470" height="641" position="center" showCaption="false"}



## ขั้นตอนที่ 2-2 : GestureLoader

- สร้างลำดับชั้น > สร้างวัตถุเปล่าและเปลี่ยนชื่อเป็น GestureLoader.
- สร้างโปรเจกต์ > สร้าง > ZEPETO > TypeScript และเปลี่ยนชื่อเป็น GestureLoader.
- เขียนสคริปต์ตัวอย่างตามด้านล่าง.

:::CodeblockTabs
GestureLoader

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { LocalPlayer, SpawnInfo, ZepetoCharacter, ZepetoPlayers } from 'ZEPETO.Character.Controller';
import { OfficialContentType, WorldService, ZepetoWorldContent, Content } from 'ZEPETO.World';
import { RawImage, Text, Button } from 'UnityEngine.UI';
import { GameObject, Texture2D, Transform, WaitUntil } from 'UnityEngine';
import Thumbnail from './Thumbnail';
 
export default class GestureLoader extends ZepetoScriptBehaviour {
 
    @HideInInspector() public contents: Content[] = [];
    @HideInInspector() public thumbnails: GameObject[] = [];
 
    @SerializeField() private _count: number = 50;
    @SerializeField() private _contentsParent: Transform;
    @SerializeField() private _prefThumb: GameObject;
 
    private _myCharacter: ZepetoCharacter;
     
    Start() {
        // Creating a Character
        ZepetoPlayers.instance.CreatePlayerWithUserId(WorldService.userId, new SpawnInfo(), true);
         
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            this._myCharacter = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
 
            // In order to take a thumbnail with my character, You need to request the content after the character is created.
            this.ContentRequest();
        });
    }
 
 
    // 1. Receive content from the server
    private ContentRequest() {
         
        // All Type Request
        ZepetoWorldContent.RequestOfficialContentList(OfficialContentType.All, contents => {
            this.contents = contents;
             
            for (let i = 0; i < this._count; i++) {
                if (!this.contents[i].IsDownloadedThumbnail) {
                    // Take a thumbnail photo using my character
                    this.contents[i].DownloadThumbnail(() =>{
                        this.CreateThumbnailObjcet(this.contents[i]);
                    });
                } else {
                    this.CreateThumbnailObjcet(this.contents[i]);
                }
            }
        });
 
    }
 
    // 2. Creating Thumbnail Objects
    private CreateThumbnailObjcet(content: Content) {
        const newThumb: GameObject = GameObject.Instantiate(this._prefThumb, this._contentsParent) as GameObject;
        newThumb.GetComponent<Thumbnail>().content = content;
 
        // Button Listener for each thumbnail
        newThumb.GetComponent<Button>().onClick.AddListener(() => {
            this.LoadAnimation(content);
        });
         
        this.thumbnails.push(newThumb);
    }
 
    // 3. Loading Animation
    private LoadAnimation(content: Content) {
        // Verify animation load
        if (!content.IsDownloadedAnimation) {
            // If the animation has not been downloaded, download it.
            content.DownloadAnimation(() => {
                // play animation clip
                this._myCharacter.SetGesture(content.AnimationClip);
            });
        } else {
            this._myCharacter.SetGesture(content.AnimationClip);
        }
    }
}
```
:::

:::hint{type="danger"}
- จำนวนคือจำนวนสูงสุดของท่าทางที่จะดาวน์โหลดในแต่ละแท็บ หากคุณตั้งค่าให้มากกว่า 100 อาจมีข้อผิดพลาดในระหว่างกระบวนการดาวน์โหลดภาพขนาดย่อ ดังนั้นโปรดตั้งค่าให้เพียงพอต่อความต้องการเท่านั้น.
:::



สคริปต์มีลำดับการไหลดังนี้:

1\) เรียกใช้ฟังก์ชัน ContentsRequest() ที่กำหนดเองเพื่อสร้างภาพขนาดย่อหลังจากโหลดตัวละคร ZEPETO.

- ฟังก์ชัน ContentsRequest() จะรับข้อมูลเนื้อหาโดยการแยกท่าทางและท่าทางออกจากกันตามลำดับ.
  - หากมีภาพขนาดย่อที่มีอยู่แล้ว จะถูกข้ามไป; มิฉะนั้น จะดึงภาพขนาดย่อมา.
  - ข้อมูลภาพขนาดย่อที่ดึงมาได้จะถูกเก็บไว้ในรายการที่เกี่ยวข้อง.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-WXIPiOdYeIXNHRY2eiqC_-20240904-102427.png "ตัวอย่างหน้าจอการตั้งค่า Gesture Script")



## ขั้นตอน 2-3 : UIController

- สร้าง Hierarchy > สร้างวัตถุเปล่าและเปลี่ยนชื่อเป็น UIContoller.
- สร้างโปรเจกต์ > สร้าง > ZEPETO > TypeScript และเปลี่ยนชื่อเป็น UIContoller.
- เขียนสคริปต์ตัวอย่างตามด้านล่าง.

:::CodeblockTabs
UIController

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Button, RawImage, Text, Toggle } from 'UnityEngine.UI';
import { LocalPlayer, ZepetoCharacter, ZepetoPlayers, ZepetoScreenTouchpad } from 'ZEPETO.Character.Controller';
import { OfficialContentType, Content } from 'ZEPETO.World';
import { Object, GameObject, Transform } from 'UnityEngine';
import GestureLoader from './GestureLoader';
import Thumbnail from './Thumbnail';
 
export default class UIController extends ZepetoScriptBehaviour {
     
    @SerializeField() private _closeButton : Button;
    @SerializeField() private _typeToggleGroup : Toggle[];
 
    private _gestureLodaer: GestureLoader;
    private _myCharacter: ZepetoCharacter;
         
    Start() {
        this._gestureLodaer = Object.FindObjectOfType<GestureLoader>();
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            this._myCharacter = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
 
            // If click the touchpad, cancel the gesture
            Object.FindObjectOfType<ZepetoScreenTouchpad>().OnPointerDownEvent.AddListener(() => {
                this.StopGesture();
            });
 
            // If click the close button, cancel the gesture
            this._closeButton.onClick.AddListener(() => {
                this.StopGesture();
            });
        });
         
        // UI Listener
        this._typeToggleGroup[0].onValueChanged.AddListener(() => {
            this.SetCategoryUI(OfficialContentType.All);
        });
        this._typeToggleGroup[1].onValueChanged.AddListener(() => {
            this.SetCategoryUI(OfficialContentType.Gesture);
        });
        this._typeToggleGroup[2].onValueChanged.AddListener(() => {
            this.SetCategoryUI(OfficialContentType.Pose);
        });
 
    }
 
    // Category Toggle UI Set
    private SetCategoryUI(category: OfficialContentType) {
         
        if (category == OfficialContentType.All) {
            this._gestureLodaer.thumbnails.forEach((Obj) => {
                Obj.SetActive(true);
            });
        }   else {
            for (let i = 0; i < this._gestureLodaer.thumbnails.length; i++) {
                const content = this._gestureLodaer.thumbnails[i].GetComponent<Thumbnail>().content;
                if (content.Keywords.includes(category)) {
                    this._gestureLodaer.thumbnails[i].SetActive(true);
                } else {
                    this._gestureLodaer.thumbnails[i].SetActive(false);
                }
            }
        }
    }
     
    private StopGesture() {
        this._myCharacter.CancelGesture();
    }
}
```
:::

สคริปต์ทำงานดังนี้:

1. แตะที่แผ่นสัมผัสหรือปุ่มปิดเพื่อยกเลิกการเล่นโดยใช้ฟังก์ชัน CancelGesture()
2. แตะที่แท็บ (ปุ่มสลับ) เพื่อเรียกใช้ฟังก์ชัน SetCategoryUI() ที่กำหนดเอง
3. ฟังก์ชัน SetCategoryUI() ใช้ข้อมูลเนื้อหาท่าทางในแต่ละภาพขนาดย่อเพื่อตั้งค่าสำหรับแต่ละหมวดหมู่ที่เกี่ยวข้อง

- เปิดใช้งานหากเป็นประเภทที่ใช้ได้ และปิดใช้งานหากไม่ใช่



หลังจากเสร็จสิ้นการเขียนสคริปต์ ให้กำหนดปุ่มปิดและ TypeToggleGroup ใน Inspector.

- การเข้าถึง Type Toggle Group คือ Toggle ที่เป็นลูกของ Toggle Group ในแผงการเคลื่อนไหว.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-8SuVSLSVfcd2bO_GZUPn--20240904-102427.png "หน้าจอตั้งค่าสคริปต์ตัวอย่าง")



## ขั้นตอนที่ 3 : รัน

:::hint{type="danger"}
**❗️ ระวัง**
ก่อนเล่น ให้ปิด PanelParent เพื่อให้เห็นเฉพาะปุ่มเปิดเมื่อเล่นเท่านั้น
:::

::Image[]{src="https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-LRFGXgh9k1X9tGdpj1xn3-20240904-102427.png" size="80" width="425" height="320" position="center" showCaption="false"}



## ขั้นตอนที่ 4 : ซิงโครไนซ์ท่าทางการเล่นหลายคน

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



## ขั้นตอน 4-1 : รหัสลูกค้า

### ภาพขนาดย่อ - หลายผู้เล่น

- เขียนสคริปต์เดียวกันกับที่ใช้ในรหัสลูกค้าแบบเล่นคนเดียว

:::CodeblockTabs
ภาพขนาดย่อ - หลายผู้เล่น

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Content } from 'ZEPETO.World';
import { RawImage, Text } from 'UnityEngine.UI';
import { Texture2D } from 'UnityEngine';
 
export default class Thumbnail extends ZepetoScriptBehaviour {
 
    @HideInInspector() public content: Content;
     
    Start() {
        this.GetComponentInChildren<Text>().text = this.content.Title;
        this.GetComponentInChildren<RawImage>().texture = this.content.Thumbnail as Texture2D;
    }
 
}
```
:::



### GestureLoader - หลายผู้เล่น

- โดยค่าเริ่มต้น สคริปต์ที่ใช้ในรหัสลูกค้าแบบเล่นคนเดียวจะเขียนเหมือนกัน
- นอกจากนี้ ลูกค้าจะประกาศอินเตอร์เฟซเพื่อให้มี PlayerGestureInfo
- เมื่อส่งข้อมูลของคุณไปยังเซิร์ฟเวอร์: ดู SendMyGesture() ฟังก์ชันที่กำหนดเอง
  - เมื่อผู้เล่นของคุณกดที่ภาพขนาดย่อเพื่อทำท่าทาง ส่ง ID ของท่าทางไปยังเซิร์ฟเวอร์โดยใช้ room.Send()
  - เมื่อคุณยกเลิกท่าทาง ให้ประมวลผลเพื่อส่งข้อมูลว่าคุณได้ยกเลิกแล้ว
- เมื่อได้รับข้อมูลท่าทางจากลูกค้าอื่นจากเซิร์ฟเวอร์: "OnChangeGesture" ข้อความห้องจะถูกส่งไปยัง this.room.AddMessageHandler() ภายใน Start()
  - การซิงโครไนซ์จะเกิดขึ้นโดยการมี ID เซสชันและ ID ท่าทางในข้อความ "OnChangeGesture" และทำให้ผู้เล่นที่เหมาะสมเล่นท่าทางนั้น

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { LocalPlayer, SpawnInfo, ZepetoCharacter, ZepetoPlayers } from 'ZEPETO.Character.Controller';
import { OfficialContentType, WorldService, ZepetoWorldContent, Content, ZepetoWorldMultiplay } from 'ZEPETO.World';
import { RawImage, Text, Button } from 'UnityEngine.UI';
import { GameObject, Texture2D, Transform, WaitUntil } from 'UnityEngine';
import Thumbnail from './Thumbnail';
import { Room, RoomData } from 'ZEPETO.Multiplay';
 
interface PlayerGestureInfo {
    sessionId: string,
    gestureId: string
}
 
const CancelMotion = "" as const;
 
export default class GestureLoaderMultiPlay extends ZepetoScriptBehaviour {
 
    public multiplay: ZepetoWorldMultiplay;
     
    @HideInInspector() public contents: Content[] = [];
    @HideInInspector() public thumbnails: GameObject[] = [];
 
    @SerializeField() private _count: number = 50;
    @SerializeField() private _contentsParent: Transform;
    @SerializeField() private _prefThumb: GameObject;
 
    private _myCharacter: ZepetoCharacter;
    private _room: Room;
    private _contentsMap: Map<string, Content> = new Map<string, Content>();
 
    Start() {
        // สร้างตัวละคร
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            this._myCharacter = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
            
            // เพื่อที่จะถ่ายภาพ thumbnail ด้วยตัวละครของฉัน คุณต้องขอเนื้อหาหลังจากที่ตัวละครถูกสร้างขึ้น
            this.ContentRequest();
        });
 
        // สำหรับ MultiPlay
        this.multiplay.RoomCreated += (room: Room) => {
            this._room = room;
            // รับข้อมูลท่าทางของผู้ใช้จากเซิร์ฟเวอร์
            this._room.AddMessageHandler("OnChangeGesture", (message: PlayerGestureInfo) => {
                let playerGestureInfo: PlayerGestureInfo = {
                    sessionId: message.sessionId,
                    gestureId: message.gestureId
                };
                this.LoadAnimation(playerGestureInfo);
            });
        };
    }
 
 
    // 1. รับเนื้อหาจากเซิร์ฟเวอร์
    private ContentRequest(){
         
        // ขอทุกประเภท
        ZepetoWorldContent.RequestOfficialContentList(OfficialContentType.All, contents => {
            this.contents = contents;
            for (let i = 0; i < this._count; i++) {
                if (!this.contents[i].IsDownloadedThumbnail) {
                    // ถ่ายภาพ thumbnail โดยใช้ตัวละครของฉัน
                    this.contents[i].DownloadThumbnail(() =>{
                        this.CreateThumbnailObjcet(this.contents[i]);
                    });
                } else {
                    this.CreateThumbnailObjcet(this.contents[i]);
                }
            }
        });
 
    }
 
    // 2. สร้างวัตถุ Thumbnail
    private CreateThumbnailObjcet(content: Content) {
        const newThumb: GameObject = GameObject.Instantiate(this._prefThumb, this._contentsParent) as GameObject;
        newThumb.GetComponent<Thumbnail>().content = content;
 
        // สร้างพจนานุกรมเพื่อค้นหาเนื้อหาด้วย Id ของเนื้อหา
        this._contentsMap.set(content.Id, content);
         
        // ตัวฟังปุ่มสำหรับแต่ละ thumbnail
        newThumb.GetComponent<Button>().onClick.AddListener(() => {
            this.SendMyGesture(content.Id);
        });
 
        // รายการ thumbnail
        this.thumbnails.push(newThumb);
 
    }
 
    // สำหรับ MultiPlay
    // ส่งข้อมูลท่าทางที่คลิกไปยังเซิร์ฟเวอร์
    public SendMyGesture(gestureId) {
        const data = new RoomData();
        data.Add("gestureId", gestureId);
        this._room.Send("OnChangeGesture", data.GetObject());
    }
 
    // 3. โหลดอนิเมชัน
    private LoadAnimation(playerGestureInfo: PlayerGestureInfo){
         
        if (!ZepetoPlayers.instance.HasPlayer(playerGestureInfo.sessionId)) {
            console.log("ผู้เล่นไม่มีอยู่");
            return;
        }
         
        const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(playerGestureInfo.sessionId).character;
 
        if (playerGestureInfo.gestureId == CancelMotion) {
            zepetoPlayer.CancelGesture();
            return;
        }
        else if(!this._contentsMap.has(playerGestureInfo.gestureId)) {
            console.log("ทรัพยากรยังไม่ได้โหลด");
            return;
        }
 
        const content = this._contentsMap.get(playerGestureInfo.gestureId);
 
        // ตรวจสอบการโหลดอนิเมชัน
        if (!content.IsDownloadedAnimation) {
            // หากอนิเมชันยังไม่ได้ดาวน์โหลด ให้ดาวน์โหลดมัน
            content.DownloadAnimation(() => {
                // เล่นคลิปอนิเมชัน
                zepetoPlayer.SetGesture(content.AnimationClip);
            });
        } else {
            zepetoPlayer.SetGesture(content.AnimationClip);
        }
    }
}
```



- หลังจากเสร็จสิ้นการเขียนสคริปต์ ผู้ตรวจสอบจะมอบวัตถุเพิ่มเติมให้กับ Multiplay โดยใช้ส่วนประกอบ Zepeto World Multiplay.



![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-ErOiXFzjb_CqNuH3v5qGF-20240904-102427.png "ตัวอย่างหน้าจอการตั้งค่าสคริปต์")

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-9V_vQfOX7KEMXb5QqajfL-20240904-102427.png "ตัวอย่างวัตถุที่มีส่วนประกอบ Zepeto World Multiplay")



### UIController - Multiplay

- ตามค่าเริ่มต้น สคริปต์ที่ใช้ในโค้ดไคลเอนต์แบบเล่นคนเดียวจะเขียนเหมือนกันทั้งหมด.
- ความแตกต่างจากโค้ดไคลเอนต์แบบเล่นคนเดียวคือฟังก์ชันที่กำหนดเอง StopGesture().
  - เรียกใช้ฟังก์ชันที่กำหนดเอง SendMyGesture() ภายใน GestureLoaderMultiplay.
  - กระบวนการส่งข้อมูลว่าท่าทางถูกยกเลิกแล้ว.

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Button, RawImage, Text, Toggle } from 'UnityEngine.UI';
import { LocalPlayer, ZepetoCharacter, ZepetoPlayers, ZepetoScreenTouchpad } from 'ZEPETO.Character.Controller';
import { OfficialContentType, Content } from 'ZEPETO.World';
import { Object, GameObject, Transform } from 'UnityEngine';
import GestureLoaderMultiPlay from './GestureLoaderMultiPlay';
import Thumbnail from './Thumbnail';
 
const CancelMotion = "" as const;
 
export default class UIController extends ZepetoScriptBehaviour {
 
    @SerializeField() private _closeButton : Button;
    @SerializeField() private _typeToggleGroup : Toggle[];
 
    private _gestureLodaer: GestureLoaderMultiPlay;
    private _myCharacter: ZepetoCharacter;
 
    Start() {
        this._gestureLodaer = Object.FindObjectOfType<GestureLoaderMultiPlay>();
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            this._myCharacter = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
 
            // หากคลิกที่ touchpad ให้ยกเลิกท่าทาง
            Object.FindObjectOfType<ZepetoScreenTouchpad>().OnPointerDownEvent.AddListener(() => {
                this.StopGesture();
            });
 
            // หากคลิกที่ปุ่มปิด ให้ยกเลิกท่าทาง
            this._closeButton.onClick.AddListener(() => {
                this.StopGesture();
            });
        });
 
        // UI Listener
        this._typeToggleGroup[0].onValueChanged.AddListener(() => {
            this.SetCategoryUI(OfficialContentType.All);
        });
        this._typeToggleGroup[1].onValueChanged.AddListener(() => {
            this.SetCategoryUI(OfficialContentType.Gesture);
        });
        this._typeToggleGroup[2].onValueChanged.AddListener(() => {
            this.SetCategoryUI(OfficialContentType.Pose);
        });
     
    }
 
    // ตั้งค่า UI หมวดหมู่ Toggle
    private SetCategoryUI(category: OfficialContentType) {
 
        if (category == OfficialContentType.All) {
            this._gestureLodaer.thumbnails.forEach((Obj) => {
                Obj.SetActive(true);
            });
        }  else {
            for (let i = 0; i < this._gestureLodaer.thumbnails.length; i++) {
                const content = this._gestureLodaer.thumbnails[i].GetComponent<Thumbnail>().content;
                if (content.Keywords.includes(category)) {
                    this._gestureLodaer.thumbnails[i].SetActive(true);
                } else {
                    this._gestureLodaer.thumbnails[i].SetActive(false);
                }
            }
        }
    }
 
    private StopGesture() {
        this._gestureLodaer.SendMyGesture(CancelMotion);
    }
}
```



## ขั้นตอน 4-2 : รหัสเซิร์ฟเวอร์

- โค้ดเซิร์ฟเวอร์จะประกาศอินเทอร์เฟซเพื่อให้มี PlayerGestureInfo ในลักษณะเดียวกัน.
  - โค้ดเซิร์ฟเวอร์อิงจากโค้ดเซิร์ฟเวอร์ใน Multiplay Sample โดยค่าเริ่มต้น.
- มันสร้าง callback onMessage() ที่ส่งข้อมูลการเคลื่อนไหวไปยังลูกค้าอื่นเมื่อมีการเปลี่ยนแปลงการเคลื่อนไหวภายใน onCreate().

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

// กำหนดอินเตอร์เฟส PlayerGestureInfo เพื่อแทนข้อมูลของท่าทางของผู้เล่น
interface PlayerGestureInfo {
    sessionId: string,
    gestureId: string
}
 
export default class extends Sandbox {
  
 	 // กำหนดวัตถุคงที่ `MESSAGE_TYPE` เพื่อเก็บประเภทข้อความที่ใช้ในสคริปต์
    MESSAGE_TYPE = {
        OnChangeGesture: "OnChangeGesture"
    }
     
    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;
 
            player.transform = transform;
        });
 
        this.onMessage("onChangedState", (client, message) => {
            const player = this.state.players.get(client.sessionId);
            player.state = message.state;
            player.subState = message.subState; // ตัวควบคุมตัวละคร V2
        });
 
        // เมื่อท่าทางถูกเปลี่ยน
        this.onMessage<PlayerGestureInfo>(this.MESSAGE_TYPE.OnChangeGesture, (client, message) => {
            let gestureInfo:PlayerGestureInfo = {
                sessionId: client.sessionId,
                gestureId: message.gestureId
            };
            // ส่งท่าทางไปยังผู้เล่นคนอื่นยกเว้นลูกค้า
            this.broadcast(this.MESSAGE_TYPE.OnChangeGesture, gestureInfo);
        });
 
    }
     
    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 ไปยังวัตถุผู้เล่น
        this.state.players.set(client.sessionId, player);
    }
     
    onLeave(client: SandboxPlayer, consented?: boolean) {
 
        // โดยการตั้งค่า allowReconnection จะสามารถรักษาการเชื่อมต่อสำหรับวงจรได้ แต่ทำความสะอาดทันทีในคู่มือพื้นฐาน
        // ลูกค้าสามารถตรวจสอบข้อมูลเกี่ยวกับวัตถุผู้เล่นที่ถูกลบโดยการเพิ่มเหตุการณ์ add_OnRemove ไปยังวัตถุผู้เล่น
        this.state.players.delete(client.sessionId);
    }
}
```

