This commit is contained in:
sli97 2022-12-07 22:24:46 +08:00
parent b7c95c5ca9
commit c5acb09642
24 changed files with 280 additions and 176 deletions

View File

@ -8,7 +8,6 @@ export abstract class EntityManager extends Component {
fsm: StateMachine fsm: StateMachine
private _state: EntityStateEnum private _state: EntityStateEnum
get state() { get state() {
return this._state return this._state
} }

View File

@ -1,5 +1,6 @@
import { _decorator, Animation, Component } from 'cc' import { _decorator, Animation, Component } from 'cc'
import { EntityTypeEnum, FsmParamTypeEnum } from '../Enum' import { EntityTypeEnum } from '../Common'
import { FsmParamTypeEnum } from '../Enum'
const { ccclass } = _decorator const { ccclass } = _decorator
import State from './State' import State from './State'
import SubStateMachine from './SubStateMachine' import SubStateMachine from './SubStateMachine'

View File

@ -1,8 +1,9 @@
import { _decorator, instantiate, ProgressBar, Label, Vec3, Tween, tween } from 'cc'; import { _decorator, instantiate, ProgressBar, Label, Vec3, Tween, tween, director } from 'cc';
import { EntityManager } from '../../Base/EntityManager'; import { EntityManager } from '../../Base/EntityManager';
import { ApiMsgEnum, EntityTypeEnum, IActor, InputTypeEnum, IVec2, toFixed } from '../../Common'; import { ApiMsgEnum, EntityTypeEnum, IActor, InputTypeEnum, IVec2, toFixed } from '../../Common';
import { EntityStateEnum } from '../../Enum'; import { EntityStateEnum, EventEnum, SceneEnum } from '../../Enum';
import DataManager from '../../Global/DataManager'; import DataManager from '../../Global/DataManager';
import EventManager from '../../Global/EventManager';
import NetworkManager from '../../Global/NetworkManager'; import NetworkManager from '../../Global/NetworkManager';
import { rad2Angle } from '../../Utils'; import { rad2Angle } from '../../Utils';
import { WeaponManager } from '../Weapon/WeaponManager'; import { WeaponManager } from '../Weapon/WeaponManager';
@ -30,6 +31,8 @@ export class ActorManager extends EntityManager implements IActor {
private tw: Tween<any> private tw: Tween<any>
private targetPos: Vec3 private targetPos: Vec3
private isDead = false
get isSelf() { get isSelf() {
return DataManager.Instance.myPlayerId === this.id return DataManager.Instance.myPlayerId === this.id
} }
@ -60,11 +63,17 @@ export class ActorManager extends EntityManager implements IActor {
this.node.active = false this.node.active = false
} }
tick(dt: number) { async tick(dt: number) {
if (!this.isSelf) { if (!this.isSelf) {
return return
} }
if (this.hp <= 0 && !this.isDead) {
EventManager.Instance.emit(EventEnum.GameEnd)
this.isDead = true
return
}
if (DataManager.Instance.jm.input.length()) { if (DataManager.Instance.jm.input.length()) {
const { x, y } = DataManager.Instance.jm.input const { x, y } = DataManager.Instance.jm.input
NetworkManager.Instance.sendMsg(ApiMsgEnum.MsgClientSync, { NetworkManager.Instance.sendMsg(ApiMsgEnum.MsgClientSync, {
@ -87,6 +96,7 @@ export class ActorManager extends EntityManager implements IActor {
} }
renderHP(data: IActor) { renderHP(data: IActor) {
this.hp = data.hp
this.hpBar.progress = data.hp / this.hpBar.totalLength this.hpBar.progress = data.hp / this.hpBar.totalLength
} }

View File

@ -21,6 +21,7 @@ export enum EventEnum {
ExplosionBorn = 'ExplosionBorn', ExplosionBorn = 'ExplosionBorn',
RoomJoin = 'RoomJoin', RoomJoin = 'RoomJoin',
GameStart = 'GameStart', GameStart = 'GameStart',
GameEnd = 'GameEnd',
} }
export enum PrefabPathEnum { export enum PrefabPathEnum {

View File

@ -16,25 +16,36 @@ const WEAPON_DAMAGE = 5
const PLAYER_RADIUS = 50 const PLAYER_RADIUS = 50
const BULLET_RADIUS = 10 const BULLET_RADIUS = 10
const mapW = 960
const mapH = 640
export default class DataManager extends Singleton { export default class DataManager extends Singleton {
static get Instance() { static get Instance() {
return super.GetInstance<DataManager>() return super.GetInstance<DataManager>()
} }
//登陆数据
myPlayerId = 1
//大厅数据
roomInfo: IRoom
//游戏数据
stage: Node stage: Node
jm: JoyStickManager jm: JoyStickManager
prefabMap: Map<string, Prefab> = new Map() prefabMap: Map<string, Prefab> = new Map()
textureMap: Map<string, SpriteFrame[]> = new Map() textureMap: Map<string, SpriteFrame[]> = new Map()
actorMap: Map<number, ActorManager> = new Map() actorMap: Map<number, ActorManager> = new Map()
bulletMap: Map<number, BulletManager> = new Map() bulletMap: Map<number, BulletManager> = new Map()
myPlayerId = 1 reset() {
roomInfo: IRoom this.stage = null
mapSize = { this.jm = null
x: 960, this.actorMap.clear()
y: 640, this.bulletMap.clear()
this.prefabMap.clear()
this.textureMap.clear()
} }
state: IState = { state: IState = {
@ -85,8 +96,8 @@ export default class DataManager extends Singleton {
player.position.x += toFixed(x * PLAYER_SPEED * dt) player.position.x += toFixed(x * PLAYER_SPEED * dt)
player.position.y += toFixed(y * PLAYER_SPEED * dt) player.position.y += toFixed(y * PLAYER_SPEED * dt)
player.position.x = clamp(player.position.x, -this.mapSize.x / 2, this.mapSize.x / 2) player.position.x = clamp(player.position.x, -mapW / 2, mapW / 2)
player.position.y = clamp(player.position.y, -this.mapSize.y / 2, this.mapSize.y / 2) player.position.y = clamp(player.position.y, -mapH / 2, mapH / 2)
player.direction = { x, y } player.direction = { x, y }
break break
@ -124,7 +135,7 @@ export default class DataManager extends Singleton {
break break
} }
} }
if (Math.abs(bullet.position.x) > this.mapSize.x / 2 || Math.abs(bullet.position.y) > this.mapSize.y / 2) { if (Math.abs(bullet.position.x) > mapW / 2 || Math.abs(bullet.position.y) > mapH / 2) {
EventManager.Instance.emit(EventEnum.ExplosionBorn, bullet.id, { EventManager.Instance.emit(EventEnum.ExplosionBorn, bullet.id, {
x: bullet.position.x, x: bullet.position.x,
y: bullet.position.y, y: bullet.position.y,

View File

@ -2,7 +2,7 @@ import Singleton from '../Base/Singleton'
import { EventEnum } from '../Enum'; import { EventEnum } from '../Enum';
interface IItem { interface IItem {
func: Function; cb: Function;
ctx: unknown; ctx: unknown;
} }
@ -13,25 +13,25 @@ export default class EventManager extends Singleton {
private map: Map<EventEnum, Array<IItem>> = new Map(); private map: Map<EventEnum, Array<IItem>> = new Map();
on(event: EventEnum, func: Function, ctx?: unknown) { on(event: EventEnum, cb: Function, ctx: unknown) {
if (this.map.has(event)) { if (this.map.has(event)) {
this.map.get(event).push({ func, ctx }); this.map.get(event).push({ cb, ctx });
} else { } else {
this.map.set(event, [{ func, ctx }]); this.map.set(event, [{ cb, ctx }]);
} }
} }
off(event: EventEnum, func: Function, ctx?: unknown) { off(event: EventEnum, cb: Function, ctx: unknown) {
if (this.map.has(event)) { if (this.map.has(event)) {
const index = this.map.get(event).findIndex(i => func === i.func && i.ctx === ctx); const index = this.map.get(event).findIndex(i => cb === i.cb && i.ctx === ctx);
index > -1 && this.map.get(event).splice(index, 1); index > -1 && this.map.get(event).splice(index, 1);
} }
} }
emit(event: EventEnum, ...params: unknown[]) { emit(event: EventEnum, ...params: unknown[]) {
if (this.map.has(event)) { if (this.map.has(event)) {
this.map.get(event).forEach(({ func, ctx }) => { this.map.get(event).forEach(({ cb, ctx }) => {
ctx ? func.apply(ctx, params) : func(...params); cb.apply(ctx, params)
}); });
} }
} }

View File

@ -1,9 +1,14 @@
import Singleton from '../Base/Singleton' import Singleton from '../Base/Singleton'
import { ApiMsgEnum, IModel, strdecode, strencode } from '../Common'; import { ApiMsgEnum, IModel } from '../Common';
import { binaryEncode, binaryDecode } from '../Common/Binary'; import { binaryEncode, binaryDecode } from '../Common/Binary';
const TIMEOUT = 5000 const TIMEOUT = 5000
interface IItem {
cb: Function;
ctx: unknown;
}
export interface ICallApiRet<T> { export interface ICallApiRet<T> {
success: boolean; success: boolean;
error?: Error; error?: Error;
@ -18,7 +23,7 @@ export default class NetworkManager extends Singleton {
ws: WebSocket ws: WebSocket
port = 8888 port = 8888
maps: Map<ApiMsgEnum, Function[]> = new Map() maps: Map<ApiMsgEnum, Array<IItem>> = new Map()
isConnected = false isConnected = false
connect() { connect() {
@ -28,16 +33,17 @@ export default class NetworkManager extends Singleton {
return return
} }
this.ws = new WebSocket(`ws://localhost:${this.port}`) this.ws = new WebSocket(`ws://localhost:${this.port}`)
//onmessage接受的数据类型只有在后端返回字节数组的时候才有效果
this.ws.binaryType = 'arraybuffer'; this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => { this.ws.onopen = () => {
console.log("ws onopen")
this.isConnected = true this.isConnected = true
resolve(true) resolve(true)
} }
this.ws.onerror = () => { this.ws.onerror = (e) => {
this.isConnected = false this.isConnected = false
console.log(e)
reject("ws onerror") reject("ws onerror")
} }
@ -53,12 +59,11 @@ export default class NetworkManager extends Singleton {
try { try {
if (this.maps.has(name) && this.maps.get(name).length) { if (this.maps.has(name) && this.maps.get(name).length) {
console.log(json); console.log(json);
this.maps.get(name).forEach(cb => cb(data)) this.maps.get(name).forEach(({ cb, ctx }) => cb.call(ctx, data))
} }
} catch (error) { } catch (error) {
console.log("this.maps.get(name).forEach(cb => cb(restData))", error) console.log("onmessage:", error)
} }
} catch (error) { } catch (error) {
console.log('解析失败不是合法的JSON格式', error) console.log('解析失败不是合法的JSON格式', error)
} }
@ -72,20 +77,19 @@ export default class NetworkManager extends Singleton {
// 超时处理 // 超时处理
const timer = setTimeout(() => { const timer = setTimeout(() => {
resolve({ success: false, error: new Error('timeout') }) resolve({ success: false, error: new Error('timeout') })
this.unlistenMsg(name, cb) this.unlistenMsg(name as any, cb, null)
}, TIMEOUT) }, TIMEOUT)
// 回调处理 // 回调处理
const cb = (res) => { const cb = (res) => {
resolve(res) resolve(res)
clearTimeout(timer) clearTimeout(timer)
this.unlistenMsg(name, cb) this.unlistenMsg(name as any, cb, null)
} }
this.listenMsg(name as any, cb) this.listenMsg(name as any, cb, null)
this.sendMsg(name as any, data) this.sendMsg(name as any, data)
} catch (error) { } catch (error) {
console.log(error)
resolve({ success: false, error: error as Error }) resolve({ success: false, error: error as Error })
} }
}) })
@ -98,18 +102,19 @@ export default class NetworkManager extends Singleton {
this.ws.send(view.buffer) this.ws.send(view.buffer)
} }
listenMsg<T extends keyof IModel['msg']>(name: T, cb: (args: IModel['msg'][T]) => void) { listenMsg<T extends keyof IModel['msg']>(name: T, cb: (args: IModel['msg'][T]) => void, ctx: unknown) {
if (this.maps.has(name)) { if (this.maps.has(name)) {
this.maps.get(name).push(cb) this.maps.get(name).push({ ctx, cb })
} else { } else {
this.maps.set(name, [cb]) this.maps.set(name, [{ ctx, cb }])
} }
} }
unlistenMsg(name: ApiMsgEnum, cb: Function) { unlistenMsg<T extends keyof IModel['msg']>(name: T, cb: (args: IModel['msg'][T]) => void, ctx: unknown) {
if (this.maps.has(name)) { if (this.maps.has(name)) {
const index = this.maps.get(name).indexOf(cb) const items = this.maps.get(name)
index > -1 && this.maps.get(name).splice(index, 1) const index = items.findIndex(i => cb === i.cb && i.ctx === ctx);
index > -1 && items.splice(index, 1)
} }
} }
} }

View File

@ -15,6 +15,11 @@ export default class ObjectPoolManager extends Singleton {
return objectName + 'Pool' return objectName + 'Pool'
} }
reset() {
this.objectPool = null
this.map.clear()
}
get(objectName: EntityTypeEnum) { get(objectName: EntityTypeEnum) {
if (this.objectPool === null) { if (this.objectPool === null) {
this.objectPool = new Node("ObjectPool") this.objectPool = new Node("ObjectPool")

View File

@ -1,37 +1,52 @@
import { _decorator, Component, Node, Prefab, instantiate, SpriteFrame } from 'cc'; import { _decorator, Component, Node, Prefab, instantiate, SpriteFrame, director } from 'cc';
import { ActorManager } from '../Entity/Actor/ActorManager'; import { ActorManager } from '../Entity/Actor/ActorManager';
import DataManager from '../Global/DataManager'; import DataManager from '../Global/DataManager';
import { JoyStickManager } from '../UI/JoyStickManager'; import { JoyStickManager } from '../UI/JoyStickManager';
import { ResourceManager } from '../Global/ResourceManager'; import { ResourceManager } from '../Global/ResourceManager';
import { PrefabPathEnum, TexturePathEnum } from '../Enum'; import { EventEnum, PrefabPathEnum, SceneEnum, TexturePathEnum } from '../Enum';
import NetworkManager from '../Global/NetworkManager'; import NetworkManager from '../Global/NetworkManager';
import ObjectPoolManager from '../Global/ObjectPoolManager'; import ObjectPoolManager from '../Global/ObjectPoolManager';
import { BulletManager } from '../Entity/Bullet/BulletManager'; import { BulletManager } from '../Entity/Bullet/BulletManager';
import { ApiMsgEnum, EntityTypeEnum, IMsgServerSync, InputTypeEnum } from '../Common'; import { ApiMsgEnum, EntityTypeEnum, IMsgServerSync, InputTypeEnum } from '../Common';
import EventManager from '../Global/EventManager';
const { ccclass } = _decorator; const { ccclass } = _decorator;
@ccclass('BattleManager') @ccclass('BattleManager')
export class BattleManager extends Component { export class BattleManager extends Component {
stage: Node private stage: Node
ui: Node private ui: Node
isInited = false private shouldUpdate = false
onLoad() {
this.stage = DataManager.Instance.stage = this.node.getChildByName("Stage")
this.ui = this.node.getChildByName("UI")
}
async start() { async start() {
//清空
this.clearGame() this.clearGame()
await this.loadRes()
this.initScene() //资源加载和网络连接同步执行
await this.connectServer() await Promise.all([this.loadRes(), this.connectServer()])
this.initGame()
// 在场景初始化完毕之前,卡主别的玩家,准备好以后再告知服务器,等所有玩家都准备好以后才开始,这里就不做了 // 在场景初始化完毕之前,卡主别的玩家,准备好以后再告知服务器,等所有玩家都准备好以后才开始,这里就不做了
NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgServerSync, this.handleSync); NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgServerSync, this.handleSync, this);
this.isInited = true NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgGameEnd, this.leaveGame, this);
EventManager.Instance.on(EventEnum.GameEnd, this.handleGameEnd, this)
} }
clearGame() { clearGame() {
//监听
NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgServerSync, this.handleSync, this);
NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgGameEnd, this.leaveGame, this);
EventManager.Instance.off(EventEnum.GameEnd, this.handleGameEnd, this)
//数据
this.shouldUpdate = false
ObjectPoolManager.Instance.reset()
DataManager.Instance.reset()
//节点
this.stage = DataManager.Instance.stage = this.node.getChildByName("Stage")
this.ui = this.node.getChildByName("UI")
this.stage.destroyAllChildren() this.stage.destroyAllChildren()
this.ui.destroyAllChildren() this.ui.destroyAllChildren()
} }
@ -53,12 +68,6 @@ export class BattleManager extends Component {
await Promise.all(list) await Promise.all(list)
} }
async initScene() {
this.initJoyStick()
this.initShoot()
this.initMap()
}
async connectServer() { async connectServer() {
if (!await NetworkManager.Instance.connect().catch(() => false)) { if (!await NetworkManager.Instance.connect().catch(() => false)) {
await new Promise((resolve) => setTimeout(resolve, 1000)) await new Promise((resolve) => setTimeout(resolve, 1000))
@ -66,6 +75,26 @@ export class BattleManager extends Component {
} }
} }
leaveGame() {
this.clearGame()
director.loadScene(SceneEnum.Hall);
}
async handleGameEnd() {
const { success, res, error } = await NetworkManager.Instance.callApi(ApiMsgEnum.ApiGameEnd, { rid: DataManager.Instance.roomInfo.id })
if (!success) {
console.log(error)
return;
}
}
async initGame() {
this.initJoyStick()
this.initShoot()
this.initMap()
this.shouldUpdate = true
}
initJoyStick() { initJoyStick() {
const prefab = DataManager.Instance.prefabMap.get(EntityTypeEnum.JoyStick) const prefab = DataManager.Instance.prefabMap.get(EntityTypeEnum.JoyStick)
const joySitck = instantiate(prefab) const joySitck = instantiate(prefab)
@ -86,14 +115,8 @@ export class BattleManager extends Component {
map.setParent(this.stage) map.setParent(this.stage)
} }
handleSync(inputs: IMsgServerSync) {
for (const input of inputs) {
DataManager.Instance.applyInput(input)
}
}
update(dt: number) { update(dt: number) {
if (!this.isInited) { if (!this.shouldUpdate) {
return return
} }
this.render() this.render()
@ -158,5 +181,12 @@ export class BattleManager extends Component {
} }
} }
} }
handleSync(inputs: IMsgServerSync) {
for (const input of inputs) {
DataManager.Instance.applyInput(input)
}
}
} }

View File

@ -25,14 +25,14 @@ export class HallManager extends Component {
onLoad() { onLoad() {
director.preloadScene(SceneEnum.Room); director.preloadScene(SceneEnum.Room);
EventManager.Instance.on(EventEnum.RoomJoin, this.joinRoom, this) EventManager.Instance.on(EventEnum.RoomJoin, this.joinRoom, this)
NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgPlayerList, this.renderPlayers); NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgPlayerList, this.renderPlayers, this);
NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgRoomList, this.renderRooms); NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgRoomList, this.renderRooms, this);
} }
onDestroy() { onDestroy() {
EventManager.Instance.off(EventEnum.RoomJoin, this.joinRoom, this) EventManager.Instance.off(EventEnum.RoomJoin, this.joinRoom, this)
NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgPlayerList, this.renderPlayers); NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgPlayerList, this.renderPlayers, this);
NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgRoomList, this.renderRooms); NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgRoomList, this.renderRooms, this);
} }
start() { start() {

View File

@ -1,8 +1,7 @@
import { _decorator, Component, Node, Prefab, director, instantiate } from 'cc'; import { _decorator, Component, Node, Prefab, director, instantiate } from 'cc';
import { ApiMsgEnum, IMsgGameStart, IMsgRoom } from '../Common'; import { ApiMsgEnum, IMsgGameStart, IMsgRoom } from '../Common';
import { EventEnum, SceneEnum } from '../Enum'; import { SceneEnum } from '../Enum';
import DataManager from '../Global/DataManager'; import DataManager from '../Global/DataManager';
import EventManager from '../Global/EventManager';
import NetworkManager from '../Global/NetworkManager'; import NetworkManager from '../Global/NetworkManager';
import { PlayerManager } from '../UI/PlayerManager'; import { PlayerManager } from '../UI/PlayerManager';
const { ccclass, property } = _decorator; const { ccclass, property } = _decorator;
@ -17,13 +16,13 @@ export class RoomManager extends Component {
onLoad() { onLoad() {
director.preloadScene(SceneEnum.Battle); director.preloadScene(SceneEnum.Battle);
NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgRoom, this.renderPlayers); NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgRoom, this.renderPlayers, this);
NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgGameStart, this.startGame); NetworkManager.Instance.listenMsg(ApiMsgEnum.MsgGameStart, this.handleGameStart, this);
} }
onDestroy() { onDestroy() {
NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgRoom, this.renderPlayers); NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgRoom, this.renderPlayers, this);
NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgGameStart, this.startGame); NetworkManager.Instance.unlistenMsg(ApiMsgEnum.MsgGameStart, this.handleGameStart, this);
} }
async start() { async start() {
@ -32,7 +31,7 @@ export class RoomManager extends Component {
}) })
} }
renderPlayers = ({ room: { players: list } }: IMsgRoom) => { renderPlayers({ room: { players: list } }: IMsgRoom) {
for (const item of this.playerContainer.children) { for (const item of this.playerContainer.children) {
item.active = false item.active = false
} }
@ -67,13 +66,10 @@ export class RoomManager extends Component {
console.log(error) console.log(error)
return; return;
} }
// director.loadScene(SceneEnum.Battle);
} }
startGame = ({ state }: IMsgGameStart) => { handleGameStart({ state }: IMsgGameStart) {
DataManager.Instance.state = state DataManager.Instance.state = state
director.loadScene(SceneEnum.Battle); director.loadScene(SceneEnum.Battle);
} }
} }

