GameStateManager
This commit is contained in:
1
examples/cocos-creator-airplane/frontend/assets/env.ts
Normal file
1
examples/cocos-creator-airplane/frontend/assets/env.ts
Normal file
@@ -0,0 +1 @@
|
||||
import 'k8w-extend-native';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,187 @@
|
||||
import { WsClient } from "tsrpc-browser";
|
||||
import { GameSystem } from "../scripts/shared/game/GameSystem";
|
||||
import { GameSystemState } from "../scripts/shared/game/GameSystemState";
|
||||
import { ClientInput, MsgGameInput } from "../scripts/shared/protocols/game/client/MsgGameInput";
|
||||
import { MsgGameStart } from "../scripts/shared/protocols/game/server/MsgGameStart";
|
||||
import { MsgServerFrame } from "../scripts/shared/protocols/game/server/MsgServerFrame";
|
||||
import { serviceProto, ServiceType } from "../scripts/shared/protocols/serviceProto";
|
||||
import { CurrentUser } from "../scripts/shared/types/CurrentUser";
|
||||
import { RoomState } from "../scripts/shared/types/RoomState";
|
||||
|
||||
/**
|
||||
* 前端游戏状态管理
|
||||
* 主要用于实现前端的预测和和解
|
||||
*/
|
||||
export class GameStateManager {
|
||||
|
||||
client: WsClient<ServiceType>;
|
||||
|
||||
roomState: RoomState;
|
||||
gameSystem!: GameSystem;
|
||||
serverState!: GameSystemState;
|
||||
lastSN = 0;
|
||||
|
||||
get state() {
|
||||
return this.gameSystem.state;
|
||||
}
|
||||
|
||||
currentUser?: CurrentUser;
|
||||
constructor() {
|
||||
let client = this.client = new WsClient(serviceProto, {
|
||||
server: `ws://${location.hostname}:3000`,
|
||||
json: true,
|
||||
// logger: console
|
||||
});
|
||||
|
||||
client.listenMsg('game/server/ServerFrame', msg => { this._onServerFrame(msg) });
|
||||
client.listenMsg('game/server/GameStart', msg => {
|
||||
this._startGame(msg);
|
||||
});
|
||||
client.listenMsg('room/server/UpdateRoomState', msg => {
|
||||
this.roomState = msg.state;
|
||||
})
|
||||
|
||||
// 模拟网络延迟 可通过 URL 参数 ?lag=200 设置延迟
|
||||
let networkLag = parseInt(new URLSearchParams(location.search).get('lag') || '0') || 0;
|
||||
if (networkLag) {
|
||||
client.flows.preRecvDataFlow.push(async v => {
|
||||
await new Promise(rs => { setTimeout(rs, networkLag) })
|
||||
return v;
|
||||
});
|
||||
client.flows.preSendDataFlow.push(async v => {
|
||||
await new Promise(rs => { setTimeout(rs, networkLag) })
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).gm = this;
|
||||
}
|
||||
|
||||
private _promiseEnsureConnected?: Promise<void>;
|
||||
async ensureConnected() {
|
||||
if (!this._promiseEnsureConnected) {
|
||||
this._promiseEnsureConnected = this._doEnsureConnected().then(() => {
|
||||
this._promiseEnsureConnected = undefined;
|
||||
});
|
||||
}
|
||||
return this._promiseEnsureConnected;
|
||||
}
|
||||
private async _doEnsureConnected() {
|
||||
if (!this.client.isConnected) {
|
||||
let resConnect = await this.client.connect();
|
||||
if (!resConnect.isSucc) {
|
||||
await new Promise(rs => { setTimeout(rs, 2000) })
|
||||
return this._doEnsureConnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async login(): Promise<boolean> {
|
||||
if (this.currentUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let nickname: string;
|
||||
while (!nickname) {
|
||||
nickname = prompt('给自己取个名字:', '');
|
||||
}
|
||||
|
||||
await this.ensureConnected();
|
||||
|
||||
let ret = await this.client.callApi('Login', { nickname: nickname });
|
||||
if (!ret.isSucc) {
|
||||
alert(ret.err.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentUser = ret.res.currentUser;
|
||||
return true;
|
||||
}
|
||||
|
||||
async join(): Promise<void> {
|
||||
await this.ensureConnected();
|
||||
let ret = await this.client.callApi('room/JoinRoom', { roomId: 123 });
|
||||
|
||||
if (!ret.isSucc) {
|
||||
if (confirm(`加入房间失败\n${ret.err.message}\n是否重试 ?`)) {
|
||||
return this.join();
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.roomState = ret.res.roomState;
|
||||
}
|
||||
|
||||
private _startGame(msg: MsgGameStart) {
|
||||
this.gameSystem = new GameSystem(msg.gameState);
|
||||
this.serverState = Object.merge({}, msg.gameState);
|
||||
this._lastLocalTime = Date.now();
|
||||
}
|
||||
|
||||
private _onServerFrame(frame: MsgServerFrame) {
|
||||
if (!this.gameSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 回滚至上一次的权威状态
|
||||
this.gameSystem.state = Object.merge({}, this.serverState);
|
||||
// 计算最新的权威状态
|
||||
for (let input of frame.inputs) {
|
||||
this.gameSystem.applyInput(input);
|
||||
}
|
||||
this.serverState = Object.merge({}, this.gameSystem.state);
|
||||
this._lastLocalTime = Date.now();
|
||||
|
||||
// 和解 = 权威状态 + 本地输入 (最新的本地预测状态)
|
||||
let lastSn = frame.lastSn ?? -1;
|
||||
this.pendingInputMsgs.remove(v => v.sn <= lastSn);
|
||||
this.pendingInputMsgs.forEach(m => {
|
||||
m.inputs.forEach(v => {
|
||||
this.gameSystem.applyInput({
|
||||
...v,
|
||||
playerId: this.currentUser.id
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pendingInputMsgs: MsgGameInput[] = [];
|
||||
sendClientInput(input: ClientInput) {
|
||||
// 已掉线或暂未加入,忽略本地输入
|
||||
if (!this.currentUser || !this.client.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造消息
|
||||
let msg: MsgGameInput = {
|
||||
sn: ++this.lastSN,
|
||||
inputs: [input]
|
||||
}
|
||||
|
||||
// 向服务端发送输入
|
||||
this.pendingInputMsgs.push(msg);
|
||||
this.client.sendMsg('game/client/GameInput', msg);
|
||||
|
||||
// 预测状态:本地立即应用输入
|
||||
this.predictTimePast();
|
||||
this.gameSystem.applyInput({
|
||||
...input,
|
||||
playerId: this.currentUser.id
|
||||
});
|
||||
}
|
||||
|
||||
// 本地预测时间流逝(但不提交,会以下一次服务器同步为准)
|
||||
private _lastLocalTime!: number;
|
||||
predictTimePast() {
|
||||
const now = Date.now();
|
||||
let dt = now - this._lastLocalTime;
|
||||
this.gameSystem.applyInput({
|
||||
type: 'TimePast',
|
||||
dt: dt
|
||||
});
|
||||
this._lastLocalTime = now;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { WsClient } from "tsrpc-browser";
|
||||
import { GameSystem, GameSystemState } from "../shared/game/GameSystem";
|
||||
import { ClientInput, MsgClientInput } from "../shared/protocols/client/MsgClientInput";
|
||||
import { MsgFrame } from "../shared/protocols/server/MsgFrame";
|
||||
import { serviceProto, ServiceType } from "../shared/protocols/serviceProto";
|
||||
|
||||
/**
|
||||
* 前端游戏状态管理
|
||||
* 主要用于实现前端的预测和和解
|
||||
*/
|
||||
export class StateManager {
|
||||
|
||||
client: WsClient<ServiceType>;
|
||||
|
||||
gameSystem = new GameSystem();
|
||||
|
||||
lastServerState: GameSystemState = this.gameSystem.state;
|
||||
lastRecvSetverStateTime = 0;
|
||||
selfPlayerId: number = -1;
|
||||
lastSN = 0;
|
||||
|
||||
get state() {
|
||||
return this.gameSystem.state;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
let client = this.client = new WsClient(serviceProto, {
|
||||
server: `ws://${location.hostname}:3000`,
|
||||
json: true,
|
||||
// logger: console
|
||||
});;
|
||||
client.listenMsg('server/Frame', msg => { this._onServerSync(msg) });
|
||||
|
||||
// 模拟网络延迟 可通过 URL 参数 ?lag=200 设置延迟
|
||||
let networkLag = parseInt(new URLSearchParams(location.search).get('lag') || '0') || 0;
|
||||
if (networkLag) {
|
||||
client.flows.preRecvDataFlow.push(async v => {
|
||||
await new Promise(rs => { setTimeout(rs, networkLag) })
|
||||
return v;
|
||||
});
|
||||
client.flows.preSendDataFlow.push(async v => {
|
||||
await new Promise(rs => { setTimeout(rs, networkLag) })
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).gm = this;
|
||||
}
|
||||
|
||||
async join(): Promise<void> {
|
||||
if (!this.client.isConnected) {
|
||||
let resConnect = await this.client.connect();
|
||||
if (!resConnect.isSucc) {
|
||||
await new Promise(rs => { setTimeout(rs, 2000) })
|
||||
return this.join();
|
||||
}
|
||||
}
|
||||
|
||||
let ret = await this.client.callApi('Join', {});
|
||||
|
||||
if (!ret.isSucc) {
|
||||
if (confirm(`加入房间失败\n${ret.err.message}\n是否重试 ?`)) {
|
||||
return this.join();
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.gameSystem.reset(ret.res.gameState);
|
||||
this.lastServerState = Object.merge(ret.res.gameState);
|
||||
this.lastRecvSetverStateTime = Date.now();
|
||||
this.selfPlayerId = ret.res.playerId;
|
||||
}
|
||||
|
||||
private _onServerSync(frame: MsgFrame) {
|
||||
// 回滚至上一次的权威状态
|
||||
this.gameSystem.reset(this.lastServerState);
|
||||
|
||||
// 计算最新的权威状态
|
||||
for (let input of frame.inputs) {
|
||||
this.gameSystem.applyInput(input);
|
||||
}
|
||||
this.lastServerState = Object.merge({}, this.gameSystem.state);
|
||||
this.lastRecvSetverStateTime = Date.now();
|
||||
|
||||
// 和解 = 权威状态 + 本地输入 (最新的本地预测状态)
|
||||
let lastSn = frame.lastSn ?? -1;
|
||||
this.pendingInputMsgs.remove(v => v.sn <= lastSn);
|
||||
this.pendingInputMsgs.forEach(m => {
|
||||
m.inputs.forEach(v => {
|
||||
this.gameSystem.applyInput({
|
||||
...v,
|
||||
playerId: this.selfPlayerId
|
||||
});
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pendingInputMsgs: MsgClientInput[] = [];
|
||||
sendClientInput(input: ClientInput) {
|
||||
// 已掉线或暂未加入,忽略本地输入
|
||||
if (!this.selfPlayerId || !this.client.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造消息
|
||||
let msg: MsgClientInput = {
|
||||
sn: ++this.lastSN,
|
||||
inputs: [input]
|
||||
}
|
||||
|
||||
// 向服务端发送输入
|
||||
this.pendingInputMsgs.push(msg);
|
||||
this.client.sendMsg('client/ClientInput', msg);
|
||||
|
||||
// 预测状态:本地立即应用输入
|
||||
this.gameSystem.applyInput({
|
||||
...input,
|
||||
playerId: this.selfPlayerId
|
||||
});
|
||||
}
|
||||
|
||||
// 本地时间流逝(会被下一次服务器状态覆盖)
|
||||
localTimePast() {
|
||||
this.gameSystem.applyInput({
|
||||
type: 'TimePast',
|
||||
dt: Date.now() - this.lastRecvSetverStateTime
|
||||
});
|
||||
this.lastRecvSetverStateTime = Date.now();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { _decorator, Component, Node, Collider, find, ITriggerEvent, Script } from 'cc';
|
||||
import { Constant } from '../framework/constant';
|
||||
import { GameManager } from '../gameManager';
|
||||
import { GameManager } from '../GameController';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { _decorator, Component, Node, Collider, ITriggerEvent } from 'cc';
|
||||
import { Constant } from '../framework/constant';
|
||||
import { GameManager } from '../gameManager';
|
||||
import { GameManager } from '../GameController';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { _decorator, Component, Node, Collider, ITriggerEvent, physics, PhysicsSystem, find, Game, Prefab, NodePool, instantiate, Vec2, Vec3, AudioSource } from 'cc';
|
||||
import { bulletManager } from '../bullet/bulletManager';
|
||||
import { Constant } from '../framework/constant';
|
||||
import { GameManager } from '../gameManager';
|
||||
import { GameManager } from '../GameController';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import { _decorator, Component, Node, Collider, ITriggerEvent } from 'cc';
|
||||
import { Constant } from '../framework/constant';
|
||||
import { GameManager } from '../gameManager';
|
||||
import { GameManager } from '../GameController';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { _decorator, Component, Node, UITransform, Vec2, Vec3, find, Script, game, Label, CameraComponent, Camera, EventTouch, v3 } from 'cc';
|
||||
import { GameManager } from '../gameManager';
|
||||
import { GameManager } from '../GameController';
|
||||
import { MovingSceneBg } from './common/movingSceneBg';
|
||||
import { Tips } from './common/tips';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user