This commit is contained in:
2022-05-04 11:13:09 +08:00
parent 3d331ee10d
commit 0b76b5935d
68 changed files with 38788 additions and 1 deletions

View File

@@ -0,0 +1,12 @@
import { ApiCallWs } from "tsrpc";
import { roomInstance } from "..";
import { ReqJoin, ResJoin } from "../shared/protocols/PtlJoin";
export async function ApiJoin(call: ApiCallWs<ReqJoin, ResJoin>): Promise<void> {
let playerId: number = roomInstance.join(call.req, call.conn);
call.succ({
playerId: playerId,
gameState: roomInstance.gameSystem.state
});
}

40
backend1/src/index.ts Normal file
View File

@@ -0,0 +1,40 @@
import "k8w-extend-native";
import * as path from "path";
import { WsConnection, WsServer } from "tsrpc";
import { Room } from "./models/Room";
import { serviceProto, ServiceType } from "./shared/protocols/serviceProto";
require("dotenv").config();
// 创建 TSRPC WebSocket Server
export const server: WsServer<ServiceType> = new WsServer(serviceProto, {
port: +process.env.PORT! || 3000,
json: true
});
// 断开连接后退出房间
server.flows.postDisconnectFlow.push(v => {
let conn: WsConnection<ServiceType> = v.conn as WsConnection<ServiceType>;
if (conn.playerId) {
roomInstance.leave(conn.playerId, conn);
}
return v;
});
export const roomInstance: Room = new Room(server);
// 初始化
async function init(): Promise<void> {
// 挂载 API 接口
await server.autoImplementApi(path.resolve(__dirname, "api"));
// TODO
// Prepare something... (e.g. connect the db)
}
// 启动入口点
async function main(): Promise<void> {
await init();
await server.start();
}
main();

101
backend1/src/models/Room.ts Normal file
View File

@@ -0,0 +1,101 @@
import { WsConnection, WsServer } from "tsrpc";
import { gameConfig } from "../shared/game/gameConfig";
import { GameSystem, GameSystemInput, PlayerJoin } from "../shared/game/GameSystem";
import { ReqJoin } from "../shared/protocols/PtlJoin";
import { ServiceType } from "../shared/protocols/serviceProto";
/**
* 服务端 - 房间 - 逻辑系统
*/
export class Room {
// 帧同步频率,次数/秒
syncRate = gameConfig.syncRate;
nextPlayerId = 1;
gameSystem = new GameSystem();
server: WsServer<ServiceType>;
conns: WsConnection<ServiceType>[] = [];
pendingInputs: GameSystemInput[] = [];
playerLastSn: { [playerId: number]: number | undefined } = {};
lastSyncTime?: number;
constructor(server: WsServer<ServiceType>) {
this.server = server;
setInterval(() => { this.sync(); }, 1000 / this.syncRate);
}
/** 加入房间 */
join(req: ReqJoin, conn: WsConnection<ServiceType>): number {
let input: PlayerJoin = {
type: "PlayerJoin",
playerId: this.nextPlayerId++,
// 初始位置随机
pos: {
x: Math.random() * 10 - 5,
y: Math.random() * 10 - 5
}
};
this.applyInput(input);
this.conns.push(conn);
conn.playerId = input.playerId;
conn.listenMsg("client/ClientInput", call => {
this.playerLastSn[input.playerId] = call.msg.sn;
call.msg.inputs.forEach(v => {
this.applyInput({
...v,
playerId: input.playerId
});
});
});
return input.playerId;
}
applyInput(input: GameSystemInput): void {
this.pendingInputs.push(input);
}
sync(): void {
let inputs = this.pendingInputs;
this.pendingInputs = [];
// Apply inputs
inputs.forEach(v => {
this.gameSystem.applyInput(v);
});
// Apply TimePast
let now = process.uptime() * 1000;
this.applyInput({
type: "TimePast",
dt: now - (this.lastSyncTime ?? now)
});
this.lastSyncTime = now;
// 发送同步帧
this.conns.forEach(v => {
v.sendMsg("server/Frame", {
inputs: inputs,
lastSn: this.playerLastSn[v.playerId!]
});
});
}
/** 离开房间 */
leave(playerId: number, conn: WsConnection<ServiceType>): void {
this.conns.removeOne(v => v.playerId === playerId);
this.applyInput({
type: "PlayerLeave",
playerId: playerId
});
}
}
declare module "tsrpc" {
export interface WsConnection {
playerId?: number;
}
}

View File