View File

@ -9,7 +9,7 @@ export class RoomManager extends Component {
init({ id, players }: { id: number, players: Array<{ id: number, nickname: string }> }) { init({ id, players }: { id: number, players: Array<{ id: number, nickname: string }> }) {
this.id = id this.id = id
const label = this.getComponent(Label) const label = this.getComponent(Label)
label.string = `房间id:${id}当前人数:${players.length}` label.string = `房间id:${id},当前人数:${players.length}`
this.node.active = true this.node.active = true
} }

View File

@ -1,4 +1,10 @@
import { IPlayer, IRoom } from "./Model" export interface IPlayer {
id: number, nickname: string, rid: number
}
export interface IRoom {
id: number, players: Array<IPlayer>
}
export interface IApiPlayerListReq { export interface IApiPlayerListReq {
} }
@ -46,4 +52,10 @@ export interface IApiGameStartReq {
rid: number rid: number
} }
export interface IApiGameStartRes { } export interface IApiGameStartRes { }
export interface IApiGameEndReq {
rid: number
}
export interface IApiGameEndRes { }

View File

@ -22,10 +22,12 @@ export enum ApiMsgEnum {
ApiRoomJoin, ApiRoomJoin,
ApiRoomLeave, ApiRoomLeave,
ApiGameStart, ApiGameStart,
ApiGameEnd,
MsgPlayerList, MsgPlayerList,
MsgRoomList, MsgRoomList,
MsgRoom, MsgRoom,
MsgGameStart, MsgGameStart,
MsgGameEnd,
MsgClientSync, MsgClientSync,
MsgServerSync, MsgServerSync,
} }

