บทเรียน
บทเรียนการเล่นหลายคน
15min
📘 ตัวอย่าง Multiplay https://github.com/naverz/zepeto-multiplay-example
สรุป | จากการสร้างเซิร์ฟเวอร์ Multiplay ไปยังไคลเอนต์ ตั้งค่าสภาพแวดล้อมที่จำเป็นในการพัฒนาโลก Multiplay. |
---|---|
ความยาก | ระดับกลาง |
เวลาที่ต้องใช้ | 1 ชั่วโมง |
- จากการสร้างเซิร์ฟเวอร์ Multiplay ไปยังไคลเอนต์ ตั้งค่าสภาพแวดล้อมที่จำเป็นในการพัฒนาโลก Multiplay.
- เรียนรู้เกี่ยวกับ Schema ที่จำเป็นสำหรับการสื่อสารระหว่างเซิร์ฟเวอร์และไคลเอนต์ และกำหนดประเภท Schema และสถานะห้อง.
- โปรดทราบว่า สคริปต์เซิร์ฟเวอร์ที่ใช้ในวิดีโอ ซึ่งรวมถึงเนื้อหาที่เกี่ยวข้องกับ hashCode ไม่ได้รับการสนับสนุนอีกต่อไป.
- ดังนั้น ให้ละเว้นโค้ดต่อไปนี้เมื่อเขียน.
TypeScript
1if (client.hashCode) {
2 player.zepetoHash = client.hashCode;
3}
- เขียนตรรกะของโลกที่จำเป็นในการซิงค์จากตำแหน่งของผู้เล่นไปยังทางออกของผู้เล่น.
- รันเซิร์ฟเวอร์และเชื่อมต่อกับ Multiplay.
schemas.json
1{
2"State" : {"players" : {"map" : "ผู้เล่น"}},
3"Player" : {"sessionId" : "string","zepetoUserId" : "string","transform" : "การเปลี่ยนแปลง","state" : "number","subState" : "number"},
4"Transform" : {"position" : "Vector3","rotation" : "Vector3"},
5"Vector3" : {"x" : "number","y" : "number","z" : "number"}
6}
index.ts
1import {Sandbox, SandboxOptions, SandboxPlayer} from "ZEPETO.Multiplay";
2import {DataStorage} from "ZEPETO.Multiplay.DataStorage";
3import {Player, Transform, Vector3} from "ZEPETO.Multiplay.Schema";
4
5export default class extends Sandbox {
6
7
8 storageMap:Map<string,DataStorage> = new Map<string, DataStorage>();
9
10 constructor() {
11 super();
12 }
13
14 onCreate(options: SandboxOptions) {
15 // ถูกเรียกเมื่อวัตถุห้องถูกสร้างขึ้น
16 // จัดการสถานะหรือการเริ่มต้นข้อมูลของวัตถุห้อง
17
18 this.onMessage("onChangedTransform", (client, message) => {
19 const player = this.state.players.get(client.sessionId);
20
21 const transform = new Transform();
22 transform.position = new Vector3();
23 transform.position.x = message.position.x;
24 transform.position.y = message.position.y;
25 transform.position.z = message.position.z;
26
27 transform.rotation = new Vector3();
28 transform.rotation.x = message.rotation.x;
29 transform.rotation.y = message.rotation.y;
30 transform.rotation.z = message.rotation.z;
31
32 if (player) {
33 player.transform = transform;
34 }
35 });
36
37 this.onMessage("onChangedState", (client, message) => {
38 const player = this.state.players.get(client.sessionId);
39 if (player) {
40 player.state = message.state;
41 player.subState = message.subState; // ตัวควบคุมตัวละคร V2
42 }
43 });
44 }
45
46
47
48 async onJoin(client: SandboxPlayer) {
49
50 // สร้างวัตถุผู้เล่นที่กำหนดใน schemas.json และตั้งค่าค่าตั้งต้น
51 console.log(`[OnJoin] sessionId : ${client.sessionId}, userId : ${client.userId}`)
52
53 const player = new Player();
54 player.sessionId = client.sessionId;
55
56 if (client.userId) {
57 player.zepetoUserId = client.userId;
58 }
59
60 // [DataStorage] การโหลด DataStorage ของผู้เล่นที่เข้ามา
61 const storage: DataStorage = client.loadDataStorage();
62
63 this.storageMap.set(client.sessionId,storage);
64
65 let visit_cnt = await storage.get("VisitCount") as number;
66 if (visit_cnt == null) visit_cnt = 0;
67
68 console.log(`[OnJoin] จำนวนการเยี่ยมชมของ ${client.sessionId} : ${visit_cnt}`)
69
70 // [DataStorage] อัปเดตจำนวนการเยี่ยมชมของผู้เล่นและบันทึก Storage
71 await storage.set("VisitCount", ++visit_cnt);
72
73 // จัดการวัตถุผู้เล่นโดยใช้ sessionId ซึ่งเป็นค่าคีย์ที่ไม่ซ้ำกันของวัตถุคลาย
74 // คลายสามารถตรวจสอบข้อมูลเกี่ยวกับวัตถุผู้เล่นที่เพิ่มโดยการตั้งค่าโดยการเพิ่มเหตุการณ์ add_OnAdd ไปยังวัตถุผู้เล่น
75 this.state.players.set(client.sessionId, player);
76 }
77
78 onTick(deltaTime: number): void {
79 // ถูกเรียกซ้ำที่เวลาที่ตั้งไว้ในเซิร์ฟเวอร์ และสามารถจัดการเหตุการณ์ในช่วงเวลาที่แน่นอนได้โดยใช้ deltaTime
80 }
81
82 async onLeave(client: SandboxPlayer, consented?: boolean) {
83
84 // โดยการตั้งค่า allowReconnection จะสามารถรักษาการเชื่อมต่อสำหรับวงจรได้ แต่จะทำความสะอาดทันทีในคู่มือพื้นฐาน
85 // คลายสามารถตรวจสอบข้อมูลเกี่ยวกับวัตถุผู้เล่นที่ถูกลบโดยการเพิ่มเหตุการณ์ add_OnRemove ไปยังวัตถุผู้เล่น
86 this.state.players.delete(client.sessionId);
87 }
88}
ClientStarter.ts
1import {ZepetoScriptBehaviour} from 'ZEPETO.Script'
2import {ZepetoWorldMultiplay} from 'ZEPETO.World'
3import {Room, RoomData} from 'ZEPETO.Multiplay'
4import {Player, State, Vector3} from 'ZEPETO.Multiplay.Schema'
5import {CharacterState, SpawnInfo, ZepetoPlayers, ZepetoPlayer, CharacterJumpState} from 'ZEPETO.Character.Controller'
6import * as UnityEngine from "UnityEngine";
7
8
9export default class ClientStarterV2 extends ZepetoScriptBehaviour {
10
11 public multiplay: ZepetoWorldMultiplay;
12
13 private room: Room;
14 private currentPlayers: Map<string, Player> = new Map<string, Player>();
15
16 private zepetoPlayer: ZepetoPlayer;
17
18 private Start() {
19
20 this.multiplay.RoomCreated += (room: Room) => {
21 this.room = room;
22 };
23
24 this.multiplay.RoomJoined += (room: Room) => {
25 room.OnStateChange += this.OnStateChange;
26 };
27
28 this.StartCoroutine(this.SendMessageLoop(0.04));
29 }
30
31 // ส่งการแปลงตัวละครท้องถิ่นไปยังเซิร์ฟเวอร์ที่เวลาที่กำหนด
32 private* SendMessageLoop(tick: number) {
33 while (true) {
34 yield new UnityEngine.WaitForSeconds(tick);
35
36 if (this.room != null && this.room.IsConnected) {
37 const hasPlayer = ZepetoPlayers.instance.HasPlayer(this.room.SessionId);
38 if (hasPlayer) {
39 const character = ZepetoPlayers.instance.GetPlayer(this.room.SessionId).character;
40 this.SendTransform(character.transform);
41 this.SendState(character.CurrentState);
42 }
43 }
44 }
45 }
46
47 private OnStateChange(state: State, isFirst: boolean) {
48
49 // เมื่อได้รับเหตุการณ์ OnStateChange ครั้งแรก จะมีการบันทึกภาพรวมของสถานะทั้งหมด
50 if (isFirst) {
51
52 // [CharacterController] (ท้องถิ่น) ถูกเรียกเมื่ออินสแตนซ์ผู้เล่นถูกโหลดอย่างสมบูรณ์ในฉาก
53 ZepetoPlayers.instance.OnAddedLocalPlayer.AddListener(() => {
54 const myPlayer = ZepetoPlayers.instance.LocalPlayer.zepetoPlayer;
55 this.zepetoPlayer = myPlayer;
56 });
57
58 // [CharacterController] (ท้องถิ่น) ถูกเรียกเมื่ออินสแตนซ์ผู้เล่นถูกโหลดอย่างสมบูรณ์ในฉาก
59 ZepetoPlayers.instance.OnAddedPlayer.AddListener((sessionId: string) => {
60 const isLocal = this.room.SessionId === sessionId;
61 if (!isLocal) {
62 const player: Player = this.currentPlayers.get(sessionId);
63
64 // [RoomState] ถูกเรียกเมื่อใดก็ตามที่สถานะของอินสแตนซ์ผู้เล่นถูกอัปเดต
65 player.OnChange += (changeValues) => this.OnUpdatePlayer(sessionId, player);
66 }
67 });
68 }
69
70 let join = new Map<string, Player>();
71 let leave = new Map<string, Player>(this.currentPlayers);
72
73 state.players.ForEach((sessionId: string, player: Player) => {
74 if (!this.currentPlayers.has(sessionId)) {
75 join.set(sessionId, player);
76 }
77 leave.delete(sessionId);
78 });
79
80 // [RoomState] สร้างอินสแตนซ์ผู้เล่นสำหรับผู้เล่นที่เข้าห้อง
81 join.forEach((player: Player, sessionId: string) => this.OnJoinPlayer(sessionId, player));
82
83 // [RoomState] ลบอินสแตนซ์ผู้เล่นสำหรับผู้เล่นที่ออกจากห้อง
84 leave.forEach((player: Player, sessionId: string) => this.OnLeavePlayer(sessionId, player));
85 }
86
87 private OnJoinPlayer(sessionId: string, player: Player) {
88 console.log(`[OnJoinPlayer] players - sessionId : ${sessionId}`);
89 this.currentPlayers.set(sessionId, player);
90
91 const spawnInfo = new SpawnInfo();
92 const position = this.ParseVector3(player.transform.position);
93 const rotation = this.ParseVector3(player.transform.rotation);
94 spawnInfo.position = position;
95 spawnInfo.rotation = UnityEngine.Quaternion.Euler(rotation);
96
97 const isLocal = this.room.SessionId === player.sessionId;
98 ZepetoPlayers.instance.CreatePlayerWithUserId(sessionId, player.zepetoUserId, spawnInfo, isLocal);
99 }
100
101 private OnLeavePlayer(sessionId: string, player: Player) {
102 console.log(`[OnRemove] players - sessionId : ${sessionId}`);
103 this.currentPlayers.delete(sessionId);
104
105 ZepetoPlayers.instance.RemovePlayer(sessionId);
106 }
107
108 private OnUpdatePlayer(sessionId: string, player: Player) {
109
110 const position = this.ParseVector3(player.transform.position);
111
112 const zepetoPlayer = ZepetoPlayers.instance.GetPlayer(sessionId);
113
114 var moveDir = UnityEngine.Vector3.op_Subtraction(position, zepetoPlayer.character.transform.position);
115 moveDir = new UnityEngine.Vector3(moveDir.x, 0, moveDir.z);
116
117 if (moveDir.magnitude < 0.05) {
118 if (player.state === CharacterState.MoveTurn)
119 return;
120 zepetoPlayer.character.StopMoving();
121 } else {
122 zepetoPlayer.character.MoveContinuously(moveDir);
123 }
124
125 if (player.state === CharacterState.Jump) {
126 if (zepetoPlayer.character.CurrentState !== CharacterState.Jump) {
127 zepetoPlayer.character.Jump();
128 }
129
130 if (player.subState === CharacterJumpState.JumpDouble) {
131 zepetoPlayer.character.DoubleJump();
132 }
133 }
134 }
135
136 private SendTransform(transform: UnityEngine.Transform) {
137 const data = new RoomData();
138
139 const pos = new RoomData();
140 pos.Add("x", transform.localPosition.x);
141 pos.Add("y", transform.localPosition.y);
142 pos.Add("z", transform.localPosition.z);
143 data.Add("position", pos.GetObject());
144
145 const rot = new RoomData();
146 rot.Add("x", transform.localEulerAngles.x);
147 rot.Add("y", transform.localEulerAngles.y);
148 rot.Add("z", transform.localEulerAngles.z);
149 data.Add("rotation", rot.GetObject());
150 this.room.Send("onChangedTransform", data.GetObject());
151 }
152
153 private SendState(state: CharacterState) {
154 const data = new RoomData();
155 data.Add("state", state);
156 if(state === CharacterState.Jump) {
157 data.Add("subState", this.zepetoPlayer.character.MotionV2.CurrentJumpState);
158 }
159 this.room.Send("onChangedState", data.GetObject());
160 }
161
162 private ParseVector3(vector3: Vector3): UnityEngine.Vector3 {
163 return new UnityEngine.Vector3
164 (
165 vector3.x,
166 vector3.y,
167 vector3.z
168 );
169 }
170}
อัปเดต 11 Oct 2024
หน้านี้ช่วยคุณได้หรือไม่?