---
title: 手势
slug: world-sdk-guide-ch/s_3rQQEXKzJoxmkZ4PAdp
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';
```



包含手势/姿势信息的内容类的成员变量和函数信息如下：

| **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                               | 内容类型（世界 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 : 设置用户界面



## 步骤 1-1 : 创建手势按钮

1\) 添加层级 > 用户界面 > 画布，并将排序顺序设置为2，以避免被其他用户界面遮挡。

![](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\) 配置一个滚动视图以显示手势缩略图。

- 添加层级 > UI > 滚动视图。
- 勾选水平并禁用滚动条图像，因为您只会使用垂直滚动，水平滚动将不需要。
- 在滚动视图的内容中添加网格布局，以将缩略图对齐为网格模式。
- 添加内容大小适配器，使对象的大小适合内容的大小。
- 当您实现脚本时，必须将滚动视图中的内容设置为手势缩略图的父级（以便整个区域被识别并滚动）

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-NTHIAOgZfjHaBM6_m7vF3-20240904-102427.png "滚动视图配置示例")



7\) 按手势类型配置选项卡。

- 添加层级 > 创建一个空对象作为面板的子项，并将其重命名为 GestureTitle。
  - 这是切换按钮的父对象。
  - 添加水平布局以水平对齐选项卡。
  - 添加切换组组件。

:::hint{type="success"}
**👍&#x20;**&#x8981;配置更多选项卡，添加层级 > UI > 滚动视图，并在滚动视图选项中勾选水平。
:::

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N--8G4Wi0_yKQCjS5EKh3Z6-20240904-102427.png "GestureTitle 配置示例")



8\) 将用作切换按钮的文本添加为GestureTitle的子项，并将其替换为全部。

- 将文本颜色设置为灰色。
- 添加在选中时显示的高亮文本作为文本的子项。
  - 设置字体内容、大小和粗细相同，并将颜色设置为黑色。
- 添加切换组件。
  - 指定组中的父对象。
  - 添加您作为Graphic的子项添加的高亮文本。
  - 仅检查将首先显示的所有切换组件的isOn。
- 以相同的方式创建Gesture和Pose切换按钮。

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-QzGzbIJmv1keZcc5bWn8x-20240904-102427.png "切换按钮配置示例")



## 步骤 1-3 : 制作缩略图预制件

使用创建缩略图按钮作为预制件的方法，然后在脚本中生成它的实例。

1\) 将 UI > 按钮添加为 Scroll View 中 Content 的子项，并将其重命名为 preThumb。

2\) 添加 Raw Image 后，请将名称更改为 Thumb。

- 此图像将作为缩略图。请适当调整大小。

3\) 添加文本。

- 设置位置以居中图像底部。
- 调整文字的大小和粗细，并添加内容大小适配器。
  - 水平适配 : 首选大小
  - 垂直适配 : 首选大小

4\) 如果设置完成，请将其制作成预制件并放入 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 : 用户界面设置指南视频

::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;**&#x89C6;频中显示的用户界面大小和位置值是推荐的，但您可以将其修改为您想要的值！
:::

一旦用户界面设置完成，继续进行脚本编写。



## 步骤 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;
    }
 
}
```
:::