View File

@ -1,9 +1,50 @@
import { IApiGameEndReq, IApiGameStartReq, IApiGameStartRes, IApiPlayerJoinReq, IApiPlayerJoinRes, IApiPlayerListReq, IApiPlayerListRes, IApiRoomCreateReq, IApiRoomCreateRes, IApiRoomJoinReq, IApiRoomJoinRes, IApiRoomLeaveReq, IApiRoomLeaveRes, IApiRoomListReq, IApiRoomListRes, IApiGameEndRes } from './Api'
import { ApiMsgEnum } from './Enum'
import { IMsgClientSync, IMsgGameEnd, IMsgGameStart, IMsgPlayerList, IMsgRoom, IMsgRoomList, IMsgServerSync } from './Msg'
export interface IPlayer { export interface IModel {
id: number, nickname: string, rid: number api: {
} [ApiMsgEnum.ApiPlayerJoin]: {
req: IApiPlayerJoinReq,
export interface IRoom { res: IApiPlayerJoinRes,
id: number, players: Array<IPlayer> }
[ApiMsgEnum.ApiPlayerList]: {
req: IApiPlayerListReq,
res: IApiPlayerListRes,
}
[ApiMsgEnum.ApiRoomList]: {
req: IApiRoomListReq,
res: IApiRoomListRes,
}
[ApiMsgEnum.ApiRoomCreate]: {
req: IApiRoomCreateReq,
res: IApiRoomCreateRes,
}
[ApiMsgEnum.ApiRoomJoin]: {
req: IApiRoomJoinReq,
res: IApiRoomJoinRes,
}
[ApiMsgEnum.ApiRoomLeave]: {
req: IApiRoomLeaveReq,
res: IApiRoomLeaveRes,
}
[ApiMsgEnum.ApiGameStart]: {
req: IApiGameStartReq,
res: IApiGameStartRes,
},
[ApiMsgEnum.ApiGameEnd]: {
req: IApiGameEndReq,
res: IApiGameEndRes,
}
},
msg: {
[ApiMsgEnum.MsgPlayerList]: IMsgPlayerList
[ApiMsgEnum.MsgRoomList]: IMsgRoomList,
[ApiMsgEnum.MsgRoom]: IMsgRoom,
[ApiMsgEnum.MsgGameStart]: IMsgGameStart,
[ApiMsgEnum.MsgGameEnd]: IMsgGameEnd,
[ApiMsgEnum.MsgClientSync]: IMsgClientSync,
[ApiMsgEnum.MsgServerSync]: IMsgServerSync,
}
} }

