GameStateManager
This commit is contained in:
parent
b9b6be0c7f
commit
5a6ae0b679
@ -1,4 +1,4 @@
|
|||||||
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;
|
||||||
@ -12,6 +12,6 @@ export async function ApiLogin(call: ApiCallWs<ReqLogin, ResLogin>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
call.succ({
|
call.succ({
|
||||||
playerId: playerId
|
currentUser: call.conn.currentUser
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -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
|
||||||
|
});
|
||||||
}
|
}
|
@ -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';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 服务端 - 房间 - 逻辑系统
|
* 服务端 - 房间 - 逻辑系统
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间流逝
|
// 时间流逝
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 = {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { RoomState } from "../../../../models/Room";
|
import { RoomState } from "../../../types/RoomState";
|
||||||
|
|
||||||
export interface MsgUpdateRoomState {
|
export interface MsgUpdateRoomState {
|
||||||
state: RoomState
|
state: RoomState
|
||||||
|
@ -570,53 +570,35 @@ export const serviceProto: ServiceProto<ServiceType> = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 3,
|
||||||
"name": "player",
|
"name": "playerId",
|
||||||
"type": {
|
|
||||||
"type": "Interface",
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"id": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type": {
|
"type": {
|
||||||
"type": "Number",
|
"type": "Number",
|
||||||
"scalarType": "uint"
|
"scalarType": "uint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 4,
|
||||||
"name": "bulletId",
|
"name": "playerBulletId",
|
||||||
"type": {
|
|
||||||
"type": "Number",
|
|
||||||
"scalarType": "uint"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "enemy",
|
|
||||||
"type": {
|
|
||||||
"type": "Interface",
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"id": 0,
|
|
||||||
"name": "id",
|
|
||||||
"type": {
|
"type": {
|
||||||
"type": "Number",
|
"type": "Number",
|
||||||
"scalarType": "uint"
|
"scalarType": "uint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 5,
|
||||||
"name": "bulletId",
|
"name": "enemyId",
|
||||||
"type": {
|
"type": {
|
||||||
"type": "Number",
|
"type": "Number",
|
||||||
"scalarType": "uint"
|
"scalarType": "uint"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "enemyBulletId",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -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';
|
||||||
|
}
|
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';
|
@ -16,8 +16,8 @@ let _temp_quat = new Quat;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ccclass('GameManager')
|
@ccclass('GameController')
|
||||||
export class GameManager extends Component {
|
export class GameController extends Component {
|
||||||
|
|
||||||
@property(Node)
|
@property(Node)
|
||||||
public playerPlane: Node = null; //玩家飞机节点
|
public playerPlane: Node = null; //玩家飞机节点
|
@ -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 { _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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user