创造你的世界
玩家和角色:基础
ZEPETO 玩家
18 分
zepetoplayers 是一个用于控制 zepeto 玩家和 zepeto 角色的管理器(单例)类。 通过将 zepetoplayers 添加到场景中,您可以创建、删除和操作 zepeto 玩家。 zepetoplayer 代表一个 zepeto 角色的单独实例,用于管理您在多人世界中直接控制的玩家和其他玩家。 在多人世界中创建的每个 zepetoplayer 都会分配一个唯一的会话 id,并使用该会话 id 进行管理。 因为 zepetoplayer 具有 zepetocharacter 的属性,所以可以使用与 zepetocharacter 相关的函数进行控制。 有三种类型的 zepetoplayer: zepetoplayer 描述 本地玩家 表示一个由本地用户直接控制的zepeto角色实例。 \ 附带角色控制器/zepeto相机组件。 网络玩家(远程玩家) 一个可以在多人游戏内容中加载和使用的zepeto角色实例。 \ 没有附带角色控制器/zepeto相机组件。 机器人玩家 一个用于多人游戏内容的zepeto角色实例,但由机器人控制,而不是由真实用户控制。 在启动玩家不足的多人世界时或玩家在游戏中离开时用作替代。 \ 没有附带角色控制器/zepeto相机组件。 请参考以下指南。 \[ zepeto 玩家 docid\ veqpnpsmnkxkrdhcmuqtl ] \[ 机器人玩家创建指南 docid\ paizjygsu1vrgomzpreja ] zepetocharacter 是zepeto角色的基本实例单元,可以在世界场景中加载和控制。 zepetocharacter具有通过zepeto应用程序创建的头像的外观。 请参考以下指南。 \[ zepeto角色 docid\ fjx8dujgdjma8i55zwctt ] 添加zepetoplayers 在层级窗口中,选择zepeto → zepetoplayers选项卡。 您现在可以将其添加到您的场景中。 请注意,仅添加 zepetoplayers 并不会将 zepeto player 引入场景。您需要使用 zepetoplayers 的角色创建 api 实现一个脚本。 如果您希望快速创建并尝试仅在场景中的本地玩家,请参考以下指南: 请参考以下指南。 \[ 创建一个zepeto角色 docid\ x uqcezxzv rams18k0bt ] 有关如何显示zepeto玩家的名称和头像的示例,请查看指南: 请参考以下指南。 \[ 用户信息 docid\ llhncmccxktu lkeb14qb ] zepeto 玩家 api 如果您对zepetoplayers api感兴趣,请参考文档: 请参考以下指南。 \[ zepeto character controller api ] 本指南主要涵盖在多人场景中使用zepeto玩家的示例。 使用zepeto玩家实现多人位置同步 在单人世界中,只需要创建一个本地玩家,由于只有本地玩家出现在屏幕上,因此不需要同步。 然而,在多人世界中,不仅需要显示您直接控制的本地玩家,还需要显示其他玩家,称为网络玩家。 网络玩家的每一个动作——移动、跳跃和执行特定手势——都应该在您的屏幕上完全相同。 这个过程称为同步。 如果不实现同步脚本,您将无法看到网络玩家的外观或移动。 在没有同步的多人游戏世界中,知道另一个客户端是否已进入房间的唯一方法是通过主页按钮。 步骤 1 设置多人游戏环境 建议通过多人游戏教程视频来了解多人游戏的基本设置和概念。 请参考以下指南。 \[ 创建多人游戏 docid\ spk tardrdz3td3r6gnae ] \[ 多人游戏 docid\ vrcnxwq2u9dbmamyi6 9d ] 步骤 2 在您的屏幕上显示其他玩家 您的本地玩家被视为其他设备上的网络玩家。 这意味着即使是您的本地玩家也必须向服务器发送信息以进行同步。 所有连接到多人游戏世界的玩家应共享他们的信息。 所有连接到多人游戏房间的客户端共享多人游戏房间状态数据。 此房间状态数据遵循在schemas json中定义的模式。 (请将 schema 视为数据结构) 在本指南中,您将通过房间状态数据同步玩家的位置。因此,在 schemas json 中定义一个可以表示每个玩家位置数据的 schema。 schema json { "state" {"players" {"map" "player"}}, "player" {"sessionid" "string","zepetouserid" "string","transform" "transform","state" "number","substate" "number"}, "transform" {"position" "vector3","rotation" "vector3"}, "vector3" {"x" "number","y" "number","z" "number"} } 请参考以下指南。 \[ 多人游戏房间状态 docid\ zax2kvvwljjodqfn eddd ] 👍 提示 将所有服务器和所有客户端应共享的信息存储在 多人房间状态。 为每个玩家保存单独的数据,例如等级、经验值、分数等,使用 数据存储。 服务器脚本可以识别当另一个玩家进入房间时,并可以将该信息发送给客户端以将该玩家加载到场景中。 步骤 2 1 基本服务器脚本 当玩家进入多人游戏世界的房间时,调用 onjoin()。 在服务器脚本中,将连接玩家的信息添加到房间状态的玩家列表中。 此外,当玩家退出房间时,调用 onleave()。 服务器脚本将从房间状态的玩家中移除离开的玩家的信息。 import {sandbox, sandboxoptions, sandboxplayer} from "zepeto multiplay"; import {player} from "zepeto multiplay schema"; export default class extends sandbox { oncreate(options sandboxoptions) { } async onjoin(client sandboxplayer) { 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); } async onleave(client sandboxplayer, consented? boolean) { this state players delete(client sessionid); } } 步骤 2 2 基本客户端脚本 在创建多人游戏世界时,需要一个客户端脚本与服务器进行通信。 下面是一个用于多人游戏的基本客户端脚本示例。 在客户端,我们使用 currentplayers 映射数据结构来管理要显示的玩家数据。 下面展示了关键代码行: zepetoplayers instance createplayerwithuserid(sessionid, player zepetouserid, spawninfo, islocal); 当客户端从服务器的房间状态接收到玩家信息,并且有新玩家加入房间时,将分配一个会话 id。如果正在创建的玩家的会话 id 与我们自己的会话 id 匹配,则该玩家被视为本地玩家。在这种情况下,玩家将被实例化为 islocal = true , 表示这是本地玩家。 随后,非本地玩家被创建为 islocal = false 。这确保了所有玩家的外观,无论是本地还是非本地,都在屏幕上呈现。 import {zepetoscriptbehaviour} from 'zepeto script' import {zepetoworldmultiplay} from 'zepeto world' import {room, roomdata} from 'zepeto multiplay' import {player, state, vector3} from 'zepeto multiplay schema' import {characterstate, spawninfo, zepetoplayers, zepetoplayer, characterjumpstate} from 'zepeto character controller' import as unityengine from "unityengine"; export default class multiplayclientcode extends zepetoscriptbehaviour { public multiplay zepetoworldmultiplay; private room room; private currentplayers map\<string, player> = new map\<string, player>(); private zepetoplayer zepetoplayer; private start() { this multiplay roomcreated += (room room) => { this room = room; }; this multiplay roomjoined += (room room) => { room onstatechange += this onstatechange; }; } private onstatechange(state state, isfirst boolean) { // 当接收到第一个 onstatechange 事件时,记录完整的状态快照。 if (isfirst) { // \[charactercontroller] (本地) 当玩家实例在场景中完全加载时调用 zepetoplayers instance onaddedlocalplayer addlistener(() => { const myplayer = zepetoplayers instance localplayer zepetoplayer; this zepetoplayer = myplayer; }); // \[charactercontroller] (本地) 当玩家实例在场景中完全加载时调用 zepetoplayers instance onaddedplayer addlistener((sessionid string) => { const islocal = this room sessionid === sessionid; if (!islocal) { const player player = this currentplayers get(sessionid); } }); } let join = new map\<string, player>(); let leave = new map\<string, player>(this currentplayers); state players foreach((sessionid string, player player) => { if (!this currentplayers has(sessionid)) { join set(sessionid, player); } leave delete(sessionid); }); // \[roomstate] 为进入房间的玩家创建玩家实例 join foreach((player player, sessionid string) => this onjoinplayer(sessionid, player)); // \[roomstate] 移除退出房间的玩家实例 leave foreach((player player, sessionid string) => this onleaveplayer(sessionid, player)); } private onjoinplayer(sessionid string, player player) { console log(`\[onjoinplayer] players sessionid ${sessionid}`); this currentplayers set(sessionid, player); // 创建玩家 const islocal = this room sessionid === player sessionid; zepetoplayers instance createplayerwithuserid(sessionid, player zepetouserid, new spawninfo(), islocal); } private onleaveplayer(sessionid string, player player) { console log(`\[onremove] players sessionid ${sessionid}`); this currentplayers delete(sessionid); zepetoplayers instance removeplayer(sessionid); } } 现在,当玩家进入时,您可以确认zepeto角色已在您的屏幕上创建。 但是,玩家的移动尚未在屏幕上反映出来。 接下来,让我们继续同步位置。 步骤 3 通过获取其他玩家的信息进行同步 为了同步,每当玩家移动或采取某些行动时,他们必须将状态更改发送到服务器。 通过房间消息通信将包含状态更改的消息发送到服务器。 当服务器收到有关玩家状态更改的消息时,它会更新房间状态。 请参考以下指南。 \[ multiplay room message docid\ wwza8fxwmtcm7xkfepebd ] 例如,假设您的本地玩家名为b。 当网络玩家a加入房间时,他们会在坐标x 0, y 0, z 0处实例化。 如果玩家a移动到位置x 10, y 0, z 0,b实际上无法知道a正在移动。 这是因为两者在不同的设备上本地操作,因此在不同的客户端上。 因此,控制a的人需要通过房间消息将他们的移动传达给服务器。 一旦服务器收到这些信息,它会通知房间内的所有人a的实时位置。这样,b最终意识到a正在移动。 为了让服务器通知其他玩家,有两种方法: 使用房间消息广播。 更新房间状态,然后让客户端获取并应用房间状态。 本指南使用更新房间状态的第二种方法。 考虑到加载到场景中的 zepeto 玩家具有 zepeto 角色属性,您可以使用 zepeto 角色功能来指挥移动到特定位置或发起跳跃。 为了让 b 的客户端可视化 a 移动到 x 10, y 0, z 0,最直观的方法是使用 movetoposition()。此功能将把玩家移动到从服务器接收到的 a 的最新位置。 不仅仅是位置变化,手势、技能使用、物品收集和所有状态变化都需要服务器与客户端之间的通信以进行同步。 您需要实现同步,以保持网络中每个动作的和谐。 👍 同步概念总结 当本地玩家状态发生变化时,他们通过房间消息将其发送到服务器。 服务器通知除本地玩家之外的所有其他玩家状态变化。 在接收到状态变化消息后,客户端代码更新发送消息的玩家的状态。 步骤 3 1 完成位置同步的服务器脚本 在基本服务器脚本中,需要额外的实现来更新房间状态,每当收到来自本地玩家客户端的状态变化消息时。 import {sandbox, sandboxoptions, sandboxplayer} from "zepeto multiplay"; import {player, transform, vector3} from "zepeto multiplay schema"; export default class extends sandbox { constructor() { super(); } 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; if (player) { player transform = transform; } }); this onmessage("onchangedstate", (client, message) => { const player = this state players get(client sessionid); if (player) { player state = message state; player substate = message substate; // 角色控制器 v2 } }); } async 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); } async onleave(client sandboxplayer, consented? boolean) { // 通过设置 allowreconnection,可以保持电路的连接,但在基本指南中立即清理。 // 客户端可以通过将 add onremove 事件添加到 players 对象来检查有关已删除玩家对象的信息。 this state players delete(client sessionid); } } 步骤 3 2 完成位置同步的客户端脚本 基本客户端脚本中的关键实现是: 当服务器的房间状态改变时,自动调用 onstatechange。 使用 sendmessageloop(0 04) 函数,每0 04秒将本地玩家的位置和角色跳跃状态信息发送到服务器。 import {zepetoscriptbehaviour} from 'zepeto script' import {zepetoworldmultiplay} from 'zepeto world' import {room, roomdata} from 'zepeto multiplay' import {player, state, vector3} from 'zepeto multiplay schema' import {characterstate, spawninfo, zepetoplayers, zepetoplayer, characterjumpstate} from 'zepeto character controller' import as unityengine from "unityengine"; export default class clientstarterv2 extends zepetoscriptbehaviour { public multiplay zepetoworldmultiplay; private room room; private currentplayers map\<string, player> = new map\<string, player>(); private zepetoplayer zepetoplayer; private start() { this multiplay roomcreated += (room room) => { this room = room; }; this multiplay roomjoined += (room room) => { room onstatechange += this onstatechange; }; this startcoroutine(this sendmessageloop(0 04)); } // 在预定的间隔时间内将本地角色变换发送到服务器。 private sendmessageloop(tick number) { while (true) { yield new unityengine waitforseconds(tick); if (this room != null && this room isconnected) { const hasplayer = zepetoplayers instance hasplayer(this room sessionid); if (hasplayer) { const character = zepetoplayers instance getplayer(this room sessionid) character; this sendtransform(character transform); this sendstate(character currentstate); } } } } private onstatechange(state state, isfirst boolean) { // 当接收到第一个 onstatechange 事件时,记录完整的状态快照。 if (isfirst) { // \[charactercontroller] (local) 当玩家实例在场景中完全加载时调用 zepetoplayers instance onaddedlocalplayer addlistener(() => { const myplayer = zepetoplayers instance localplayer zepetoplayer; this zepetoplayer = myplayer; }); // \[charactercontroller] (local) 当玩家实例在场景中完全加载时调用 zepetoplayers instance onaddedplayer addlistener((sessionid string) => { const islocal = this room sessionid === sessionid; if (!islocal) { const player player = this currentplayers get(sessionid); // \[roomstate] 每当玩家实例的状态更新时调用。 player onchange += (changevalues) => this onupdateplayer(sessionid, player); } }); } let join = new map\<string, player>(); let leave = new map\<string, player>(this currentplayers); state players foreach((sessionid string, player player) => { if (!this currentplayers has(sessionid)) { join set(sessionid, player); } leave delete(sessionid); }); // \[roomstate] 为进入房间的玩家创建玩家实例 join foreach((player player, sessionid string) => this onjoinplayer(sessionid, player)); // \[roomstate] 移除退出房间的玩家实例 leave foreach((player player, sessionid string) => this onleaveplayer(sessionid, player)); } private onjoinplayer(sessionid string, player player) { console log(`\[onjoinplayer] players sessionid ${sessionid}`); this currentplayers set(sessionid, player); const spawninfo = new spawninfo(); const position = this parsevector3(player transform position); const rotation = this parsevector3(player transform rotation); spawninfo position = position; spawninfo rotation = unityengine quaternion euler(rotation); const islocal = this room sessionid === player sessionid; zepetoplayers instance createplayerwithuserid(sessionid, player zepetouserid, spawninfo, islocal); } private onleaveplayer(sessionid string, player player) { console log(`\[onremove] players sessionid ${sessionid}`); this currentplayers delete(sessionid); zepetoplayers instance removeplayer(sessionid); } private onupdateplayer(sessionid string, player player) { const position = this parsevector3(player transform position); const zepetoplayer = zepetoplayers instance getplayer(sessionid); var movedir = unityengine vector3 op subtraction(position, zepetoplayer character transform position); movedir = new unityengine vector3(movedir x, 0, movedir z); if (movedir magnitude < 0 05) { if (player state === characterstate moveturn) return; zepetoplayer character stopmoving(); } else { zepetoplayer character movecontinuously(movedir); } if (player state === characterstate jump) { if (zepetoplayer character currentstate !== characterstate jump) { zepetoplayer character jump(); } if (player substate === characterjumpstate jumpdouble) { zepetoplayer character doublejump(); } } } private sendtransform(transform unityengine transform) { const data = new roomdata(); const pos = new roomdata(); pos add("x", transform localposition x); pos add("y", transform localposition y); pos add("z", transform localposition z); data add("position", pos getobject()); const rot = new roomdata(); rot add("x", transform localeulerangles x); rot add("y", transform localeulerangles y); rot add("z", transform localeulerangles z); data add("rotation", rot getobject()); this room send("onchangedtransform", data getobject()); } private sendstate(state characterstate) { const data = new roomdata(); data add("state", state); if(state === characterstate jump) { data add("substate", this zepetoplayer character motionv2 currentjumpstate); } this room send("onchangedstate", data getobject()); } private parsevector3(vector3 vector3) unityengine vector3 { return new unityengine vector3 ( vector3 x, vector3 y, vector3 z ); } } 👍 提示 本指南仅实现位置同步。手势同步、对象同步等尚未实现。 原理对所有人都是相同的,但在每个必要时刻发送和接收房间消息的过程是必要的。 如果您想更方便地实现同步,请考虑使用多玩家同步模块。