---
title: 与物体互动
slug: world-sdk-guide-ch/Pxo-t83PZI_b_hKmPpMgY
docTags: 
createdAt: 2024-09-02T08:34:13.135Z
---

实现一个交互按钮，当ZEPETO角色接近一个物体时出现。



![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-o2OUpU-H0unEf2II---pB-20240904-102424.gif)



## 步骤 1 : 环境设置

- 您可以从以下链接下载用于交互示例和指南的动画和按钮资源。

:::hint{type="info"}
**📘 ZEPETO 交互示例**
[Zepeto 交互模块](https://github.com/naverz/zepeto-multiplay-example/tree/main/Assets/Zepeto%20Interaction%20Module)
:::

- 在场景中实现ZEPETO角色创建代码作为默认。

:::hint{type="info"}
**📘&#x20;**&#x8BF7;参考以下指南。 \[[ZEPETO 玩家](docId\:VeqpnpSmnKxkrDHcMuqtL)]
:::



## 步骤 2 : 设置对象

设置与 ZEPETO 角色交互的对象。



1\) 放置 ZEPETO 角色将与之交互的对象。

2\) 创建一个层级 > 创建空对象并将其重命名为 DockPoint。

- 这是 ZEPETO 角色将与之交互的点。调整对象的位置。

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



- 检查Unity编辑器顶部的变换小工具切换按钮是否为本地，并将Z轴（蓝色箭头）旋转到物体外部。

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



- 添加Collider组件后，检查isTrigger。
- 调整Collider的大小以匹配玩家可以与物体交互的范围。

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



3\) 创建层级 > 创建空对象作为DockPoint的子对象，并将其重命名为IconPos。

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



## 步骤 3 : 设置用户界面

1\) 创建层级 > 用户界面 > 画布，作为 ZEPETO 角色将要交互的对象的子项，并将其重命名为 PrefIconCanvas。

- 将渲染模式设置为世界空间。
- 将宽度和高度分别设置为 1。
- 在图形射线投射器组件上取消选中忽略反向图形选项。

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-biLqEI0N4uIicpgpv56ui-20240904-102423.png "画布设置示例")



2\) 创建层级 > 用户界面 > 按钮，作为 PrefIconCanvas 的子项。

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-nZpnkH0GsyFdwuadH5WKC-20240904-102423.png "按钮图像示例")



3\) 设置完成后，将其制作成Prefab，并删除层级中的剩余PrefIconCanvas。

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-4Jv152YJP6BCLC3LOe0rO-20240904-102423.png "Prefab配置示例")



## 步骤 4 : 编写脚本

### 步骤 4 - 1 : InteractionIcon

1\) 创建项目 > 创建 > ZEPETO > TypeScript 并将其重命名为 InteractionIcon。

2\) 编写如下示例脚本。

