---
title: Gesture
slug: world-sdk-guide-kr/gesture
docTags: 
createdAt: 2024-09-02T08:34:13.134Z
---

ZepetoWorldContent API를 사용하면 원하는 제스처/포즈 카테고리에 대한 썸네일을 설정하고 썸네일을 클릭할 때 특정 제스처/포즈를 활성화할 수 있습니다.

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



ZepetoWorldContent API를 사용하려면 다음과 같이 import 문을 작성해야 합니다.

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



제스처/포즈 정보를 포함하는 Content 클래스의 멤버 변수 및 함수 정보는 다음과 같습니다:

| **API**                                                  | **설명**                                                                                                                                                                                                 |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| public get Id(): string                                  | 콘텐츠 고유 ID                                                                                                                                                                                              |
| 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\) 계층 구조 > UI > 캔버스를 추가하고 정렬 순서를 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 > Panel을 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;**&#xB354; 많은 탭을 구성하려면 계층 추가 > UI > 스크롤 뷰를 추가하고 스크롤 뷰 옵션에서 수평을 체크합니다.
:::

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N--8G4Wi0_yKQCjS5EKh3Z6-20240904-102427.png "GestureTitle 구성 예시")



8\) GestureTitle의 자식으로 토글 버튼으로 사용할 텍스트를 추가하고, 이를 All로 교체합니다.

- 텍스트의 색상을 회색으로 설정합니다.
- 체크할 때 표시될 강조된 텍스트를 텍스트의 자식으로 추가합니다.
  - 폰트 내용, 크기 및 두께를 동일하게 설정하고 색상을 검정으로 설정합니다.
- Toggle 구성 요소를 추가합니다.
  - 그룹에서 부모 객체를 지정합니다.
  - Graphic에 자식으로 추가한 강조된 텍스트를 추가합니다.
  - 먼저 표시될 All 토글 구성 요소에 대해서만 isOn을 확인합니다.
- Gesture와 Pose 토글 버튼을 동일한 방식으로 생성합니다.

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-QzGzbIJmv1keZcc5bWn8x-20240904-102427.png "토글 버튼 구성의 예")



## 1-3단계 : 썸네일 프리팹 만들기

썸네일 버튼을 프리팹으로 생성한 다음, 스크립트에서 인스턴스로 생성하는 방법을 사용하세요.

1\) UI > 버튼을 스크롤 뷰의 콘텐츠 자식으로 추가하고 이름을 preThumb로 변경하세요.

2\) Raw Image를 추가한 후 이름을 Thumb으로 변경하세요.

- 이 이미지는 썸네일이 됩니다. 크기를 적절하게 조정하세요.

3\) 텍스트를 추가하세요.

- 이미지의 하단을 중앙에 위치하도록 설정하세요.
- 글자의 크기와 두께를 조정하고, Content Size Fitter를 추가하세요.
  - 수평 맞춤 : 선호 크기
  - 수직 맞춤 : 선호 크기

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단계 : 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;**&#xBE44;디오에 표시된 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;
    }
 
}
```
:::

- 스크립트를 생성한 후, 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\) ZEPETO 캐릭터를 로드한 후 썸네일 생성을 위해 ContentsRequest() 사용자 정의 함수를 호출합니다.

- 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() 사용자 정의 함수 참조
  - 플레이어가 썸네일을 눌러 제스처를 만들 때, 제스처 ID를 room.Send()를 사용하여 서버에 전송합니다.
  - 제스처를 취소할 때, 취소했다는 정보를 전송하도록 처리합니다.
- 서버에서 다른 클라이언트로부터 제스처 정보를 받을 때: "OnChangeGesture" 룸 메시지가 Start() 내의 this.room.AddMessageHandler()로 전송됩니다.
  - 동기화는 "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);
        }
    }
}
```



- 스크립트를 완료한 후, 검사자는 Zepeto World Multiplay 구성 요소와 함께 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의 서버 코드를 기반으로 합니다.
- onCreate() 내에서 제스처가 변경될 때 다른 클라이언트에 제스처 정보를 전송하는 onMessage() 콜백을 생성합니다.

```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를 사용하여 Player 객체를 관리합니다.
        // 클라이언트는 players 객체에 add_OnAdd 이벤트를 추가하여 추가된 플레이어 객체에 대한 정보를 확인할 수 있습니다.
        this.state.players.set(client.sessionId, player);
    }
     
    onLeave(client: SandboxPlayer, consented?: boolean) {
 
        // allowReconnection을 설정하면 회로에 대한 연결을 유지할 수 있지만 기본 가이드에서 즉시 정리합니다.
        // 클라이언트는 players 객체에 add_OnRemove 이벤트를 추가하여 삭제된 플레이어 객체에 대한 정보를 확인할 수 있습니다.
        this.state.players.delete(client.sessionId);
    }
}
```

