GameStateManager

This commit is contained in:
k8w 2021-12-28 22:45:50 +08:00
parent b9b6be0c7f
commit 5a6ae0b679
22 changed files with 887 additions and 815 deletions

View File

@ -1,17 +1,17 @@
import { ApiCall, ApiCallWs } from "tsrpc"; import { ApiCallWs } from "tsrpc";
import { ReqLogin, ResLogin } from "../shared/protocols/PtlLogin"; import { ReqLogin, ResLogin } from "../shared/protocols/PtlLogin";
let nextPlayerId = 1; let nextPlayerId = 1;
export async function ApiLogin(call: ApiCallWs<ReqLogin, ResLogin>) { export async function ApiLogin(call: ApiCallWs<ReqLogin, ResLogin>) {
let playerId = nextPlayerId++; let playerId = nextPlayerId++;
call.conn.currentUser = { call.conn.currentUser = {
id: playerId, id: playerId,
nickname: call.req.nickname nickname: call.req.nickname
} }
call.succ({ call.succ({
playerId: playerId currentUser: call.conn.currentUser
}) })
} }

View File

@ -11,5 +11,7 @@ export async function ApiJoinRoom(call: ApiCallWs<ReqJoinRoom, ResJoinRoom>) {
return call.error(op.errMsg); return call.error(op.errMsg);
} }
call.succ({}); call.succ({
roomState: room.state
});
} }

View File

@ -7,15 +7,10 @@ import { GameSystemState } from "../shared/game/GameSystemState";
import { MsgGameInput } from "../shared/protocols/game/client/MsgGameInput"; import { MsgGameInput } from "../shared/protocols/game/client/MsgGameInput";
import { ServiceType } from "../shared/protocols/serviceProto"; import { ServiceType } from "../shared/protocols/serviceProto";
import { CurrentUser } from "../shared/types/CurrentUser"; import { CurrentUser } from "../shared/types/CurrentUser";
import { RoomState } from "../shared/types/RoomState";
const MAX_ROOM_USER = 2; const MAX_ROOM_USER = 2;
export interface RoomState {
id: uint;
players: { id: uint, nickname: string, isReady: boolean }[];
status: 'wait' | 'ready' | 'start';
}
/** /**
* - - * - -
*/ */

View File

@ -78,11 +78,11 @@ export class GameSystem {
} }
// 子弹碰撞,抵消 // 子弹碰撞,抵消
case 'BulletHit': { case 'BulletHit': {
let player = this.state.players.find(v => v.id === input.player.id); let player = this.state.players.find(v => v.id === input.playerId);
let enemy = this.state.enemies.find(v => v.id === input.enemy.id); let enemy = this.state.enemies.find(v => v.id === input.enemyId);
if (player && enemy) { if (player && enemy) {
player.bullets.removeOne(v => v.id === input.player.bulletId); player.bullets.removeOne(v => v.id === input.playerBulletId);
enemy.bullets.removeOne(v => v.id === input.enemy.bulletId); enemy.bullets.removeOne(v => v.id === input.enemyBulletId);
} }
break; break;
} }

View File