```typescript
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { Camera, Canvas, Collider, GameObject, Transform, Object } from 'UnityEngine';
import { Button } from 'UnityEngine.UI';
import { UnityEvent } from 'UnityEngine.Events';
import { ZepetoPlayers } from 'ZEPETO.Character.Controller';
 
export default class InteractionIcon extends ZepetoScriptBehaviour {
     
    // 图标
    @Header("[图标]")
    @SerializeField() private prefIconCanvas: GameObject;
    @SerializeField() private iconPosition: Transform;
     
    // Unity 事件    
    @Header("[Unity 事件]")
    public OnClickEvent: UnityEvent;
    public OnTriggerEnterEvent: UnityEvent;
    public OnTriggerExitEvent: UnityEvent;
 
    private _button: Button;
    private _canvas: Canvas;   
    private _cachedWorldCamera: Camera;
    private _isIconActive: boolean = false;
    private _isDoneFirstTrig: boolean = false;
     
     
    private Update() {
        if (this._isDoneFirstTrig && this._canvas?.gameObject.activeSelf) {
            this.UpdateIconRotation();
        }
    }
     
    private OnTriggerEnter(coll: Collider) {
        if (coll != ZepetoPlayers.instance.LocalPlayer?.zepetoPlayer?.character.GetComponent<Collider>()) {
            return;
        }
         
        this.ShowIcon();
        this.OnTriggerEnterEvent?.Invoke();
    }
 
    private OnTriggerExit(coll: Collider) {
        if (coll != ZepetoPlayers.instance.LocalPlayer?.zepetoPlayer?.character.GetComponent<Collider>()) {
            return;
        }
         
        this.HideIcon();
        this.OnTriggerExitEvent?.Invoke();
    }
     
    public ShowIcon(){
        if (!this._isDoneFirstTrig) {
            this.CreateIcon();
            this._isDoneFirstTrig = true;
        }
        else {
            this._canvas.gameObject.SetActive(true);
        }
        this._isIconActive = true;
    }
     
    public HideIcon() {
        this._canvas?.gameObject.SetActive(false);
        this._isIconActive = false;
    }
 
    private CreateIcon() {
        if (this._canvas === undefined) {
            const canvas = GameObject.Instantiate(this.prefIconCanvas, this.iconPosition) as GameObject;
            this._canvas = canvas.GetComponent<Canvas>();
            this._button = canvas.GetComponentInChildren<Button>();
            this._canvas.transform.position = this.iconPosition.position;
        }
        this._cachedWorldCamera = Object.FindObjectOfType<Camera>();
        this._canvas.worldCamera = this._cachedWorldCamera;
 
        this._button.onClick.AddListener(() => {
            this.OnClickIcon();
        });
    }
     
    private UpdateIconRotation() {
        this._canvas.transform.LookAt(this._cachedWorldCamera.transform);
    }
 
    private OnClickIcon() {
        this.OnClickEvent?.Invoke();
    }
}
```

- 脚本的流程如下：
  - 更新()
    - 调用 UpdateIconRotation() 自定义函数以旋转图标画布以匹配相机旋转。
  - OnTriggerEnter(), OnTriggerExit()
    - 当你进入碰撞体区域并检测到触发时，调用 ShowIcon() 自定义函数以激活图标。
    - 当你离开碰撞体区域时，调用 HideIcon() 自定义函数以禁用图标。



3\) 完成脚本创建后，将脚本添加到 DockPoint 对象中。

4\) 从检查器中分配 Pref 图标画布和图标位置。



![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-eTD65UaUNU-2_Yf4IoYMU-20240904-102423.png "示例脚本设置屏幕")



### 步骤 4 - 2 : 手势交互

1\) 创建项目 > 创建 > ZEPETO > TypeScript 并将其重命名为 GestureInteraction。

2\) 写一个类似下面的示例脚本。