- 创建脚本后，打开 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() {
        // 创建角色
        ZepetoPlayers.instance.CreatePlayerWithUserId(WorldService.userId, new SpawnInfo(), true);
         
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            this._myCharacter = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
 
            // 为了用我的角色获取缩略图，您需要在角色创建后请求内容。
            this.ContentRequest();
        });
    }
 
 
    // 1. 从服务器接收内容
    private ContentRequest() {
         
        // 所有类型请求
        ZepetoWorldContent.RequestOfficialContentList(OfficialContentType.All, contents => {
            this.contents = contents;
             
            for (let i = 0; i < this._count; i++) {
                if (!this.contents[i].IsDownloadedThumbnail) {
                    // 使用我的角色拍摄缩略图
                    this.contents[i].DownloadThumbnail(() =>{
                        this.CreateThumbnailObjcet(this.contents[i]);
                    });
                } else {
                    this.CreateThumbnailObjcet(this.contents[i]);
                }
            }
        });
 
    }
 
    // 2. 创建缩略图对象
    private CreateThumbnailObjcet(content: Content) {
        const newThumb: GameObject = GameObject.Instantiate(this._prefThumb, this._contentsParent) as GameObject;
        newThumb.GetComponent<Thumbnail>().content = content;
 
        // 每个缩略图的按钮监听器
        newThumb.GetComponent<Button>().onClick.AddListener(() => {
            this.LoadAnimation(content);
        });
         
        this.thumbnails.push(newThumb);
    }
 
    // 3. 加载动画
    private LoadAnimation(content: Content) {
        // 验证动画加载
        if (!content.IsDownloadedAnimation) {
            // 如果动画尚未下载，则下载它。
            content.DownloadAnimation(() => {
                // 播放动画片段
                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 "手势脚本设置屏幕示例")



## 步骤 2-3 : UIController

- 创建一个层次结构 > 创建空对象并将其重命名为 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()函数使用每个缩略图中的手势内容信息为每个相应类别设置。

- 如果是适用类型则启用，不是则禁用。



完成脚本后，将关闭按钮和类型切换组分配给检查器。

- 类型切换组的入口是手势面板中切换组的子切换。

![](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;
    }
 
}
```
:::



### 手势加载器 - 多人游戏

- 默认情况下，单人游戏客户端代码中实现的脚本是相同的。
- 此外，客户端声明接口以包含 PlayerGestureInfo。
- 当将您的信息发送到服务器时：请参见 SendMyGesture() 自定义函数
  - 当您的玩家按下缩略图以进行手势时，使用 room.Send() 将手势 ID 发送到服务器。
  - 当您取消手势时，处理它以发送您已取消的信息。
- 从服务器接收来自另一个客户端的手势信息时："OnChangeGesture" 房间消息被发送到 this.room.AddMessageHandler() 内的 Start()
  - 通过在 "OnChangeGesture" 消息中包含会话 ID 和手势 ID 来实现同步，并使适当的玩家执行手势。

```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;
            
            // 为了用我的角色获取缩略图，您需要在角色创建后请求内容。
            this.ContentRequest();
        });
 
        // 多人游戏
        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) {
                    // 使用我的角色拍摄缩略图
                    this.contents[i].DownloadThumbnail(() =>{
                        this.CreateThumbnailObjcet(this.contents[i]);
                    });
                } else {
                    this.CreateThumbnailObjcet(this.contents[i]);
                }
            }
        });
 
    }
 
    // 2. 创建缩略图对象
    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);
         
        // 每个缩略图的按钮监听器
        newThumb.GetComponent<Button>().onClick.AddListener(() => {
            this.SendMyGesture(content.Id);
        });
 
        // 缩略图列表
        this.thumbnails.push(newThumb);
 
    }
 
    // 多人游戏
    // 将点击的手势信息发送到服务器
    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世界多人游戏组件的对象示例")



### UIController - 多人游戏

- 默认情况下，单人游戏客户端代码中实现的脚本是相同的。
- 与单人游戏客户端代码的区别在于 StopGesture() 自定义函数。
  - 在 GestureLoaderMultiplay 中调用 SendMyGesture() 自定义函数。
  - 处理发送手势已被取消的信息。

```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;
 
            // 如果点击触摸板，取消手势
            Object.FindObjectOfType<ZepetoScreenTouchpad>().OnPointerDownEvent.AddListener(() => {
                this.StopGesture();
            });
 
            // 如果点击关闭按钮，取消手势
            this._closeButton.onClick.AddListener(() => {
                this.StopGesture();
            });
        });
 
        // UI 监听
        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 设置
    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 中的服务器代码。
- 它创建了一个 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 事件添加到 players 对象来检查有关添加的玩家对象的信息。
        this.state.players.set(client.sessionId, player);
    }
     
    onLeave(client: SandboxPlayer, consented?: boolean) {
 
        // 通过设置 allowReconnection，可以保持电路的连接，但在基本指南中立即清理。
        // 客户端可以通过将 add_OnRemove 事件添加到 players 对象来检查有关已删除玩家对象的信息。
        this.state.players.delete(client.sessionId);
    }
}
```