View File

@ -1,4 +1,4 @@
import { IPlayer, IRoom } from "./Model" import { IPlayer, IRoom } from "./Api"
import { IClientInput, IState } from "./State" import { IClientInput, IState } from "./State"
export interface IMsgPlayerList { export interface IMsgPlayerList {
@ -22,6 +22,10 @@ export interface IMsgGameStart {
state: IState state: IState
} }
export interface IMsgGameEnd {
}
export type IMsgClientSync = IClientInput export type IMsgClientSync = IClientInput
export type IMsgServerSync = Array<IClientInput> export type IMsgServerSync = Array<IClientInput>

View File

@ -7,36 +7,43 @@ export default class Room {
id: number id: number
players: Set<Player> = new Set() players: Set<Player> = new Set()
lastSyncTime?: number lastSyncTime?: number
timers: NodeJS.Timer[] = []
private timers: NodeJS.Timer[] = []
private inputs: Array<IClientInput> = [] private inputs: Array<IClientInput> = []
constructor(rid: number) { constructor(rid: number) {
this.id = rid this.id = rid
} }
join(uid: number) { join(uid: number) {
const player = PlayerManager.Instance.getPlayerById(uid) const player = PlayerManager.Instance.getPlayerById(uid)
if (player) { if (player) {
player.rid = this.id player.rid = this.id
this.players.add(player) this.players.add(player)
} }
} }
leave(uid: number) { leave(uid: number) {
const player = PlayerManager.Instance.getPlayerById(uid) const player = PlayerManager.Instance.getPlayerById(uid)
if (player) { if (player) {
player.rid = -1 player.rid = -1
player.connection.unlistenMsg(ApiMsgEnum.MsgClientSync, this.getInput, this)
this.players.delete(player) this.players.delete(player)
if (!this.players.size) { if (!this.players.size) {
this.timers.forEach(t => clearInterval(t))
RoomManager.Instance.closeRoom(this.id) RoomManager.Instance.closeRoom(this.id)
} }
} }
} }
close() {
this.timers.forEach(t => clearInterval(t))
for (const player of this.players) {
player.connection.sendMsg(ApiMsgEnum.MsgGameEnd, {})
player.connection.unlistenMsg(ApiMsgEnum.MsgClientSync, this.getInput, this)
}
this.players.clear()
}
sync() { sync() {
for (const player of this.players) { for (const player of this.players) {
player.connection.sendMsg(ApiMsgEnum.MsgRoom, { player.connection.sendMsg(ApiMsgEnum.MsgRoom, {
@ -71,10 +78,10 @@ export default class Room {
player.connection.sendMsg(ApiMsgEnum.MsgGameStart, { player.connection.sendMsg(ApiMsgEnum.MsgGameStart, {
state state
}) })
player.connection.listenMsg(ApiMsgEnum.MsgClientSync, this.getInput, this)
} }
this.listenPlayer()
let t1 = setInterval(() => { let t1 = setInterval(() => {
this.syncInput() this.sendInput()
}, 100) }, 100)
let t2 = setInterval(() => { let t2 = setInterval(() => {
this.timePast() this.timePast()
@ -82,19 +89,14 @@ export default class Room {
this.timers = [t1, t2] this.timers = [t1, t2]
} }
listenPlayer() { getInput(input: IClientInput) {
for (const player of this.players) { this.inputs.push(input)
player.connection.listenMsg(ApiMsgEnum.MsgClientSync, (input) => {
this.inputs.push(input)
})
}
} }
syncInput() { sendInput() {
const inputs = this.inputs const inputs = this.inputs
this.inputs = [] this.inputs = []
for (const player of this.players) { for (const player of this.players) {
player.connection.sendMsg(ApiMsgEnum.MsgServerSync, inputs) player.connection.sendMsg(ApiMsgEnum.MsgServerSync, inputs)
} }

View File

@ -37,6 +37,7 @@ export default class RoomManager extends Singleton {
closeRoom(rid: number) { closeRoom(rid: number) {
const room = this.getRoomById(rid) const room = this.getRoomById(rid)
if (room) { if (room) {
room.close()
this.rooms.delete(room) this.rooms.delete(room)
this.idMapRoom.delete(rid) this.idMapRoom.delete(rid)
} }

View File

@ -1,6 +1,3 @@
import { IApiGameStartReq, IApiGameStartRes, IApiPlayerJoinReq, IApiPlayerJoinRes, IApiPlayerListReq, IApiPlayerListRes, IApiRoomCreateReq, IApiRoomCreateRes, IApiRoomJoinReq, IApiRoomJoinRes, IApiRoomLeaveReq, IApiRoomLeaveRes, IApiRoomListReq, IApiRoomListRes } from './Api'
import { ApiMsgEnum } from './Enum'
import { IMsgClientSync, IMsgGameStart, IMsgPlayerList, IMsgRoom, IMsgRoomList, IMsgServerSync } from './Msg'
export * from './Api' export * from './Api'
export * from './Msg' export * from './Msg'
export * from './Enum' export * from './Enum'
@ -9,44 +6,3 @@ export * from './State'
export * from './Utils' export * from './Utils'
export * from './Binary' export * from './Binary'
export interface IModel {
api: {
[ApiMsgEnum.ApiPlayerJoin]: {
req: IApiPlayerJoinReq,
res: IApiPlayerJoinRes,
}
[ApiMsgEnum.ApiPlayerList]: {
req: IApiPlayerListReq,
res: IApiPlayerListRes,
}
[ApiMsgEnum.ApiRoomList]: {
req: IApiRoomListReq,
res: IApiRoomListRes,
}
[ApiMsgEnum.ApiRoomCreate]: {
req: IApiRoomCreateReq,
res: IApiRoomCreateRes,
}
[ApiMsgEnum.ApiRoomJoin]: {
req: IApiRoomJoinReq,
res: IApiRoomJoinRes,
}
[ApiMsgEnum.ApiRoomLeave]: {
req: IApiRoomLeaveReq,
res: IApiRoomLeaveRes,
}
[ApiMsgEnum.ApiGameStart]: {
req: IApiGameStartReq,
res: IApiGameStartRes,
}
},
msg: {
[ApiMsgEnum.MsgPlayerList]: IMsgPlayerList
[ApiMsgEnum.MsgRoomList]: IMsgRoomList,
[ApiMsgEnum.MsgRoom]: IMsgRoom,
[ApiMsgEnum.MsgGameStart]: IMsgGameStart,
[ApiMsgEnum.MsgClientSync]: IMsgClientSync,
[ApiMsgEnum.MsgServerSync]: IMsgServerSync,
}
}

View File

@ -9,10 +9,15 @@ export enum ConnectionEventEnum {
Close = 'Close', Close = 'Close',
} }
interface IItem {
cb: Function;
ctx: unknown;
}
export class Connection extends EventEmitter { export class Connection extends EventEmitter {
server: MyServer server: MyServer
ws: WebSocket ws: WebSocket
msgMap: Map<ApiMsgEnum, Function[]> = new Map() msgMap: Map<ApiMsgEnum, Array<IItem>> = new Map()
playerId?: number; playerId?: number;
constructor(server: MyServer, ws: WebSocket) { constructor(server: MyServer, ws: WebSocket) {
@ -34,7 +39,7 @@ export class Connection extends EventEmitter {
if (this.server.apiMap.has(name)) { if (this.server.apiMap.has(name)) {
try { try {
const cb = this.server.apiMap.get(name) const cb = this.server.apiMap.get(name)
const res = cb?.(this, data) const res = cb.call(null, data)
this.sendMsg(name, { this.sendMsg(name, {
success: true, success: true,
res, res,
@ -45,31 +50,34 @@ export class Connection extends EventEmitter {
error: (error as Error)?.message, error: (error as Error)?.message,
}) })
} }
return } else {
} try {
if (this.msgMap.has(name)) {
if (this.msgMap.has(name)) { this.msgMap.get(name)?.forEach(({ cb, ctx }) => cb.call(ctx, data))
this.msgMap.get(name)?.forEach(cb => cb(data)) }
} catch (error) {
console.log(error)
}
} }
} catch (error) { } catch (error) {
// console.log(`解析失败,${str}不是合法的JSON格式`, error) console.log(`解析失败不是合法的JSON格式`, error)
console.log(error)
} }
}) })
} }
listenMsg<T extends keyof IModel['msg']>(name: T, cb: (args: IModel['msg'][T]) => void) { listenMsg<T extends keyof IModel['msg']>(name: T, cb: (args: IModel['msg'][T]) => void, ctx: unknown) {
if (this.msgMap.has(name)) { if (this.msgMap.has(name)) {
this.msgMap.get(name)?.push(cb) this.msgMap.get(name)?.push({ cb, ctx })
} else { } else {
this.msgMap.set(name, [cb]) this.msgMap.set(name, [{ cb, ctx }])
} }
} }
unlistenMsg<T extends keyof IModel['msg']>(name: T, cb: (args: IModel['msg'][T]) => void) { unlistenMsg<T extends keyof IModel['msg']>(name: T, cb: (args: IModel['msg'][T]) => void, ctx: unknown) {
if (this.msgMap.has(name)) { if (this.msgMap.has(name)) {
const index = this.msgMap.get(name)?.indexOf(cb) || -1 const items = this.msgMap.get(name)
index > -1 && this.msgMap.get(name)?.splice(index, 1) const index = items.findIndex(i => cb === i.cb && i.ctx === ctx);
index > -1 && items.splice(index, 1)
} }
} }
@ -80,7 +88,7 @@ export class Connection extends EventEmitter {
}) })
const view = binaryEncode(name, data) const view = binaryEncode(name, data)
const buffer = Buffer.from(view.buffer) const buffer = Buffer.from(view.buffer)
console.log(`${getTime()}发送|字节数${buffer.length}|${this.playerId || -1}|${msg}`) console.log(`${getTime()}发送|字节数${buffer.length}|${this.playerId || -1}|内存${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB|${msg}`)
this.ws.send(buffer) this.ws.send(buffer)
} }
} }

View File

@ -2,7 +2,7 @@ import { Connection, MyServer, MyServerEventEnum } from './Core';
import PlayerManager from './Biz/PlayerManager'; import PlayerManager from './Biz/PlayerManager';
import RoomManager from './Biz/RoomManager'; import RoomManager from './Biz/RoomManager';
import { getTime, symlinkCommon } from './Utils'; import { getTime, symlinkCommon } from './Utils';
import { ApiMsgEnum, IApiGameStartReq, IApiGameStartRes, IApiPlayerJoinReq, IApiPlayerJoinRes, IApiPlayerListReq, IApiPlayerListRes, IApiRoomCreateReq, IApiRoomCreateRes, IApiRoomJoinReq, IApiRoomJoinRes, IApiRoomLeaveReq, IApiRoomLeaveRes, IApiRoomListReq, IApiRoomListRes, IModel } from './Common'; import { ApiMsgEnum, IApiGameEndReq, IApiGameEndRes, IApiGameStartReq, IApiGameStartRes, IApiPlayerJoinReq, IApiPlayerJoinRes, IApiPlayerListReq, IApiPlayerListRes, IApiRoomCreateReq, IApiRoomCreateRes, IApiRoomJoinReq, IApiRoomJoinRes, IApiRoomLeaveReq, IApiRoomLeaveRes, IApiRoomListReq, IApiRoomListRes, IModel } from './Common';
const server = new MyServer({ port: 8888 }) const server = new MyServer({ port: 8888 })
@ -31,7 +31,6 @@ server.setApi(ApiMsgEnum.ApiPlayerJoin, (connection: Connection, { nickname }: I
} }
}) })
server.setApi(ApiMsgEnum.ApiRoomList, (connection: Connection, data: IApiRoomListReq): IApiRoomListRes => { server.setApi(ApiMsgEnum.ApiRoomList, (connection: Connection, data: IApiRoomListReq): IApiRoomListRes => {
return { list: RoomManager.Instance.getRoomsView() } return { list: RoomManager.Instance.getRoomsView() }
}) })
@ -114,6 +113,28 @@ server.setApi(ApiMsgEnum.ApiGameStart, (connection: Connection, data: IApiGameSt
} }
}) })
server.setApi(ApiMsgEnum.ApiGameEnd, (connection: Connection, data: IApiGameEndReq): IApiGameEndRes => {
if (connection.playerId) {
const player = PlayerManager.Instance.getPlayerById(connection.playerId)
if (player) {
const rid = player.rid
if (rid) {
RoomManager.Instance.closeRoom(rid)
PlayerManager.Instance.syncPlayers()
RoomManager.Instance.syncRooms()
return {}
} else {
throw new Error("ApiGameEnd 玩家不在房间")
}
} else {
throw new Error("ApiGameEnd 玩家不存在")
}
} else {
throw new Error("ApiGameEnd 玩家未登录")
}
})
// start!! // start!!
server.start().then(() => { server.start().then(() => {
symlinkCommon() symlinkCommon()

View File

@ -72,7 +72,7 @@
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */ /* Type Checking */
"strict": true, /* Enable all strict type-checking options. */ // "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */

View File

@ -5,5 +5,10 @@
"version": "1.0.0", "version": "1.0.0",
"workspaces": [ "workspaces": [
"apps/*" "apps/*"
] ],
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"dev": "yarn workspace @game/server run dev"
}
} }

View File

@ -1,6 +0,0 @@
const ab = new ArrayBuffer(10)
const view = new DataView(ab)
view.setFloat32(0, 0.0012)
console.log(view.getFloat32(0));