@@ -0,0 +1,143 @@
import { gameConfig } from "./gameConfig";
import { ArrowState } from "./state/ArrowState";
import { PlayerState } from "./state/PlayerState";
// 状态定义
export interface GameSystemState {
// 当前的时间(游戏时间)
now: number,
// 玩家
players: PlayerState[],
// 飞行中的箭矢
arrows: ArrowState[],
// 箭矢的 ID 生成
nextArrowId: number
}
/**
* 前后端复用的状态计算模块
*/
export class GameSystem {
// 当前状态
private _state: GameSystemState = {
now: 0,
players: [],
arrows: [],
nextArrowId: 1
}
get state(): Readonly<GameSystemState> {
return this._state
}
// 重设状态
reset(state: GameSystemState) {
this._state = Object.merge({}, state);
}
// 应用输入,计算状态变更
applyInput(input: GameSystemInput) {
if (input.type === 'PlayerMove') {
let player = this._state.players.find(v => v.id === input.playerId);
if (!player) {
return;
}
if (player.dizzyEndTime && player.dizzyEndTime > this._state.now) {
return;
}
player.pos.x += input.speed.x * input.dt;
player.pos.y += input.speed.y * input.dt;
}
else if (input.type === 'PlayerAttack') {
let player = this._state.players.find(v => v.id === input.playerId);
if (player) {
let newArrow: ArrowState = {
id: this._state.nextArrowId++,
fromPlayerId: input.playerId,
targetPos: { ...input.targetPos },
targetTime: input.targetTime
};
this._state.arrows.push(newArrow);
this.onNewArrow.forEach(v => v(newArrow));
}
}
else if (input.type === 'PlayerJoin') {
this.state.players.push({
id: input.playerId,
pos: { ...input.pos }
})
}
else if (input.type === 'PlayerLeave') {
this.state.players.remove(v => v.id === input.playerId);
}
else if (input.type === 'TimePast') {
this._state.now += input.dt;
// 落地的 Arrow
for (let i = this._state.arrows.length - 1; i > -1; --i) {
let arrow = this._state.arrows[i];
if (arrow.targetTime <= this._state.now) {
// 伤害判定
let damagedPlayers = this._state.players.filter(v => {
return (v.pos.x - arrow.targetPos.x) * (v.pos.x - arrow.targetPos.x) + (v.pos.y - arrow.targetPos.y) * (v.pos.y - arrow.targetPos.y) <= gameConfig.arrowAttackRadius * gameConfig.arrowAttackRadius
});
damagedPlayers.forEach(p => {
// 设置击晕状态
p.dizzyEndTime = this._state.now + gameConfig.arrowDizzyTime;
// Event
})
this._state.arrows.splice(i, 1);
}
}
}
}
/*
* 事件
* 某些转瞬即逝的事件,可能不会直观的体现在前后两帧状态的变化中,但表面层又需要知晓。
* 例如一颗狙击枪的子弹,在少于一帧的时间内创建和销毁,前后两帧的状态中都不包含这颗子弹;但表现层却需要绘制出子弹的弹道。
* 此时,可以通过事件的方式通知表现层。
*/
// 发射箭矢
onNewArrow: ((arrow: ArrowState) => void)[] = [];
}
export interface PlayerMove {
type: 'PlayerMove',
playerId: number,
speed: { x: number, y: number },
// 移动的时间 (秒)
dt: number,
}
export interface PlayerAttack {
type: 'PlayerAttack',
playerId: number,
// 落点坐标
targetPos: { x: number, y: number },
// 落点时间(游戏时间)
targetTime: number
}
export interface PlayerJoin {
type: 'PlayerJoin',
playerId: number,
pos: { x: number, y: number }
}
export interface PlayerLeave {
type: 'PlayerLeave',
playerId: number
}
// 时间流逝
export interface TimePast {
type: 'TimePast',
dt: number
}
// 输入定义
export type GameSystemInput = PlayerMove
| PlayerAttack
| PlayerJoin
| PlayerLeave
| TimePast;

View File

@@ -0,0 +1,14 @@
export const gameConfig = {
syncRate: 10,
moveSpeed: 10,
// 箭矢飞行时间(毫秒)
arrowFlyTime: 500,
// 箭矢投掷距离
arrowDistance: 8,
// 箭矢落地命中判定半径
arrowAttackRadius: 2,
// 被箭矢几种后的晕眩时间(毫秒)
arrowDizzyTime: 1000
};

View File

@@ -0,0 +1,9 @@
export type ArrowState = {
id: number,
// 谁发出的箭
fromPlayerId: number,
// 落地时间(游戏时间)
targetTime: number,
// 落点位置(游戏位置)
targetPos: { x: number, y: number }
}

View File

@@ -0,0 +1,7 @@
export interface PlayerState {
id: number,
// 位置
pos: { x: number, y: number },
// 晕眩结束时间
dizzyEndTime?: number,
}

View File

@@ -0,0 +1,15 @@
import { GameSystemState } from "../game/GameSystem";
/** 加入房间 */
export interface ReqJoin {
}
export interface ResJoin {
/** 加入房间后,自己的 ID */
playerId: number;
/** 状态同步:一次性同步当前状态 */
gameState: GameSystemState;
}
// export const conf = {}

View File

@@ -0,0 +1,9 @@
import { PlayerAttack, PlayerMove } from "../../game/GameSystem";
/** 发送自己的输入 */
export interface MsgClientInput {
sn: number,
inputs: ClientInput[]
};
export type ClientInput = Omit<PlayerMove, 'playerId'> | Omit<PlayerAttack, 'playerId'>;

View File