```typescript
import { AnimationClip, Animator, HumanBodyBones, Physics, Transform, Vector3, WaitForEndOfFrame} from 'UnityEngine';
import { ZepetoScriptBehaviour } from 'ZEPETO.Script';
import { ZepetoPlayers, ZepetoCharacter } from "ZEPETO.Character.Controller";
 
import InteractionIcon from './InteractionIcon';
 
export default class GestureInteraction extends ZepetoScriptBehaviour {
 
    @SerializeField() private animationClip: AnimationClip;
    @SerializeField() private isSnapBone: boolean = true;
    @SerializeField() private bodyBone: HumanBodyBones;
    @SerializeField() private allowOverlap: boolean = false;
 
    private _interactionIcon: InteractionIcon;
    private _isFirst: boolean = true;
    private _localCharacter: ZepetoCharacter;
    private _outPosition: Vector3;
    private _playerGesturePosition: Vector3;
 
    private Start() {
        this._interactionIcon = this.transform.GetComponent<InteractionIcon>();
        ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
            this._localCharacter = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer.character;
        });
 
        this._interactionIcon.OnClickEvent.AddListener(()=> {
            // 当点击交互图标时
            this._interactionIcon.HideIcon();
            this.DoInteraction();
        });
    }
 
    private DoInteraction() {
        this._outPosition = this.transform.position;
 
        if (this.isSnapBone) {
            // 是否位置为空
            if (this.allowOverlap || this.FindOtherPlayerNum() < 1) {
                this._localCharacter.SetGesture(this.animationClip);
                this.StartCoroutine(this.SnapBone());
                this.StartCoroutine(this.WaitForExit());
            } else {
                // 座位已满。
                this._interactionIcon.ShowIcon();
            }
        } else {
            this._localCharacter.SetGesture(this.animationClip);
            this.StartCoroutine(this.WaitForExit());
        }
    }
 
    private *SnapBone() {
        const animator: Animator = this._localCharacter.ZepetoAnimator;
        const bone: Transform = animator.GetBoneTransform(this.bodyBone);
 
        let idx = 0;
        while(true) {
            const distance = Vector3.op_Subtraction(bone.position, this._localCharacter.transform.position);
            const newPos: Vector3 = Vector3.op_Subtraction(this.transform.position, distance);
 
            this._playerGesturePosition = newPos;
            this._localCharacter.transform.position = this._playerGesturePosition;
            this._localCharacter.transform.rotation = this.transform.rotation;
            yield new WaitForEndOfFrame();
            idx++;
            // 在5帧动画期间校准位置。
            if (idx > 5) {
                return;
            }
        }
    }
 
    // 确切的方法必须通过服务器代码， 
    // 但它是由本地客户端计算以优化服务器。
    private FindOtherPlayerNum() {
        const hitInfos = Physics.OverlapSphere(this.transform.position, 0.1);
 
        let playerNum = 0;
        if (hitInfos.length > 0) {
            hitInfos.forEach((hitInfo) => {
                if (hitInfo.transform.GetComponent<ZepetoCharacter>()) {
                    playerNum ++;
                }
            });
        }
        return playerNum;
    }
 
    private *WaitForExit() {
        if (this._localCharacter) {
            while (true) {
                if (this._localCharacter.tryJump || this._localCharacter.tryMove) {
                    this._localCharacter.CancelGesture();
 
                    this.transform.position = this._outPosition;
                    this._interactionIcon.ShowIcon();
                    break;
                } else if(this.isSnapBone && this._playerGesturePosition != this._localCharacter.transform.position){
                    this._interactionIcon.ShowIcon();
                    break;
                }
                yield;
            }
        }
    }
}
```

- 脚本的流程如下：
  - 开始()
    - 当图标被点击时，它会停用并调用 DoInteraction() 自定义函数。
  - DoInteraction()
    - 如果 isSnapBone 被选中，
      - 如果座位是空的（allowOverlap 被选中，或者 FindOtherPlayerNum() 自定义函数返回值小于 1）
        - 获取分配给 animationClip 的手势。
        - 启动 SnapBone() 协程并将 ZEPETO 角色的 bodyBone 附加到 targetTranform。
        - 启动 WaitForExit() 协程。
          - 当 ZEPETO 角色跳跃或移动，或离开碰撞体区域时，取消手势并激活图标。
      - 当座位容量满时激活图标。
    - 如果 isSnapBone 没有被选中，
      - 获取分配给 animationClip 的手势。
      - 启动 WaitForExit() 协程。
  - FindOtherPlayerNum()
    - 查找存在于附加脚本的对象位置的 ZEPETO 角色，并返回有多少个。
    - 您可以检查座位是否为空。



3\) 完成脚本创建后，将脚本添加到 DockPoint 对象中。

4\) 在检查器中分配动画剪辑、是否为快照骨骼、身体骨骼和允许重叠。

- 分配动画剪辑。这些是与交互相关的手势。
- 检查是否为快照骨骼。确保分配给身体骨骼的部分被定位为停靠点。
- 将身体骨骼设置为臀部。确保臀部被定位在停靠点，因为这将是一个坐姿手势。
- 允许重叠可以让你决定是否可以让多个人坐在一个座位上。

![](https://api.archbee.com/api/optimize/fCt3n1oCa8rgNJ8fw9I2N-cBvVJDHLbk1Fm5Io1E7N_-20240904-102423.png "示例脚本设置屏幕")



## 步骤 5 : 玩

当 ZEPETO 角色接近物体时，按钮会出现，当它远离时，按钮会消失。

如果您设置的手势在接近和与按钮交互时播放，那就是成功。

除了手势外，还可以实现各种事件在交互后发生。

以下是实现一个事件的示例，该事件在交互后创建一个物品。

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

:::hint{type="info"}
**📘 Zepeto 世界示例 - 第 3 章 交互示例**
[https://github.com/naverz/zepeto-world-sample/tree/main/Assets/Chapter3](https://github.com/naverz/zepeto-world-sample/tree/main/Assets/Chapter3)
:::

