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 {
|
||||
id: uint,
|
||||
nickname: string,
|
||||
// 得分
|
||||
score: number,
|
||||
// 生命值
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { EnemyType } from "./GameSystemState";
|
||||
|
||||
export const gameConfig = {
|
||||
syncRate: 10,
|
||||
|
||||
enemy: {
|
||||
bulletSpeed: 20,
|
||||
// 第一次发射子弹的延迟时间
|
||||
@ -22,5 +20,6 @@ export const gameConfig = {
|
||||
|
||||
player: {
|
||||
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",
|
||||
"type": "3d",
|
||||
"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