@@ -0,0 +1,11 @@
import { GameSystemInput } from "../../game/GameSystem";
/**
* 服务端定期广播的同步帧
* 包含了这一段期间所有输入
*/
export interface MsgFrame {
inputs: GameSystemInput[],
/** 当前用户提交的,经服务端确认的最后一条输入的 SN */
lastSn?: number
}

View File

@@ -0,0 +1,496 @@
import { ServiceProto } from "tsrpc-proto";
import { MsgClientInput } from "./client/MsgClientInput";
import { ReqJoin, ResJoin } from "./PtlJoin";
import { MsgFrame } from "./server/MsgFrame";
export interface ServiceType {
api: {
"Join": {
req: ReqJoin,
res: ResJoin
}
};
msg: {
"client/ClientInput": MsgClientInput,
"server/Frame": MsgFrame
};
}
export const serviceProto: ServiceProto<ServiceType> = {
"services": [
{
"id": 0,
"name": "client/ClientInput",
"type": "msg"
},
{
"id": 1,
"name": "Join",
"type": "api"
},
{
"id": 2,
"name": "server/Frame",
"type": "msg"
}
],
"types": {
"client/MsgClientInput/MsgClientInput": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "sn",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "inputs",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "client/MsgClientInput/ClientInput"
}
}
}
]
},
"client/MsgClientInput/ClientInput": {
"type": "Union",
"members": [
{
"id": 0,
"type": {
"target": {
"type": "Reference",
"target": "../game/GameSystem/PlayerMove"
},
"keys": [
"playerId"
],
"type": "Omit"
}
},
{
"id": 1,
"type": {
"target": {
"type": "Reference",
"target": "../game/GameSystem/PlayerAttack"
},
"keys": [
"playerId"
],
"type": "Omit"
}
}
]
},
"../game/GameSystem/PlayerMove": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "PlayerMove"
}
},
{
"id": 1,
"name": "playerId",
"type": {
"type": "Number"
}
},
{
"id": 2,
"name": "speed",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
},
{
"id": 3,
"name": "dt",
"type": {
"type": "Number"
}
}
]
},
"../game/GameSystem/PlayerAttack": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "PlayerAttack"
}
},
{
"id": 1,
"name": "playerId",
"type": {
"type": "Number"
}
},
{
"id": 2,
"name": "targetPos",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
},
{
"id": 3,
"name": "targetTime",
"type": {
"type": "Number"
}
}
]
},
"PtlJoin/ReqJoin": {
"type": "Interface"
},
"PtlJoin/ResJoin": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "playerId",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "gameState",
"type": {
"type": "Reference",
"target": "../game/GameSystem/GameSystemState"
}
}
]
},
"../game/GameSystem/GameSystemState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "now",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "players",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "../game/state/PlayerState/PlayerState"
}
}
},
{
"id": 2,
"name": "arrows",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "../game/state/ArrowState/ArrowState"
}
}
},
{
"id": 3,
"name": "nextArrowId",
"type": {
"type": "Number"
}
}
]
},
"../game/state/PlayerState/PlayerState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "pos",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
},
{
"id": 2,
"name": "dizzyEndTime",
"type": {
"type": "Number"
},
"optional": true
}
]
},
"../game/state/ArrowState/ArrowState": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "id",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "fromPlayerId",
"type": {
"type": "Number"
}
},
{
"id": 2,
"name": "targetTime",
"type": {
"type": "Number"
}
},
{
"id": 3,
"name": "targetPos",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
}
]
},
"server/MsgFrame/MsgFrame": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "inputs",
"type": {
"type": "Array",
"elementType": {
"type": "Reference",
"target": "../game/GameSystem/GameSystemInput"
}
}
},
{
"id": 1,
"name": "lastSn",
"type": {
"type": "Number"
},
"optional": true
}
]
},
"../game/GameSystem/GameSystemInput": {
"type": "Union",
"members": [
{
"id": 0,
"type": {
"type": "Reference",
"target": "../game/GameSystem/PlayerMove"
}
},
{
"id": 1,
"type": {
"type": "Reference",
"target": "../game/GameSystem/PlayerAttack"
}
},
{
"id": 2,
"type": {
"type": "Reference",
"target": "../game/GameSystem/PlayerJoin"
}
},
{
"id": 3,
"type": {
"type": "Reference",
"target": "../game/GameSystem/PlayerLeave"
}
},
{
"id": 4,
"type": {
"type": "Reference",
"target": "../game/GameSystem/TimePast"
}
}
]
},
"../game/GameSystem/PlayerJoin": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "PlayerJoin"
}
},
{
"id": 1,
"name": "playerId",
"type": {
"type": "Number"
}
},
{
"id": 2,
"name": "pos",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "x",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "y",
"type": {
"type": "Number"
}
}
]
}
}
]
},
"../game/GameSystem/PlayerLeave": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "PlayerLeave"
}
},
{
"id": 1,
"name": "playerId",
"type": {
"type": "Number"
}
}
]
},
"../game/GameSystem/TimePast": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "type",
"type": {
"type": "Literal",
"literal": "TimePast"
}
},
{
"id": 1,
"name": "dt",
"type": {
"type": "Number"
}
}
]
}
}
};