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

步骤 1 : 环境设置
- 您可以从以下链接下载用于交互示例和指南的动画和按钮资源。
📘 ZEPETO 交互示例 Zepeto 交互模块
- 在场景中实现ZEPETO角色创建代码作为默认。
📘 请参考以下指南。 [ZEPETO 玩家]
步骤 2 : 设置对象
设置与 ZEPETO 角色交互的对象。
1) 放置 ZEPETO 角色将与之交互的对象。
2) 创建一个层级 > 创建空对象并将其重命名为 DockPoint。
- 这是 ZEPETO 角色将与之交互的点。调整对象的位置。

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

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

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

步骤 3 : 设置用户界面
1) 创建层级 > 用户界面 > 画布,作为 ZEPETO 角色将要交互的对象的子项,并将其重命名为 PrefIconCanvas。
- 将渲染模式设置为世界空间。
- 将宽度和高度分别设置为 1。
- 在图形射线投射器组件上取消选中忽略反向图形选项。

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

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

步骤 4 : 编写脚本
步骤 4 - 1 : InteractionIcon
1) 创建项目 > 创建 > ZEPETO > TypeScript 并将其重命名为 InteractionIcon。
2) 编写如下示例脚本。
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 图标画布和图标位置。

步骤 4 - 2 : 手势交互
1) 创建项目 > 创建 > ZEPETO > TypeScript 并将其重命名为 GestureInteraction。
2) 写一个类似下面的示例脚本。
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) 在检查器中分配动画剪辑、是否为快照骨骼、身体骨骼和允许重叠。
- 分配动画剪辑。这些是与交互相关的手势。
- 检查是否为快照骨骼。确保分配给身体骨骼的部分被定位为停靠点。
- 将身体骨骼设置为臀部。确保臀部被定位在停靠点,因为这将是一个坐姿手势。
- 允许重叠可以让你决定是否可以让多个人坐在一个座位上。

步骤 5 : 玩
当 ZEPETO 角色接近物体时,按钮会出现,当它远离时,按钮会消失。
如果您设置的手势在接近和与按钮交互时播放,那就是成功。
除了手势外,还可以实现各种事件在交互后发生。
以下是实现一个事件的示例,该事件在交互后创建一个物品。
📘 Zepeto 世界示例 - 第 3 章 交互示例 https://github.com/naverz/zepeto-world-sample/tree/main/Assets/Chapter3