export module JCMGO { /** * @name Socket客户端 * @description 包含了前后端所有的交互接口。(建议继承该类来使用) */ export class SocketClient { private _url: string = null; private _ws: WebSocket = null; /** * @name 连接服务端 * @param {string} url 服务端ws地址 */ public connect(url: string) { if (!url) throw "url can not be null"; this._url = url; this._ws = new WebSocket(url); this._ws.onclose = this.handleOnClose.bind(this); this._ws.onmessage = (e) => this.invoke(JSON.parse(e.data)); } /** * @name 重新连接 * @deprecated 可在onDestroy\onMiss时调用 */ public reconnect() { if (this._url != null && this._ws == null) { this.connect(this._url); } } /** * @name 关闭连接 */ public close() { if (this._ws != null) { this._ws.close(); this._ws = null; } } private _id: number = null; /**ID,由服务端发放 */ public get id() { return this._id; } private _isValid = false; /** 是否有效(可通信) */ public get isValid() { return this._isValid; } private _loaded = false; /** 是否已经载入过 */ public get loaded() { return this._loaded; } /**监听-载入 */ protected onLoad() {} /**监听-重载 */ protected onReload() {} /**监听-销毁(连上后断开) */ protected onDestroy() {} /**监听-迷失(请求连接但连接失败)*/ protected onMiss() {} /**调用远程函数 */ private call(func: string, ...args: any[]) { if (args == undefined) args = []; let data: SocketDataPack = {func: func, args: args}; this._ws.send(JSON.stringify(data)); } /**调用本地函数 */ private invoke(dataPack: SocketDataPack) { let theFunc = this[dataPack.func] as Function; if (theFunc instanceof Function) theFunc.apply(this, dataPack.args); else console.error(`invoke fail: can not find func [${dataPack.func}]`); } /**初始化函数-被服务端远程调用 */ private initByServer(id: number) { this._id = id; this._isValid = true; try { if (this._loaded) { this.onReload(); } else { this.onLoad(); } } catch (e) { console.error(e); } this._loaded = true; } /**监听处理-WebSocket关闭 */ private handleOnClose() { this._ws = null; try { if (this._isValid) { this._isValid = false; this.onDestroy(); } else { this.onMiss(); } } catch (e) { console.error(e); } } //========================API======================== /**单人随机匹配 */ public singlePersonRandomMatching(param: MatchParam) { this.call("SinglePersonRandomMatching", JSON.stringify(param)); } /**开始帧同步 */ public startFrameSync() { this.call("StartFrameSync"); } private _cachedInputs: object[] = []; /**缓存帧数据输入 */ public cacheInputs(...inputs: object[]) { this._cachedInputs.push(...inputs); } /**上传帧数据输入*/ public uploadInputs(sign: string = "") { if (this._cachedInputs.length > 0) { this.call("UploadInputs", this._cachedInputs, sign); this._cachedInputs = []; } } //========================Broadcast======================== /**当房间加入玩家 */ protected onRoomAddPlayer(e: BroadcastEvent) {} /**当房间移除玩家 */ protected onRoomRemovePlayer(e: BroadcastEvent) {} /**当房间结束匹配 */ protected onRoomEndMatching(e: BroadcastEvent) {} /**当帧同步开始 */ protected onFrameSyncStart(e: BroadcastEvent) {} /**当接收帧同步数据 */ protected onRecvFrame(e: BroadcastEvent) {} } //========================传输协议======================== /**匹配参数 */ export interface MatchParam { /**玩家基本信息 */ playerInfo: object; /**匹配房间配置 */ matchRoomConfig: MatchRoomConfig } /**匹配房间配置 */ export interface MatchRoomConfig { /**同一类型的房间才会匹配到一起 */ type: string; /**最多容纳多少人 */ maxMembers: number; /**服务端下发同步帧的频率 */ syncFrameRate: number; /**匹配多久结束 */ maxMatchingTime: number; } /**同步帧 */ export interface Frame { /**帧序列号 */ id: number; /**帧操作(房间内所有玩家上传的操作会合并在内) */ inputs: object[]; /**时间间隔(毫秒) */ dt: number; /**服务端返回的最新记号(需要在上传帧数据输入时,填入参数sign) */ sign: string; } /**房间信息 */ export interface RoomInfo { id: number; playerInfos: Map; } /**RPC协议 */ interface SocketDataPack { func: string; args: any[]; } //========================广播事件======================== /**房间结束匹配 */ export interface RoomEndMatchingBst { roomInfo: RoomInfo; timestamp: number; } /**房间加入玩家 */ export interface RoomAddPlayerBst { /**最新的房间信息 */ roomInfo: RoomInfo; playerIndex: number; isSelf: boolean; } /**房间移除玩家 */ export interface RoomRemovePlayerBst { /**最新的房间信息(目标已离开房间,所以里面没目标的信息) */ roomInfo: RoomInfo; playerIndex: number; isSelf: boolean; } /**广播事件 */ export interface BroadcastEvent { /**序列号 */ seq: number; /**数据 */ data: T; } //========================Object======================== /**对象处理工具 */ export class ObjectUtils { /** * 将sources合并到target,该合并全部是深拷贝 * @param target * @param sources * @returns {object} */ public static merge(target: object, ...sources: object[]): object { for (let i = 0; i < sources.length; ++i) { let source = sources[i]; if (typeof source != 'object' || source == null) { continue; } for (let skey in source) { //只处理自身的key 这里可能来自于外部prototype的扩展 if (!source.hasOwnProperty(skey)) { continue; } if (source[skey] instanceof Date) { //Date类型 要克隆一份 保证深拷贝 target[skey] = new Date(source[skey]); continue; } else if (typeof (target[skey]) == 'object' && target[skey] != null && typeof (source[skey]) == 'object' && source[skey] != null) { // 两个都是Object 递归merge之 this.merge(target[skey], source[skey]); } else { if (Array.isArray(source[skey])) { // 数组merge后还是数组 target[skey] = this.merge([], source[skey]); } else if (typeof (source[skey]) == 'object' && source[skey] !== null) { // Object要克隆一份以确保深拷贝 target[skey] = this.merge({}, source[skey]); } else { // 基本类型 直接赋值即可 target[skey] = source[skey]; } } } } return target; } public static values(obj: object): any[] { let output = []; for (let k in obj) { obj.hasOwnProperty(k) && output.push(obj[k]); } return output; } public static entries(obj: object): any[][] { let output = []; for (let key in obj) { if (!obj.hasOwnProperty(key)) { continue; } output.push([key, obj[key]]); } return output; } public static forEach(obj: object, handler: (value: any, key: string, obj: object) => void): void { for (let key in obj) { if (!obj.hasOwnProperty(key)) { return; } handler(obj[key], key, obj); } } } //========================Math======================== /** * 多平台一致精确计算 * [Math.round、Math.min、Math.max,Math.floor、Math.ceil,这些系统方法一般情况下是可以放心使用的] */ export class ExactMath { // 计算精度 // sin、cos、tan方法的误差小数点后16位 public static readonly ACCURACY_SIN_ERROR = 1e-16; public static readonly ACCURACY_COS_ERROR = 1e-16; public static readonly ACCURACY_TAN_ERROR = 1e-16; // 角度弧度常量 public static readonly DEG = 57.29577951308232; public static readonly RAD = 0.017453292519943295; public static readonly PI = 3.141592653589793; public static readonly E = 2.718281828459045; public static readonly LN2 = 0.6931471805599453; public static readonly LN10 = 2.302585092994046; public static readonly LOG2E = 1.4426950408889634; public static readonly LOG10E = 0.4342944819032518; public static readonly SQRT1_2 = 0.7071067811865476; public static readonly SQRT2 = 1.4142135623730951; /****************************************************基础****************************************************/ /** * 获得小数位数 * @param {Number} num 浮点数 * @returns {Number} */ public static getDecimalPlace(num: number) { if (num && num !== Math.floor(num)) { for (let n = 1, m = 10, temp = 0; n < 20; n += 1, m *= 10) { temp = num * m; if (temp == Math.floor(temp)) return n; } return 20; } else { return 0; } } /** * 保留n为小数,并四舍五入 * @param {Number} num 浮点数 * @param {Number} n 整数 * @returns {Number} */ public static toFixed(num: number, n: number = 0) { if (n == 0) { return Math.round(num); } else { const m = Math.pow(10, n); return Math.round(num * (m * 10) / 10) / m; } } public static abs(x: number) { return Math.abs(x); } public static round(x: number) { return Math.round(x); } public static ceil(x: number) { return Math.ceil(x) } public static floor(x: number) { return Math.floor(x) } public static min(...args: number[]) { return Math.min(...args); } public static max(...args: number[]) { return Math.max(...args); } /** * 小数相加 * @param {Number} num1 浮点数 * @param {Number} num2 浮点数 * @returns {Number} */ public static add(...args: number[]) { if (args.length === 2) { const num1 = args[0]; const num2 = args[1]; const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2))); return (this.toFixed(num1 * m) + this.toFixed(num2 * m)) / m; } else { return args.reduce((a, b) => this.add(a, b)) } }; /** * 小数相减 * @param {Number} num1 浮点数 * @param {Number} num2 浮点数 * @returns {Number} */ public static sub(...args: number[]) { if (args.length === 2) { const num1 = args[0]; const num2 = args[1]; const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2))); return (this.toFixed(num1 * m) - this.toFixed(num2 * m)) / m; } else { return args.reduce((a, b) => this.sub(a, b)) } }; /** * 小数相乘 * @param {Number} num1 浮点数 * @param {Number} num2 浮点数 * @returns {Number} */ public static mul(...args: number[]) { if (args.length === 2) { let num1 = args[0]; let num2 = args[1]; // 方案1: // 直接相乘,但是相乘两数小数点过多会导致中间值[(n1 * m1) * (n2 * m2)]过大 // const n1 = this.getDecimalPlace(num1); // const n2 = this.getDecimalPlace(num2); // const m1 = Math.pow(10, n1); // const m2 = Math.pow(10, n2); // return (n1 * m1) * (n2 * m2) / (m1 * m2); // 方案2: // 用除法实现乘法,不会存在过大中间值 let n1 = this.getDecimalPlace(num1); let n2 = this.getDecimalPlace(num2); let m = Math.pow(10, n2); num2 = m / this.toFixed(num2 * m); m = Math.pow(10, Math.max(n1, this.getDecimalPlace(num2))); m = this.toFixed(num1 * m) / this.toFixed(num2 * m); let n = Math.min(this.getDecimalPlace(m), n1 + n2); return this.toFixed(m, n); } else { return args.reduce((a, b) => this.mul(a, b)) } }; /** * 小数相除法 * @param {Number} num1 浮点数 * @param {Number} num2 浮点数 * @returns {Number} */ public static div(...args: number[]) { if (args.length === 2) { const num1 = args[0]; const num2 = args[1]; const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2))); return this.toFixed(num1 * m) / this.toFixed(num2 * m); } else { return args.reduce((a, b) => this.div(a, b)) } }; /** * 取余 * @param {Number} num1 浮点数 * @param {Number} num2 浮点数 * @returns {Number} */ public static rem(...args: number[]) { if (args.length === 2) { const num1 = args[0]; const num2 = args[1]; const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2))); return this.toFixed(num1 * m) % this.toFixed(num2 * m) / m; } else { return args.reduce((a, b) => this.rem(a, b)) } }; /** * n次方,仅支持整数次方(正负都可以) * @param {Number} num 浮点数 * @param {Number} n 整数 */ public static pow(num: number, n: number) { if (num == 0 && n == 0) { return 1; } if (num == 0 && n > 0) { return 0 } if (num == 0 && n < 0) { return Infinity; } // num为负数,n为负小数,返回NaN if (num < 0 && n < 0 && Math.round(n) != n) { return NaN; } if (Math.round(n) != n) { throw new Error('n must be an integer'); } let result = 1; if (n > 0) { for (let index = 0; index < n; index++) { result = this.mul(result, num); } } else if (n < 0) { for (let index = 0, len = Math.abs(n); index < len; index++) { result = this.div(result, num); } } return result; }; /** * 开方运算【牛顿迭代法】 * * @param {Number} n * @returns */ public static sqrt(n: number) { if (n < 0) return NaN; if (n === 0) return 0; if (n === 1) return 1; let last = 0; let res = 1; let c = 50; while (res != last && --c >= 0) { last = res; res = this.div(this.add(res, this.div(n, res)), 2) } return res; // float InvSqrt(float x) // { // float xhalf = 0.5f * x; // int i = * (int *) & x; // get bits for floating VALUE // i = 0x5f375a86 - (i >> 1); // gives initial guess y0 // x = * (float *) & i; // convert bits BACK to float // x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy // x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy // x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy // return 1 / x; // } }; /****************************************************随机****************************************************/ private static getSeed(seed: number) { if (typeof seed !== "number" || isNaN(seed)) throw "seed invalid"; seed = Math.floor(seed % 233280); return seed; } private static randomSeed: number; /** * 设置随机种子 */ public static setSeed(seed: number) { this.randomSeed = this.getSeed(seed); }; /** * 随机 */ public static random() { if (typeof this.randomSeed !== "number" || isNaN(this.randomSeed)) throw "seed invalid"; this.randomSeed = (this.randomSeed * 9301 + 49297) % 233280; return this.randomSeed / 233280.0; }; /** * 根据随机种子随机 * @param {number} seed */ public static randomBySeed(seed: number) { seed = this.getSeed(seed); seed = (seed * 9301 + 49297) % 233280; return seed / 233280.0; }; /****************************************************角度弧度转换****************************************************/ /** * 弧度数转角度数 * @param {Number} radians 浮点数 * @returns {Numbe} 浮点数 */ public static radiansToDegrees(radians: number) { return this.div(radians, this.RAD); }; /** * 角度数转弧度数 * @param {Number} degrees 浮点数 * @returns {Numbe} 浮点数 */ public static degreesToRadians(degrees: number) { return this.div(degrees, this.DEG); }; /** * 将角度值转换到[0, 360)范围内 * @param {Number} angle 浮点数 * @returns {Number} 整数 */ public static get0To360Angle(angle: number) { if (angle === 0) { return 0; } else if (angle < 0) { return this.add(this.rem(angle, 360), 360); } else { return this.rem(angle, 360); } }; /****************************************************三角函数****************************************************/ /** * 查表 */ private static _sin = {}; private static _cos = {}; private static _tan = {}; /** * 3个三角函数,根据需求自行添加 * 为了效率,应该尽量使用查表法 * 表内查不到的,目前使用系统方法的结果并取前4位小数 */ public static sin(x: number) { // if (x == 0) { // return 0; // } else if (x == 90) { // return 1; // } // let n = x, sum = 0, i = 1; // do { // i++; // sum = this.add(sum, n); // // n = -n * x * x / (2 * i - 1) / (2 * i - 2); // n = this.div(this.mul(-1, n, x, x), this.sub(this.mul(2, i), 1), this.sub(this.mul(2, i), 2)); // } while (Math.abs(n) >= ACCURACY_SIN_ERROR); // return sum; if (this._sin.hasOwnProperty(x)) { return this._sin[x]; } return this.toFixed(Math.sin(x), 4); }; public static cos(x: number) { if (this._cos.hasOwnProperty(x)) { return this._cos[x]; } return this.toFixed(Math.cos(x), 4); }; public static tan(x: number) { if (this._tan.hasOwnProperty(x)) { return this._tan[x]; } return this.toFixed(Math.tan(x), 4); }; } }