[Base Class](../groups/Extension.Base Class.md) / ModuleService
ModuleService Class
Server client and data module management
When you start developing an online game, you find that the client and server always need to be considered. The achieve of multiplayer games is not simple. If you want to add multiplayer games to the game, you should consider it properly in the design and development as soon as possible.
- Why is it divided into client and server?
In game development, there are several main reasons for dividing games into client-side and server-side:
Division of labor and cooperation: The client and server are each responsible for different tasks and functions. The client is mainly responsible for processing Player 'input, rendering and displaying game graphics, while the server is responsible for processing game logic, data storage and communication between multiple Player. This division of labor and cooperation can improve the performance and efficiency of the game.
Security and anti cheating: Placing game logic and critical data processing on the server can improve the security of the game. The client is only responsible for input and display, while the server has the final decision-making power, which can prevent the client from cheating and modifying game rules. Through server verification and control of Player 'operations, the fairness of the game can be maintained and plug-ins can be prevented.
Sync and coordination: As the main control center of the game, the server is responsible for sync and coordination of the status and behavior of multiple clients. Through the unified control of the server, it can ensure that the game experience between multiple clients is always consistent. For example, in a multiplayer game, the server is responsible for receiving and processing Player 'operations, and broadcasting the results to all clients, thus achieve sync and interaction between Player.
Network communication: the client and server communicate through the network to achieve the interaction between Player. The server acts as an intermediary, receiving and processing client requests, and transmitting corresponding information to other clients to achieve real-time communication and interaction between players. The server network architecture can ensure the smooth operation of the game between different Player, and deal with network delays and connectivity issues.
Scalability and flexibility: Separating game logic and data processing to the server can give the game better scalability and flexibility. By modifying and enhancing the server, it is easy to introduce new features and expand the scale of the game. The client can focus more on the user interface and interactive experience, while the server is responsible for handling the core logic and data management of the game.
- How do clients and servers communicate?
The editor defaults to multiplayer games. And run using a client server model. The server is the ultimate authority in maintaining the experience status, responsible for keeping all connected clients synchronized with the server.
Communication from a server to a specific client. For example, when a new Player joins the game, the server will fill the Player's bag with a group of items.
Communication from any client to the server. For example, the Player presses the P key to drink the invisibility potion and tells the server to make the Player's character invisible to all other Player.
Communication between the server and all connected clients. For example, the server will notify all players that a player has used an invisibility potion.
You don't need to consider complex communication methods such as HTTP, WebSocket, or RPC here, just build your client server code in a certain format.
The server development cost is usually an important part of the multiplayer game development cost, which may account for 30% to 50% or more of the total development cost. The specific ratio will vary depending on the characteristics of the game. We will provide you with a multiplayer game server for free.
- Which logic is written on the client side and which logic is written on the server side?
When creating a new script, it is dual ended by default, that is, if you write a piece of code in onStart(), the server will execute it and the client will execute it. At the beginning, you may not realize that you need to call if (SystemUtil. isPrincipal()) {...}
or if (SystemUtil. isServer()) {...}
. This is a means of selecting whether your code is executed on the server or client side.
The client is only responsible for rendering the image. The client receives data from the server, including various attributes and states of the player character, such as casting skills, movement, health, magic value, etc. However, the client only replays these property value changes according to the messages sent by the server.
For example, when the Player character wants to cast skills, the whole process is as follows:
First, the client sends the command "cast abilities" to the server. The server responds to the client: 'A certain skill has been released in a certain direction at a certain location.'.
Then, the client create a effect based on this information and lets the effect fly along the specified direction. The server will use the collision detect logic to determine whether the skill will collision with the enemy hero.
When a skill collides with an enemy hero, the server will inform the client, and the client will immediately delete the special effect and follow the server's instructions to reduce the health of the hit hero, while playing the hit special effect.
In short, the main task of the client is to present the results of the game based on the data transmitted by the server, and cannot make substantial changes to the core logic of the game. This design ensures consistency in the game, allowing all players to enjoy the same gaming experience in the game world.
- How to handle data in the game?
Please refer to the subdata class.
- Usage steps:
->(1) Write module C-end, module S-end, and module data
Usage example: C&S code architecture.
// Module C (Client)
export class MyModuleC extends ModuleC<MyModuleS, MyModuleData> {
}
// Module S (server)
export class MyModuleS extends ModuleS<MyModuleC, MyModuleData> {
}
// Module data
export class MyModuleData extends Subdata {
@Decorator.persistence()
myName: string;
setMyName(name: string) {
this.myName = name;
this.save(true);
}
}
// Module C (Client)
export class MyModuleC extends ModuleC<MyModuleS, MyModuleData> {
}
// Module S (server)
export class MyModuleS extends ModuleS<MyModuleC, MyModuleData> {
}
// Module data
export class MyModuleData extends Subdata {
@Decorator.persistence()
myName: string;
setMyName(name: string) {
this.myName = name;
this.save(true);
}
}
->(2) Registration module
Example usage: C&S registration module.
@Component
export default class GameStart extends Script {
protected onStart(): void {
ModuleService.registerModule(MyModuleS, MyModuleC, MyModuleData);
}
}
@Component
export default class GameStart extends Script {
protected onStart(): void {
ModuleService.registerModule(MyModuleS, MyModuleC, MyModuleData);
}
}
Usage example: C&S code example.
@Component
export default class GameStart extends Script {
protected onStart(): void {
ModuleService.registerModule(AppleModS,AppleModC,null);
}
}
class AppleModS extends ModuleS<AppleModC,null> {
public net_appleChange(player:Player) {
this.getClient(player).net_addApple();
this.getClient(this.currentPlayer).net_removeApple();
}
}
class AppleModC extends ModuleC<AppleModS,null> {
public appleNum : number;
public npc:Player;
protected onStart(): void {
this.appleNum = 10;
InputUtil.onKeyDown(Keys.P,()=>{
Player.getAllPlayers().forEach((element)=>{
this.npc = element;
});
ModuleService.getModule(AppleModC).sendApple(this.npc);
});
}
public net_removeApple() {
this.appleNum -= 1;
console.log("The current number of apples the player has is:" + this.appleNum);
}
public net_addApple() {
this.appleNum += 1;
console.log("The current number of apples the player has is:" + this.appleNum);
}
public sendApple(player:Player) {
this.server.net_appleChange(player);
}
}
@Component
export default class GameStart extends Script {
protected onStart(): void {
ModuleService.registerModule(AppleModS,AppleModC,null);
}
}
class AppleModS extends ModuleS<AppleModC,null> {
public net_appleChange(player:Player) {
this.getClient(player).net_addApple();
this.getClient(this.currentPlayer).net_removeApple();
}
}
class AppleModC extends ModuleC<AppleModS,null> {
public appleNum : number;
public npc:Player;
protected onStart(): void {
this.appleNum = 10;
InputUtil.onKeyDown(Keys.P,()=>{
Player.getAllPlayers().forEach((element)=>{
this.npc = element;
});
ModuleService.getModule(AppleModC).sendApple(this.npc);
});
}
public net_removeApple() {
this.appleNum -= 1;
console.log("The current number of apples the player has is:" + this.appleNum);
}
public net_addApple() {
this.appleNum += 1;
console.log("The current number of apples the player has is:" + this.appleNum);
}
public sendApple(player:Player) {
this.server.net_appleChange(player);
}
}
Note: This is just a preliminary discussion on the instructions of ModuleService ModuleC ModuleS. Considering that in real games, data (the number of apples) need to be stored separately, and clients are prone to cheating; Please refer to Subdata for a complete example.
When not using Module Service, the same functionality is written as follows:
Usage example: Usage example without using C&S code architecture.
@Component
export default class GameStartTwo extends Script {
public npc:Player;
public Apple:number = 10;
protected onStart(): void {
if(SystemUtil.isClient()){
Event.addServerListener("remove", () => {
this.removeApple();
});
Event.addServerListener("add",()=>{
this.addApple();
})
InputUtil.onKeyDown(Keys.P,()=>{
Player.getAllPlayers().forEach((element)=>{
this.npc = element;
});
this.sendApple(this.npc, Player.localPlayer);
})
}
if(SystemUtil.isServer()){
Event.addClientListener("send",(loca:Player, play:Player)=>{
Event.dispatchToClient(play, "add");
Event.dispatchToClient(loca, "remove");
})
}
}
public removeApple(){
this.Apple -= 1;
console.log("The current number of apples the player has is:" + this.Apple);
}
public addApple(){
this.Apple += 1;
console.log("The current number of apples the player has is:" + this.Apple);
}
public sendApple(player:Player, loca:Player){
Event.dispatchToServer("send", player, loca);
}
}
@Component
export default class GameStartTwo extends Script {
public npc:Player;
public Apple:number = 10;
protected onStart(): void {
if(SystemUtil.isClient()){
Event.addServerListener("remove", () => {
this.removeApple();
});
Event.addServerListener("add",()=>{
this.addApple();
})
InputUtil.onKeyDown(Keys.P,()=>{
Player.getAllPlayers().forEach((element)=>{
this.npc = element;
});
this.sendApple(this.npc, Player.localPlayer);
})
}
if(SystemUtil.isServer()){
Event.addClientListener("send",(loca:Player, play:Player)=>{
Event.dispatchToClient(play, "add");
Event.dispatchToClient(loca, "remove");
})
}
}
public removeApple(){
this.Apple -= 1;
console.log("The current number of apples the player has is:" + this.Apple);
}
public addApple(){
this.Apple += 1;
console.log("The current number of apples the player has is:" + this.Apple);
}
public sendApple(player:Player, loca:Player){
Event.dispatchToServer("send", player, loca);
}
}
It can be seen that when using module management, the code has been improved as follows:
Write the client and server separately to avoid the problem of difficulty in distinguishing between end and end code.
No longer need to listen and dispatch events back and forth, just add net_ before the method to complete the call of communication events.
The code is split from the original script into two modules, reducing the coupling and facilitating the development and management of multiple people.
Example usage: Create a script called Module Example, place it in the object bar, open the script, modify the original content to the following, save and run the game. The client log will first output the log of the start of the hud module, and then output the log of the start of the player module. Press the F and G keys, and you will see the information of the player module in the client log
@Component
export default class ModuleExample extends Script {
protected onStart(): void {
ModuleService.setClientFirstStartModule(HudModuleC);
ModuleService.registerModule(PlayerModuleS, PlayerModuleC, PlayerModuleData);
ModuleService.registerModule(HudModuleS, HudModuleC, HudModuleData);
}
}
class HudModuleC extends ModuleC<HudModuleS, HudModuleData>{
protected onStart(): void {
Console.log ("------------ Client ud module start ----------");
}
protected onExecute(type: number, ...params: any[]): void {
switch (type) {
case 0:
//The priority start module needs to call this function with type 0 in onExecute. The editor will wait for fun to complete before executing onStart for other modules
this.onExecuteStart(params[0]);
break;
case 1:
this.traceHudExecute(params[0], params[1], params[2]);
break;
}
}
//The priority start module needs to call this function in onExecute, and the editor will wait for fun to complete before executing onStart for other modules
protected async onExecuteStart(fun: Function): Promise<void> {
await TimeUtil.delaySecond(1);
Console.log ("---------- Client ud module ready to end ----------");
fun();
}
//Call through callExecute
private traceHudExecute(testNum: number, testPos: Vector, testString: string): void {
Console. log ("------------ client hud module call ------------");
console.log("testNum:" + testNum);
console.log("testPos:" + testPos.x, testPos.y, testPos.z);
console.log("testString:" + testString);
}
//Direct call
public traceHud(testNum: number, testPos: Vector, testString: string): void {
Console. log ("------------ client hud module call ------------");
console.log("testNum:" + testNum);
console.log("testPos:" + testPos.x, testPos.y, testPos.z);
console.log("testString:" + testString);
}
}
class HudModuleS extends ModuleS<HudModuleC, HudModuleData>{
}
class HudModuleData extends Subdata {
}
class PlayerModuleC extends ModuleC<PlayerModuleS, PlayerModuleData>{
protected onStart(): void {
Console.log ("---------- Client layer module start -----------");
InputUtil.onKeyDown(Keys.F, () => {
let playerData = this.data;
ModuleService.callExecute(HudModuleC, 1, playerData.getLevel(), playerData.getPos(), playerData.getName());
})
InputUtil.onKeyDown(Keys.G, () => {
let playerData = this.data;
let hudModuleC = ModuleService.getModule(HudModuleC);
hudModuleC.traceHud(playerData.getLevel(), playerData.getPos(), playerData.getName());
})
}
}
class PlayerModuleS extends ModuleS<PlayerModuleC, PlayerModuleData>{
}
class PlayerModuleData extends Subdata {
@Decorator.persistence()
private level: number = 1;
@Decorator.persistence()
private pos: Vector = new Vector(0, 0, 0);
@Decorator.persistence()
private name: string = "test";
public getLevel(): number {
return this.level;
}
public getPos(): Vector {
return this.pos;
}
public getName(): string {
return this.name;
}
}
@Component
export default class ModuleExample extends Script {
protected onStart(): void {
ModuleService.setClientFirstStartModule(HudModuleC);
ModuleService.registerModule(PlayerModuleS, PlayerModuleC, PlayerModuleData);
ModuleService.registerModule(HudModuleS, HudModuleC, HudModuleData);
}
}
class HudModuleC extends ModuleC<HudModuleS, HudModuleData>{
protected onStart(): void {
Console.log ("------------ Client ud module start ----------");
}
protected onExecute(type: number, ...params: any[]): void {
switch (type) {
case 0:
//The priority start module needs to call this function with type 0 in onExecute. The editor will wait for fun to complete before executing onStart for other modules
this.onExecuteStart(params[0]);
break;
case 1:
this.traceHudExecute(params[0], params[1], params[2]);
break;
}
}
//The priority start module needs to call this function in onExecute, and the editor will wait for fun to complete before executing onStart for other modules
protected async onExecuteStart(fun: Function): Promise<void> {
await TimeUtil.delaySecond(1);
Console.log ("---------- Client ud module ready to end ----------");
fun();
}
//Call through callExecute
private traceHudExecute(testNum: number, testPos: Vector, testString: string): void {
Console. log ("------------ client hud module call ------------");
console.log("testNum:" + testNum);
console.log("testPos:" + testPos.x, testPos.y, testPos.z);
console.log("testString:" + testString);
}
//Direct call
public traceHud(testNum: number, testPos: Vector, testString: string): void {
Console. log ("------------ client hud module call ------------");
console.log("testNum:" + testNum);
console.log("testPos:" + testPos.x, testPos.y, testPos.z);
console.log("testString:" + testString);
}
}
class HudModuleS extends ModuleS<HudModuleC, HudModuleData>{
}
class HudModuleData extends Subdata {
}
class PlayerModuleC extends ModuleC<PlayerModuleS, PlayerModuleData>{
protected onStart(): void {
Console.log ("---------- Client layer module start -----------");
InputUtil.onKeyDown(Keys.F, () => {
let playerData = this.data;
ModuleService.callExecute(HudModuleC, 1, playerData.getLevel(), playerData.getPos(), playerData.getName());
})
InputUtil.onKeyDown(Keys.G, () => {
let playerData = this.data;
let hudModuleC = ModuleService.getModule(HudModuleC);
hudModuleC.traceHud(playerData.getLevel(), playerData.getPos(), playerData.getName());
})
}
}
class PlayerModuleS extends ModuleS<PlayerModuleC, PlayerModuleData>{
}
class PlayerModuleData extends Subdata {
@Decorator.persistence()
private level: number = 1;
@Decorator.persistence()
private pos: Vector = new Vector(0, 0, 0);
@Decorator.persistence()
private name: string = "test";
public getLevel(): number {
return this.level;
}
public getPos(): Vector {
return this.pos;
}
public getName(): string {
return this.name;
}
}
Table of contents
Methods
callExecute<T : extends ModuleS <any , any > ModuleC <any , any >>(moduleClass : TypeName <T : extends ModuleS <any , any > ModuleC <any , any >>, type? : number , ...params : any []): any other |
---|
Call the onExecute method of a module |
getModule<T : extends ModuleS <any , any > ModuleC <any , any >>(ModuleClass : TypeName <T : extends ModuleS <any , any > ModuleC <any , any >>): T : extends ModuleS <any , any > ModuleC <any , any > other |
Retrieve a module based on its type. |
getUpdateTimeLog(): string other |
Retrieve the execution duration of the update method for each module and return it as a string, which needs to be displayed or printed by oneself |
ready(): Promise <void > other |
Is the registered module ready |
registerModule(ServerModuleType : TypeName <ModuleS <any , any >>, ClientModuleType : TypeName <ModuleC <any , any >>, ModuleDataType? : TypeName <Subdata >): ModuleService other |
Registering modules is the core function of ModuleService. |
setClientFirstStartModule(ModuleClass : TypeName <ModuleC <any , any >>): ModuleService other |
Set the first module to be launched on the client |
Methods
callExecute
• Static
callExecute<T
>(moduleClass
, type?
, ...params
): any
other
Call the onExecute method of a module
Parameters
moduleClass TypeName <T > | modular |
---|---|
type? number | The operation type passed to the onExecute method requires each module to define its own default: 0 range: type: |
...params any [] | The parameters passed to the onExecute method need to be defined by each module themselves |
Returns
any | The result of the return of the onExecute method |
---|
Type parameters
T | extends ModuleS <any , any > ModuleC <any , any > |
---|
getModule
• Static
getModule<T
>(ModuleClass
): T
other
Retrieve a module based on its type.
Parameters
ModuleClass TypeName <T > | Module type |
---|
Returns
T | Module Object |
---|
Achieve cross module call (modules call each other's methods).
Let the external code call (for example, the method in the module needs to be used in the UI script).
Type parameters
T | extends ModuleS <any , any > ModuleC <any , any > |
---|
getUpdateTimeLog
• Static
getUpdateTimeLog(): string
other
Retrieve the execution duration of the update method for each module and return it as a string, which needs to be displayed or printed by oneself
Returns
string | Log string |
---|
Precautions
This method will only take effect when debugging Module Service is enabled. This method consumes performance and is only used for performance analysis. Do not use it in the official version
ready
• Static
ready(): Promise
<void
> other
Is the registered module ready
Returns
Promise <void > | Promise for asynchronous operations |
---|
Precautions
Only modules registered during the onStart lifecycle are valid and cannot be modules registered asynchronously after waiting
registerModule
• Static
registerModule(ServerModuleType
, ClientModuleType
, ModuleDataType?
): ModuleService
other
Registering modules is the core function of ModuleService.
Parameters
ServerModuleType TypeName <ModuleS <any , any >> | Server type of module |
---|---|
ClientModuleType TypeName <ModuleC <any , any >> | Client type of module |
ModuleDataType? TypeName <Subdata > | Module data type default: null |
Returns
ModuleService | ModuleService Self, can be used as a chain call |
---|
Add all modules to Modulus Service for easy access and management.
Execute onAwake for each module in order of registration onStart、onEnterScene。
Register methods starting with 'net_' as network methods.
Associate C and S with the same data (the data is obtained from the S end and synchronized to the client every time the player goes online and the server saves the data).
setClientFirstStartModule
• Static
setClientFirstStartModule(ModuleClass
): ModuleService
other
Set the first module to be launched on the client
Parameters
ModuleClass TypeName <ModuleC <any , any >> | Module class |
---|
Returns
ModuleService | ModuleService itself can be used as a chained call |
---|