@ -1,4 +1,4 @@
import { uint } from "tsrpc"; import { uint } from "tsrpc-proto";
import { PlayerState } from "./GameSystemState"; import { PlayerState } from "./GameSystemState";
// 移动并攻击 // 移动并攻击
@ -34,14 +34,10 @@ export interface PlayerHurt {
// 子弹互相碰撞,双双消失 // 子弹互相碰撞,双双消失
export interface BulletHit { export interface BulletHit {
type: 'BulletHit', type: 'BulletHit',
player: { playerId: uint,
id: uint, playerBulletId: uint,
bulletId: uint enemyId: uint,
}, enemyBulletId: uint
enemy: {
id: uint,
bulletId: uint
},
} }
// 时间流逝 // 时间流逝

View File

@ -1,4 +1,4 @@
import { uint } from "tsrpc"; import { CurrentUser } from "../types/CurrentUser";
import { BaseConf, BaseRequest, BaseResponse } from "./base"; import { BaseConf, BaseRequest, BaseResponse } from "./base";
export interface ReqLogin extends BaseRequest { export interface ReqLogin extends BaseRequest {
@ -6,7 +6,7 @@ export interface ReqLogin extends BaseRequest {
} }
export interface ResLogin extends BaseResponse { export interface ResLogin extends BaseResponse {
playerId: uint currentUser: CurrentUser
} }
export const conf: BaseConf = { export const conf: BaseConf = {

View File

@ -1,4 +1,5 @@
import { uint } from "tsrpc-proto"; import { uint } from "tsrpc-proto";
import { RoomState } from "../../types/RoomState";
import { BaseConf, BaseRequest, BaseResponse } from "../base"; import { BaseConf, BaseRequest, BaseResponse } from "../base";
export interface ReqJoinRoom extends BaseRequest { export interface ReqJoinRoom extends BaseRequest {
@ -6,7 +7,7 @@ export interface ReqJoinRoom extends BaseRequest {
} }
export interface ResJoinRoom extends BaseResponse { export interface ResJoinRoom extends BaseResponse {
roomState: RoomState;
} }
export const conf: BaseConf = { export const conf: BaseConf = {

View File

@ -1,4 +1,4 @@
import { RoomState } from "../../../../models/Room"; import { RoomState } from "../../../types/RoomState";
export interface MsgUpdateRoomState { export interface MsgUpdateRoomState {
state: RoomState state: RoomState

View File

@ -570,53 +570,35 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
}, },
{ {
"id": 1, "id": 3,
"name": "player", "name": "playerId",
"type": { "type": {
"type": "Interface", "type": "Number",
"properties": [ "scalarType": "uint"
{
"id": 0,
"name": "id",
"type": {
"type": "Number",
"scalarType": "uint"
}
},
{
"id": 1,
"name": "bulletId",
"type": {
"type": "Number",
"scalarType": "uint"
}
}
]
} }
}, },
{ {
"id": 2, "id": 4,
"name": "enemy", "name": "playerBulletId",
"type": { "type": {
"type": "Interface", "type": "Number",
"properties": [ "scalarType": "uint"
{ }
"id": 0, },
"name": "id", {
"type": { "id": 5,
"type": "Number", "name": "enemyId",
"scalarType": "uint" "type": {
} "type": "Number",
}, "scalarType": "uint"
{ }
"id": 1, },
"name": "bulletId", {
"type": { "id": 6,
"type": "Number", "name": "enemyBulletId",
"scalarType": "uint" "type": {
} "type": "Number",
} "scalarType": "uint"
]
} }
} }
] ]
@ -1143,11 +1125,11 @@ export const serviceProto: ServiceProto<ServiceType> = {
], ],
"properties": [ "properties": [
{ {
"id": 0, "id": 1,
"name": "playerId", "name": "currentUser",
"type": { "type": {
"type": "Number", "type": "Reference",
"scalarType": "uint" "target": "../types/CurrentUser/CurrentUser"
} }
} }
] ]
@ -1155,6 +1137,26 @@ export const serviceProto: ServiceProto<ServiceType> = {
"base/BaseResponse": { "base/BaseResponse": {
"type": "Interface" "type": "Interface"
}, },
"../types/CurrentUser/CurrentUser": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id",
"type": {
"type": "Number",
"scalarType": "uint"
}
},
{
"id": 1,
"name": "nickname",
"type": {
"type": "String"
}
}
]
},
"room/PtlCreateRoom/ReqCreateRoom": { "room/PtlCreateRoom/ReqCreateRoom": {
"type": "Interface", "type": "Interface",
"extends": [ "extends": [
@ -1245,55 +1247,19 @@ export const serviceProto: ServiceProto<ServiceType> = {
"target": "base/BaseResponse" "target": "base/BaseResponse"
} }
} }
]
},
"room/PtlSetReady/ReqSetReady": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "base/BaseRequest"
}
}
], ],
"properties": [ "properties": [
{ {
"id": 0, "id": 0,
"name": "isReady", "name": "roomState",
"type": {
"type": "Boolean"
}
}
]
},
"room/PtlSetReady/ResSetReady": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": { "type": {
"type": "Reference", "type": "Reference",
"target": "base/BaseResponse" "target": "../types/RoomState/RoomState"
} }
} }
] ]
}, },
"room/server/MsgUpdateRoomState/MsgUpdateRoomState": { "../types/RoomState/RoomState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "state",
"type": {
"type": "Reference",
"target": "../../models/Room/RoomState"
}
}
]
},
"../../models/Room/RoomState": {
"type": "Interface", "type": "Interface",
"properties": [ "properties": [
{ {
@ -1305,7 +1271,7 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
}, },
{ {
"id": 3, "id": 1,
"name": "players", "name": "players",
"type": { "type": {
"type": "Array", "type": "Array",
@ -1369,6 +1335,52 @@ export const serviceProto: ServiceProto<ServiceType> = {
} }
} }
] ]
},
"room/PtlSetReady/ReqSetReady": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "base/BaseRequest"
}
}
],
"properties": [
{
"id": 0,
"name": "isReady",
"type": {
"type": "Boolean"
}
}
]
},
"room/PtlSetReady/ResSetReady": {
"type": "Interface",
"extends": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "base/BaseResponse"
}
}
]
},
"room/server/MsgUpdateRoomState/MsgUpdateRoomState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "state",
"type": {
"type": "Reference",
"target": "../types/RoomState/RoomState"
}
}
]
} }
} }
}; };

