UNPKG

incheon

Version:

A Node.js based real-time game server

257 lines (220 loc) 9.35 kB
'use strict'; const TwoVector = require('./TwoVector'); const GameObject = require('./GameObject'); const Serializer = require('./Serializer'); const MathUtils = require('../lib/MathUtils'); /** * DynamicObject is the base class of the game's objects, for games which * rely on SimplePhysicsEngine. It defines the * base object which can move around in the game world. The * extensions of this object (the subclasses) * will be periodically synchronized from the server to every client. * * The dynamic objects have pseudo-physical properties, which * allow the client to extrapolate the position * of dynamic objects in-between server updates. */ class DynamicObject extends GameObject { /** * The netScheme is a dictionary of attributes in this game * object. The attributes listed in the netScheme are those exact * attributes which will be serialized and sent from the server * to each client on every server update. * The netScheme member is implemented as a getter. * * You may choose not to implement this method, in which * case your object only transmits the default attributes * which are already part of {@link DynamicObject}. * But if you choose to add more attributes, make sure * the return value includes the netScheme of the super class. * * @memberof DynamicObject * @member {Object} netScheme * @example * static get netScheme() { * return Object.assign({ * mojo: { type: Serializer.TYPES.UINT8 }, * }, super.netScheme); * } */ static get netScheme() { return Object.assign({ playerId: { type: Serializer.TYPES.INT16 }, position: { type: Serializer.TYPES.CLASSINSTANCE }, velocity: { type: Serializer.TYPES.CLASSINSTANCE }, angle: { type: Serializer.TYPES.FLOAT32 } }, super.netScheme); } /** * Creates an instance of a dynamic object. * Override to provide starting values for position, velocity, etc. * The object ID should be the next value provided by `world.idCount` * @param {String} id - the object id * @param {TwoVector} position - position vector * @param {TwoVector} velocity - velocity vector * @example * // Ship is a subclass of DynamicObject: * Ship(++this.world.idCount); */ constructor(id, position, velocity) { super(id); /** * ID of player who created this object * @member {Number} */ this.playerId = 0; this.position = new TwoVector(0, 0); this.velocity = new TwoVector(0, 0); /** * position * @member {TwoVector} */ if (position) this.position.copy(position); /** * velocity * @member {TwoVector} */ if (velocity) this.velocity.copy(velocity); /** * object orientation angle in degrees * @member {Number} */ this.angle = 90; /** * should rotate left by {@link DynamicObject#rotationSpeed} on next step * @member {Boolean} */ this.isRotatingLeft = false; /** * should rotate right by {@link DynamicObject#rotationSpeed} on next step * @member {Boolean} */ this.isRotatingRight = false; /** * should accelerate by {@link DynamicObject#acceleration} on next step * @member {Boolean} */ this.isAccelerating = false; /** * angle rotation per step * @member {Number} */ this.rotationSpeed = 2.5; /** * acceleration per step * @member {Number} */ this.acceleration = 0.1; this.bending = new TwoVector(0, 0); this.bendingAngle = 0; this.deceleration = 0.99; } // convenience getters get x() { return this.position.x; } get y() { return this.position.y; } /** * Formatted textual description of the dynamic object. * The output of this method is used to describe each instance in the traces, * which significantly helps in debugging. * * @return {String} description - a string describing the DynamicObject */ toString() { function round3(x) { return Math.round(x * 1000) / 1000; } return `dObj[${this.id}] player${this.playerId} Pos=${this.position} Vel=${this.velocity} angle${round3(this.angle)}`; } /** * Formatted textual description of the game object's current bending properties. * @return {String} description - a string description */ bendingToString() { if (this.bendingIncrements) return `bend=${this.bending} angle=${this.bendingAngle} num_increments=${this.bendingIncrements}`; return 'no bending'; } /** * The maximum velocity allowed. If returns null then ignored. * @memberof DynamicObject * @member {Number} maxSpeed */ get maxSpeed() { return null; } syncTo(other) { this.id = other.id; this.playerId = other.playerId; this.position.copy(other.position); this.velocity.copy(other.velocity); this.bending.copy(other.bending); this.bendingAngle = other.bendingAngle; this.angle = other.angle; this.rotationSpeed = other.rotationSpeed; this.acceleration = other.acceleration; this.deceleration = other.deceleration; } bendToCurrent(original, bending, worldSettings, isLocal, bendingIncrements) { // TODO: the bending parameters should now be an object, // with a single getter bendingMultiples which has local // and remote values for position, velocity, and angle this.bendingIncrements = bendingIncrements; // if the object has defined a bending multiples for this object, use them if (typeof this.bendingMultiple === 'number') bending = this.bendingMultiple; // velocity bending factor let velocityBending = bending; if (typeof this.bendingVelocityMultiple === 'number') velocityBending = this.bendingVelocityMultiple; // angle bending factor let angleBending = bending; if (typeof this.bendingAngleMultiple === 'number') angleBending = this.bendingAngleMultiple; if (isLocal && (typeof this.bendingAngleLocalMultiple === 'number')) angleBending = this.bendingAngleLocalMultiple; // bend to position, velocity, and angle gradually // TODO: consider using lerp() method of TwoVector instead. // you will need implement lerpWrapped() first. if (worldSettings.worldWrap) { this.bending.x = MathUtils.interpolateDeltaWithWrapping(original.position.x, this.position.x, bending, 0, worldSettings.width) / bendingIncrements; this.bending.y = MathUtils.interpolateDeltaWithWrapping(original.position.y, this.position.y, bending, 0, worldSettings.height) / bendingIncrements; } else { this.bending.x = MathUtils.interpolateDelta(original.position.x, this.position.x, bending) / bendingIncrements; this.bending.y = MathUtils.interpolateDelta(original.position.y, this.position.y, bending) / bendingIncrements; } this.bendingAngle = MathUtils.interpolateDeltaWithWrapping(original.angle, this.angle, angleBending, 0, 360) / bendingIncrements; this.velocity.x = MathUtils.interpolate(original.velocity.x, this.velocity.x, velocityBending); this.velocity.y = MathUtils.interpolate(original.velocity.y, this.velocity.y, velocityBending); // revert to original this.position.copy(original.position); this.angle = original.angle; } applyIncrementalBending() { if (this.bendingIncrements === 0) return; this.position.add(this.bending); this.angle += this.bendingAngle; this.bendingIncrements--; } interpolate(nextObj, playPercentage, worldSettings) { let px = this.position.x; let py = this.position.y; let angle = this.angle; // TODO allow netscheme to designate interpolatable attribute (power, shield, etc) // first copy all the assignable attributes for (let k of Object.keys(this.constructor.netScheme)) { let val = nextObj[k]; if (Serializer.typeCanAssign(this.constructor.netScheme[k].type)) this[k] = val; else if (typeof val.clone === 'function') this[k] = val.clone(); } // update other objects with interpolation // TODO interpolate using TwoVector methods, including wrap-around function calcInterpolate(start, end, wrap, p) { if (Math.abs(end - start) > wrap / 2) return end; return (end - start) * p + start; } this.position.x = calcInterpolate(px, nextObj.position.x, worldSettings.width, playPercentage); this.position.y = calcInterpolate(py, nextObj.position.y, worldSettings.height, playPercentage); var shortestAngle = ((((nextObj.angle - angle) % 360) + 540) % 360) - 180; this.angle = angle + shortestAngle * playPercentage; } } module.exports = DynamicObject;