airplane basic server
This commit is contained in:
parent
52618125e1
commit
b9b6be0c7f
17
examples/cocos-creator-airplane/backend/src/api/ApiLogin.ts
Normal file
17
examples/cocos-creator-airplane/backend/src/api/ApiLogin.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { ApiCall, ApiCallWs } from "tsrpc";
|
||||||
|
import { ReqLogin, ResLogin } from "../shared/protocols/PtlLogin";
|
||||||
|
|
||||||
|
let nextPlayerId = 1;
|
||||||
|
|
||||||
|
export async function ApiLogin(call: ApiCallWs<ReqLogin, ResLogin>) {
|
||||||
|
let playerId = nextPlayerId++;
|
||||||
|
|
||||||
|
call.conn.currentUser = {
|
||||||
|
id: playerId,
|
||||||
|
nickname: call.req.nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
call.succ({
|
||||||
|
playerId: playerId
|
||||||
|
})
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
import { ApiCall } from "tsrpc";
|
|
||||||
import { server } from "..";
|
|
||||||
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
|
|
||||||
|
|
||||||
// This is a demo code file
|
|
||||||
// Feel free to delete it
|
|
||||||
|
|
||||||
export async function ApiSend(call: ApiCall<ReqSend, ResSend>) {
|
|
||||||
// Error
|
|
||||||
if (call.req.content.length === 0) {
|
|
||||||
call.error('Content is empty')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success
|
|
||||||
let time = new Date();
|
|
||||||
call.succ({
|
|
||||||
time: time
|
|
||||||
});
|
|
||||||
|
|
||||||
// Broadcast
|
|
||||||
server.broadcastMsg('Chat', {
|
|
||||||
content: call.req.content,
|
|
||||||
time: time
|
|
||||||
})
|
|
||||||
}
|
|
@ -0,0 +1,7 @@
|
|||||||
|
import { ApiCall } from "tsrpc";
|
||||||
|
import { ReqCreateRoom, ResCreateRoom } from "../../shared/protocols/room/PtlCreateRoom";
|
||||||
|
|
||||||
|
export async function ApiCreateRoom(call: ApiCall<ReqCreateRoom, ResCreateRoom>) {
|
||||||
|
// TODO
|
||||||
|
call.error('API Not Implemented');
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { ApiCall } from "tsrpc";
|
||||||
|
import { ReqGetRoomList, ResGetRoomList } from "../../shared/protocols/room/PtlGetRoomList";
|
||||||
|
|
||||||
|
export async function ApiGetRoomList(call: ApiCall<ReqGetRoomList, ResGetRoomList>) {
|
||||||
|
// TODO
|
||||||
|
call.error('API Not Implemented');
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import { ApiCallWs } from "tsrpc";
|
||||||
|
import { Room } from "../../models/Room";
|
||||||
|
import { ReqJoinRoom, ResJoinRoom } from "../../shared/protocols/room/PtlJoinRoom";
|
||||||
|
|
||||||
|
// TEST
|
||||||
|
let room = new Room(123);
|
||||||
|
|
||||||
|
export async function ApiJoinRoom(call: ApiCallWs<ReqJoinRoom, ResJoinRoom>) {
|
||||||
|
let op = room.join(call.conn);
|
||||||
|
if (!op.isSucc) {
|
||||||
|
return call.error(op.errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
call.succ({});
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { ApiCall } from "tsrpc";
|
||||||
|
import { ReqSetReady, ResSetReady } from "../../shared/protocols/room/PtlSetReady";
|
||||||
|
|
||||||
|
export async function ApiSetReady(call: ApiCall<ReqSetReady, ResSetReady>) {
|
||||||
|
if (!call.conn.room) {
|
||||||
|
return call.error('您还未加入房间')
|
||||||
|
}
|
||||||
|
|
||||||
|
call.conn.room.setReady(call.conn.currentUser.id, call.req.isReady);
|
||||||
|
call.succ({})
|
||||||
|
}
|
210
examples/cocos-creator-airplane/backend/src/models/Room.ts
Normal file
210
examples/cocos-creator-airplane/backend/src/models/Room.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import seedrandom from "seedrandom";
|
||||||
|
import { MsgCallWs, uint, WsConnection } from "tsrpc";
|
||||||
|
import { server } from "..";
|
||||||
|
import { gameConfig } from "../shared/game/gameConfig";
|
||||||
|
import { GameSystemInput } from "../shared/game/GameSystemInput";
|
||||||
|
import { GameSystemState } from "../shared/game/GameSystemState";
|
||||||
|
import { MsgGameInput } from "../shared/protocols/game/client/MsgGameInput";
|
||||||
|
import { ServiceType } from "../shared/protocols/serviceProto";
|
||||||
|
import { CurrentUser } from "../shared/types/CurrentUser";
|
||||||
|
|
||||||
|
const MAX_ROOM_USER = 2;
|
||||||
|
|
||||||
|
export interface RoomState {
|
||||||
|
id: uint;
|
||||||
|
players: { id: uint, nickname: string, isReady: boolean }[];
|
||||||
|
status: 'wait' | 'ready' | 'start';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务端 - 房间 - 逻辑系统
|
||||||
|
*/
|
||||||
|
export class Room {
|
||||||
|
|
||||||
|
// 房间信息
|
||||||
|
state: RoomState;
|
||||||
|
conns: WsConnection<ServiceType>[] = [];
|
||||||
|
|
||||||
|
constructor(roomId: uint) {
|
||||||
|
this.state = {
|
||||||
|
id: roomId,
|
||||||
|
players: [],
|
||||||
|
status: 'wait'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #region Room Control
|
||||||
|
// 加入房间
|
||||||
|
join(conn: WsConnection<ServiceType>): { isSucc: true } | { isSucc: false, errMsg: string } {
|
||||||
|
if (this.state.status !== 'wait') {
|
||||||
|
return { isSucc: false, errMsg: '该房间游戏已开始' }
|
||||||
|
}
|
||||||
|
if (this.conns.length >= MAX_ROOM_USER) {
|
||||||
|
return { isSucc: false, errMsg: '房间已满员' }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.conns.push(conn);
|
||||||
|
this.state.players.push({ ...conn.currentUser, isReady: false });
|
||||||
|
conn.room = this;
|
||||||
|
this._syncRoomState();
|
||||||
|
|
||||||
|
return { isSucc: true };
|
||||||
|
}
|
||||||
|
private _timerStartGame?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
setReady(playerId: number, isReady: boolean) {
|
||||||
|
if (this.state.status !== 'wait') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let player = this.state.players.find(v => v.id === playerId);
|
||||||
|
if (player) {
|
||||||
|
player.isReady = isReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果人满了且所有人都准备了,5 秒后开始游戏
|
||||||
|
if (this.state.players.length >= MAX_ROOM_USER && this.state.players.every(v => v.isReady)) {
|
||||||
|
this.state.status = 'ready';
|
||||||
|
this._syncRoomState();
|
||||||
|
|
||||||
|
this._timerStartGame = setTimeout(() => {
|
||||||
|
this._timerStartGame = undefined;
|
||||||
|
this.startGame();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 离开房间
|
||||||
|
leave(conn: WsConnection<ServiceType>) {
|
||||||
|
conn.room = undefined;
|
||||||
|
this.conns.removeOne(v => v === conn);
|
||||||
|
this.state.players.removeOne(v => v.id === conn.currentUser.id);
|
||||||
|
|
||||||
|
// 由于有人秒退,游戏无法正常开始
|
||||||
|
if (this._timerStartGame && this.conns.length < MAX_ROOM_USER) {
|
||||||
|
this.state.status = 'wait';
|
||||||
|
clearTimeout(this._timerStartGame);
|
||||||
|
this._timerStartGame = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._syncRoomState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _syncRoomState() {
|
||||||
|
server.broadcastMsg('room/server/UpdateRoomState', {
|
||||||
|
state: this.state
|
||||||
|
}, this.conns)
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Game
|
||||||
|
private _lastSn: { [playerId: number]: number | undefined } = {};
|
||||||
|
private _lastFrameIndex: number = 0;
|
||||||
|
private _lastSyncTime!: number;
|
||||||
|
private _gameOveredUids: number[] = [];
|
||||||
|
|
||||||
|
async startGame() {
|
||||||
|
this.conns.forEach(v => {
|
||||||
|
v.listenMsg('game/client/GameInput', (call: MsgCallWs<MsgGameInput, ServiceType>) => {
|
||||||
|
this._lastSn[call.conn.currentUser.id] = call.msg.sn;
|
||||||
|
this._syncInputs(call.msg.inputs.map(v => ({ ...v, playerId: call.conn.currentUser.id })))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新房间状态
|
||||||
|
this.state.status = 'start';
|
||||||
|
this._syncRoomState();
|
||||||
|
|
||||||
|
// 生成游戏初始状态
|
||||||
|
let seed = '' + Math.random();
|
||||||
|
let prng = seedrandom(seed, { state: true });
|
||||||
|
let initGameState: GameSystemState = {
|
||||||
|
now: 0,
|
||||||
|
players: this.conns.map(v => ({
|
||||||
|
id: v.currentUser.id,
|
||||||
|
nickname: v.currentUser.nickname,
|
||||||
|
score: 0,
|
||||||
|
life: gameConfig.player.totalLife,
|
||||||
|
currentBulletType: 'M',
|
||||||
|
pos: { x: 0, y: 100 },
|
||||||
|
bullets: [],
|
||||||
|
nextId: {
|
||||||
|
bullet: 0
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
enemies: [],
|
||||||
|
fightIcons: [],
|
||||||
|
|
||||||
|
// ID 生成器
|
||||||
|
nextId: {
|
||||||
|
enemy: 0,
|
||||||
|
fightIcon: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// 上次创建敌机的时间
|
||||||
|
lastCreateEnemyTime: 3000,
|
||||||
|
|
||||||
|
// 伪随机数发生器状态
|
||||||
|
random: {
|
||||||
|
seed: '' + Math.random(),
|
||||||
|
state: prng.state()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._lastSn = {};
|
||||||
|
this._lastSyncTime = Date.now();
|
||||||
|
this._lastFrameIndex = 0;
|
||||||
|
|
||||||
|
// 广播游戏初始状态
|
||||||
|
server.broadcastMsg('game/server/GameStart', {
|
||||||
|
frameIndex: this._lastFrameIndex,
|
||||||
|
gameState: initGameState
|
||||||
|
}, this.conns);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收到输入,立即同步给所有人
|
||||||
|
private _syncInputs(inputs: GameSystemInput[]) {
|
||||||
|
// 增加时间流逝
|
||||||
|
let now = process.uptime() * 1000;
|
||||||
|
inputs.unshift({
|
||||||
|
type: 'TimePast',
|
||||||
|
dt: now - this._lastSyncTime
|
||||||
|
});
|
||||||
|
this._lastSyncTime = now;
|
||||||
|
|
||||||
|
// 把输入广播给所有用户
|
||||||
|
this.conns.forEach(v => {
|
||||||
|
v.sendMsg('game/server/ServerFrame', {
|
||||||
|
frameIndex: this._lastFrameIndex,
|
||||||
|
inputs: inputs,
|
||||||
|
lastSn: this._lastSn[v.currentUser.id!]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async gameOver(uid: number) {
|
||||||
|
if (this.state.status !== 'start' || this._gameOveredUids.includes(uid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._gameOveredUids.push(uid);
|
||||||
|
|
||||||
|
// 所有人都 GameOver,Game 真的 Over
|
||||||
|
if (this.conns.every(v => this._gameOveredUids.includes(v.currentUser.id))) {
|
||||||
|
this.conns.forEach(v => {
|
||||||
|
v.unlistenMsgAll('game/client/GameInput');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state.status = 'wait';
|
||||||
|
this._syncRoomState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'tsrpc' {
|
||||||
|
export interface BaseConnection {
|
||||||
|
currentUser: CurrentUser,
|
||||||
|
room?: Room,
|
||||||
|
}
|
||||||
|
}
|
@ -38,6 +38,7 @@ export interface GameSystemState {
|
|||||||
// 玩家
|
// 玩家
|
||||||
export interface PlayerState {
|
export interface PlayerState {
|
||||||
id: uint,
|
id: uint,
|
||||||
|
nickname: string,
|
||||||
// 得分
|
// 得分
|
||||||
score: number,
|
score: number,
|
||||||
// 生命值
|
// 生命值
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { EnemyType } from "./GameSystemState";
|
import { EnemyType } from "./GameSystemState";
|
||||||
|
|
||||||
export const gameConfig = {
|
export const gameConfig = {
|
||||||
syncRate: 10,
|
|
||||||
|
|
||||||
enemy: {
|
enemy: {
|
||||||
bulletSpeed: 20,
|
bulletSpeed: 20,
|
||||||
// 第一次发射子弹的延迟时间
|
// 第一次发射子弹的延迟时间
|
||||||
@ -22,5 +20,6 @@ export const gameConfig = {
|
|||||||
|
|
||||||
player: {
|
player: {
|
||||||
bulletSpeed: 10,
|
bulletSpeed: 10,
|
||||||
|
totalLife: 10,
|
||||||
},
|
},
|
||||||
}
|
}
|
@ -1,7 +0,0 @@
|
|||||||
// This is a demo code file
|
|
||||||
// Feel free to delete it
|
|
||||||
|
|
||||||
export interface MsgChat {
|
|
||||||
content: string,
|
|
||||||
time: Date
|
|
||||||
}
|
|
@ -0,0 +1,14 @@
|
|||||||
|
import { uint } from "tsrpc";
|
||||||
|
import { BaseConf, BaseRequest, BaseResponse } from "./base";
|
||||||
|
|
||||||
|
export interface ReqLogin extends BaseRequest {
|
||||||
|
nickname: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResLogin extends BaseResponse {
|
||||||
|
playerId: uint
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conf: BaseConf = {
|
||||||
|
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
// This is a demo code file
|
|
||||||
// Feel free to delete it
|
|
||||||
|
|
||||||
export interface ReqSend {
|
|
||||||
content: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResSend {
|
|
||||||
time: Date
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
import { BulletHit, PlayerHitEnemy, PlayerHitFightIcon, PlayerHurt, PlayerMove } from "../../../game/GameSystemInput";
|
||||||
|
|
||||||
|
/** 发送自己的输入 */
|
||||||
|
export interface MsgGameInput {
|
||||||
|
sn: number,
|
||||||
|
inputs: ClientInput[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientInput = Omit<PlayerMove, 'playerId'>
|
||||||
|
| Omit<PlayerHitEnemy, 'playerId'>
|
||||||
|
| Omit<PlayerHitFightIcon, 'playerId'>
|
||||||
|
| Omit<PlayerHurt, 'playerId'>
|
||||||
|
| Omit<BulletHit, 'playerId'>;
|
@ -0,0 +1,5 @@
|
|||||||
|
export interface MsgGameOver {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const conf = {}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { uint } from "tsrpc";
|
||||||
|
import { GameSystemState } from "../../../game/GameSystemState";
|
||||||
|
|
||||||
|
export interface MsgGameStart {
|
||||||
|
frameIndex: uint,
|
||||||
|
gameState: GameSystemState
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { uint } from "tsrpc";
|
||||||
|
import { GameSystemInput } from "../../../game/GameSystemInput";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务端定期广播的同步帧
|
||||||
|
* 包含了这一段期间所有输入
|
||||||
|
*/
|
||||||
|
export interface MsgServerFrame {
|
||||||
|
frameIndex: uint,
|
||||||
|
inputs: GameSystemInput[],
|
||||||
|
/** 当前用户提交的,经服务端确认的最后一条输入的 SN */
|
||||||
|
lastSn?: number
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { uint } from "tsrpc-proto";
|
||||||
|
import { BaseConf, BaseRequest, BaseResponse } from "../base";
|
||||||
|
|
||||||
|
export interface ReqCreateRoom extends BaseRequest {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResCreateRoom extends BaseResponse {
|
||||||
|
roomId: uint
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conf: BaseConf = {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { BaseRequest, BaseResponse, BaseConf } from "../base";
|
||||||
|
|
||||||
|
export interface ReqGetRoomList extends BaseRequest {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResGetRoomList extends BaseResponse {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conf: BaseConf = {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { uint } from "tsrpc-proto";
|
||||||
|
import { BaseConf, BaseRequest, BaseResponse } from "../base";
|
||||||
|
|
||||||
|
export interface ReqJoinRoom extends BaseRequest {
|
||||||
|
roomId: uint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResJoinRoom extends BaseResponse {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conf: BaseConf = {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { BaseConf, BaseRequest, BaseResponse } from "../base";
|
||||||
|
|
||||||
|
/** 准备 */
|
||||||
|
export interface ReqSetReady extends BaseRequest {
|
||||||
|
isReady: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResSetReady extends BaseResponse {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conf: BaseConf = {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { RoomState } from "../../../../models/Room";
|
||||||
|
|
||||||
|
export interface MsgUpdateRoomState {
|
||||||
|
state: RoomState
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const conf = {}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
|||||||
|
import { uint } from "tsrpc";
|
||||||
|
|
||||||
|
export interface CurrentUser {
|
||||||
|
id: uint,
|
||||||
|
nickname: string
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -5,5 +5,8 @@
|
|||||||
"name": "airplane",
|
"name": "airplane",
|
||||||
"type": "3d",
|
"type": "3d",
|
||||||
"uuid": "c794458c-05f6-4c9f-909b-20d54897d219",
|
"uuid": "c794458c-05f6-4c9f-909b-20d54897d219",
|
||||||
"version": "3.4.0"
|
"version": "3.4.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tsrpc-browser": "^3.1.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user