View File

@ -0,0 +1,7 @@
import { uint } from "tsrpc-proto";
export interface RoomState {
id: uint;
players: { id: uint, nickname: string, isReady: boolean }[];
status: 'wait' | 'ready' | 'start';
}

View File

@ -0,0 +1 @@
import 'k8w-extend-native';

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -1,7 +1,7 @@
import { _decorator, Component, Node, Collider, find, ITriggerEvent, Script } from 'cc'; import { _decorator, Component, Node, Collider, find, ITriggerEvent, Script } from 'cc';
import { Constant } from '../framework/constant'; import { Constant } from '../framework/constant';
import { GameManager } from '../gameManager'; import { GameManager } from '../GameController';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;

View File

@ -1,7 +1,7 @@
import { _decorator, Component, Node, Collider, ITriggerEvent } from 'cc'; import { _decorator, Component, Node, Collider, ITriggerEvent } from 'cc';
import { Constant } from '../framework/constant'; import { Constant } from '../framework/constant';
import { GameManager } from '../gameManager'; import { GameManager } from '../GameController';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;

View File

@ -2,7 +2,7 @@
import { _decorator, Component, Node, Collider, ITriggerEvent, physics, PhysicsSystem, find, Game, Prefab, NodePool, instantiate, Vec2, Vec3, AudioSource } from 'cc'; import { _decorator, Component, Node, Collider, ITriggerEvent, physics, PhysicsSystem, find, Game, Prefab, NodePool, instantiate, Vec2, Vec3, AudioSource } from 'cc';
import { bulletManager } from '../bullet/bulletManager'; import { bulletManager } from '../bullet/bulletManager';
import { Constant } from '../framework/constant'; import { Constant } from '../framework/constant';
import { GameManager } from '../gameManager'; import { GameManager } from '../GameController';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;

View File

@ -1,7 +1,7 @@
import { _decorator, Component, Node, Collider, ITriggerEvent } from 'cc'; import { _decorator, Component, Node, Collider, ITriggerEvent } from 'cc';
import { Constant } from '../framework/constant'; import { Constant } from '../framework/constant';
import { GameManager } from '../gameManager'; import { GameManager } from '../GameController';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;

View File

@ -1,6 +1,6 @@
import { _decorator, Component, Node, UITransform, Vec2, Vec3, find, Script, game, Label, CameraComponent, Camera, EventTouch, v3 } from 'cc'; 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 { MovingSceneBg } from './common/movingSceneBg';
import { Tips } from './common/tips'; import { Tips } from './common/tips';

View File

@ -7,6 +7,10 @@
"uuid": "c794458c-05f6-4c9f-909b-20d54897d219", "uuid": "c794458c-05f6-4c9f-909b-20d54897d219",
"version": "3.4.0", "version": "3.4.0",
"dependencies": { "dependencies": {
"seedrandom": "^3.0.5",
"tsrpc-browser": "^3.1.4" "tsrpc-browser": "^3.1.4"
},
"devDependencies": {
"@types/seedrandom": "^3.0.1"
} }
} }

View File

@ -1,9 +1,9 @@
{ {
/* Base configuration. Do not edit this field. */ /* Base configuration. Do not edit this field. */
"extends": "./temp/tsconfig.cocos.json", "extends": "./temp/tsconfig.cocos.json",
/* Add your custom configuration here. */ /* Add your custom configuration here. */
"compilerOptions": { "compilerOptions": {
"strict": false "strict": false,
"allowSyntheticDefaultImports": true
} }
} }