From: Rubén Beltrán del Río Date: Tue, 29 May 2018 02:35:38 +0000 (-0500) Subject: Add Grab System (#9) X-Git-Url: https://git.r.bdr.sh/rbdr/sumo/commitdiff_plain/1676911c8a2621274bf75ff7271faa926cf58d6c?ds=sidebyside Add Grab System (#9) * Remove unused code * Adds grab components * Adds grab nodes * Add sensors to the physics world * Add grab systems * Add new sprites * Attach grab components * Add grab system to engine * Update changelog --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 83de4a7..c038284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,5 +18,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Matter-js powered physics system - Scripts to setup hooks, linting, docs, and bundling - Keyboard Support +- Dash & grab mechanics [Unreleased]: https://github.com/rbdr/sumo/compare/master...develop diff --git a/lib/components/grab.js b/lib/components/grab.js new file mode 100644 index 0000000..ab808a8 --- /dev/null +++ b/lib/components/grab.js @@ -0,0 +1,60 @@ +import { Component } from '@serpentity/serpentity'; + +/** + * Component that stores a dash skill + * + * @extends {external:Serpentity.Component} + * @class GrabComponent + * @param {object} config a configuration object to extend. + */ +export default class GrabComponent extends Component { + constructor(config) { + + super(config); + + /** + * The properthy that holds the grab state + * + * @property {boolean} grabbing + * @instance + * @memberof GrabComponent + */ + this.dashing = this.grabbing || false; + + /** + * The constraint used for the grab + * + * @property {external:MatterJs.Constraint} constraint + * @instance + * @memberof GrabComponent + */ + this.constraint = this.constraint || null; + + /** + * Whether the grab is locked from occuring + * + * @property {boolean} locked + * @instance + * @memberof GrabComponent + */ + this.locked = this.locked || false; + + /** + * Cooldown before lock is removed + * + * @property {number} cooldown + * @instance + * @memberof GrabComponent + */ + this.cooldown = this.cooldown || 3000; + + /** + * Current cooldown + * + * @property {number} currentCooldown + * @instance + * @memberof GrabComponent + */ + this.currentCooldown = 0; + } +}; diff --git a/lib/components/grab_area.js b/lib/components/grab_area.js new file mode 100644 index 0000000..3966c83 --- /dev/null +++ b/lib/components/grab_area.js @@ -0,0 +1,34 @@ +import { Component } from '@serpentity/serpentity'; + +/** + * Component that stores an area used to grab things + * + * @extends {external:Serpentity.Component} + * @class GrabAreaComponent + * @param {object} config a configuration object to extend. + */ +export default class GrabAreaComponent extends Component { + constructor(config) { + + super(config); + + /** + * The properthy that holds the angle. Assumes the area behaves as a sensor + * + * @property {external:MatterJs.Body} area + * @instance + * @memberof GrabAreaComponent + */ + this.area = this.area || null; + + /** + * The constraint that ties this area to its parent + * + * @property {external:MatterJs.Constraint} constraint + * @instance + * @memberof GrabAreaComponent + */ + this.constraint = this.constraint || null; + } +}; + diff --git a/lib/components/grabbable.js b/lib/components/grabbable.js new file mode 100644 index 0000000..223b95c --- /dev/null +++ b/lib/components/grabbable.js @@ -0,0 +1,11 @@ +import { Component } from '@serpentity/serpentity'; + +/** + * Component that stores grab status + * + * @extends {external:Serpentity.Component} + * @class GrabbableComponent + * @param {object} config a configuration object to extend. + */ +export default class GrabbableComponent extends Component {}; + diff --git a/lib/factories/pixi.js b/lib/factories/pixi.js index 786639f..f941ffe 100644 --- a/lib/factories/pixi.js +++ b/lib/factories/pixi.js @@ -68,6 +68,9 @@ export default { body.addChild(this.createBlush({ radius })); + body.addChild(this.createEffortMark({ radius })); + body.addChild(this.createShadow({ radius })); + // The group body.addChild(smile); body.addChild(frown); @@ -203,5 +206,108 @@ export default { blush.addChild(rightBlush); return blush; + }, + + /** + * Creates an effort mark + * + * @function createEffortMark + * @memberof PixiFactory + * @return {external:CreateJs.Container} the created container + */ + createEffortMark(config) { + + const radius = config.radius; + + const effortMark = new Graphics(); + + const centerX = -3 * radius / 4; + const centerY = -3 * radius / 4; + + effortMark.name = 'effort'; + effortMark.visible = false; + + const color = 0xff00ff; + const lineWidth = 2; + + const topRightArch = new Graphics(); + topRightArch.lineStyle(lineWidth, color, 1) + .arc( + centerX + 2 * radius / 7, centerY - 2 * radius / 7, + radius / 5, + Math.PI / 2, + Math.PI + ); + + const topLeftArch = new Graphics(); + topLeftArch.lineStyle(lineWidth, color, 1) + .arc( + centerX - 2 * radius / 7, centerY - radius / 4, + radius / 5, + 0, + Math.PI / 2 + ); + + const bottomRightArch = new Graphics(); + bottomRightArch.lineStyle(lineWidth, color, 1) + .arc( + centerX + 2 * radius / 7, centerY + radius / 4, + radius / 5, + Math.PI, + 3 * Math.PI / 2 + ); + + const bottomLeftArch = new Graphics(); + bottomLeftArch.lineStyle(lineWidth, color, 1) + .arc( + centerX - 2 * radius / 7, centerY + 2 * radius / 7, + radius / 5, + 3 * Math.PI / 2, + 0 + ); + + effortMark.addChild(topRightArch); + effortMark.addChild(topLeftArch); + effortMark.addChild(bottomRightArch); + effortMark.addChild(bottomLeftArch); + + return effortMark; + }, + + /** + * Creates a shadow for the eye + * + * @function createShadow + * @memberof PixiFactory + * @return {external:CreateJs.Container} the created container + */ + createShadow(config) { + + const radius = config.radius; + + const shadow = new Graphics(); + + const centerX = radius / 2; + const centerY = -3 * radius / 4; + + shadow.name = 'shadow'; + shadow.visible = false; + + const color = 0x9900ff; + const lineWidth = 2; + + shadow.lineStyle(lineWidth, color, 1) + .moveTo(centerX - radius / 4, centerY - radius / 3) + .lineTo(centerX - radius / 4, centerY + radius / 5) + .moveTo(centerX - radius / 8, centerY - radius / 4) + .lineTo(centerX - radius / 8, centerY + radius / 5) + .moveTo(centerX, centerY - radius / 5) + .lineTo(centerX, centerY + radius / 5) + .moveTo(centerX + radius / 8, centerY - radius / 6) + .lineTo(centerX + radius / 8, centerY + radius / 6) + .moveTo(centerX + radius / 4, centerY - radius / 5) + .lineTo(centerX + radius / 4, centerY + radius / 5); + + return shadow; } }; diff --git a/lib/factories/sumo.js b/lib/factories/sumo.js index e8417f9..7b50f8a 100644 --- a/lib/factories/sumo.js +++ b/lib/factories/sumo.js @@ -10,6 +10,9 @@ import CoupledEntitiesComponent from '../components/coupled_entities'; import DashComponent from '../components/dash'; import ElasticComponent from '../components/elastic'; import ForceComponent from '../components/force'; +import GrabAreaComponent from '../components/grab_area'; +import GrabbableComponent from '../components/grabbable'; +import GrabComponent from '../components/grab'; import MaxVelocityComponent from '../components/max_velocity'; import PositionComponent from '@serpentity/components.position'; import PixiContainerComponent from '../components/pixi_container'; @@ -96,6 +99,18 @@ export default { }); entity.addComponent(new BodyComponent({ body })); + // GRAB + + const areaSizeFactor = 2; // Multiplier vs the radius + const area = Bodies.circle(position.x / Config.meterSize, position.y / Config.meterSize, (radius * areaSizeFactor) / Config.meterSize, { + label: 'Sumo Grab Area', + isSensor: true + }); + + entity.addComponent(new GrabAreaComponent({ area })); + entity.addComponent(new GrabComponent({ body })); + entity.addComponent(new GrabbableComponent({ body })); + if (engine) { engine.addEntity(entity); } @@ -235,6 +250,16 @@ export default { component: DashComponent, property: 'dashing' } + }, + { + source: { + type: 'keyboard', + index: 88 // X + }, + target: { + component: GrabComponent, + property: 'grabbing' + } } ] })); diff --git a/lib/nodes/drawn_grabber.js b/lib/nodes/drawn_grabber.js new file mode 100644 index 0000000..49af6d4 --- /dev/null +++ b/lib/nodes/drawn_grabber.js @@ -0,0 +1,27 @@ +import { Node } from '@serpentity/serpentity'; + +import GrabComponent from '../components/grab'; +import PixiContainerComponent from '../components/pixi_container'; + +/** + * Node identifying an entity that can grab and is drawn + * + * @extends {external:Serpentity.Node} + * @class DrawnGrabberNode + */ +export default class DrawnGrabberNode extends Node { + +}; + +/** + * Holds the types that are used to identify a drawn grabber entity + * + * @property {object} types + * @name types + * @memberof DrawnGrabberNode + */ +DrawnGrabberNode.types = { + container: PixiContainerComponent, + grab: GrabComponent +}; + diff --git a/lib/nodes/grab_area.js b/lib/nodes/grab_area.js new file mode 100644 index 0000000..9f6c3e0 --- /dev/null +++ b/lib/nodes/grab_area.js @@ -0,0 +1,25 @@ +import { Node } from '@serpentity/serpentity'; + +import GrabAreaComponent from '../components/grab_area'; + +/** + * Node identifying an entity that have a physical grab area + * + * @extends {external:Serpentity.Node} + * @class GrabAreaNode + */ +export default class GrabAreaNode extends Node { + +}; + +/** + * Holds the types that are used to identify an entity with a grab + * area + * + * @property {object} types + * @name types + * @memberof GrabAreaNode + */ +GrabAreaNode.types = { + grabArea: GrabAreaComponent +}; diff --git a/lib/nodes/grabbable.js b/lib/nodes/grabbable.js new file mode 100644 index 0000000..4711764 --- /dev/null +++ b/lib/nodes/grabbable.js @@ -0,0 +1,27 @@ +import { Node } from '@serpentity/serpentity'; + +import BodyComponent from '../components/body'; +import GrabbableComponent from '../components/grabbable'; + +/** + * Node identifying an entity that can grab another + * + * @extends {external:Serpentity.Node} + * @class GrabbableNode + */ +export default class GrabbableNode extends Node { + +}; + +/** + * Holds the types that are used to identify a grabbable entity + * + * @property {object} types + * @name types + * @memberof GrabbableNode + */ +GrabbableNode.types = { + body: BodyComponent, + grabbable: GrabbableComponent +}; + diff --git a/lib/nodes/grabber.js b/lib/nodes/grabber.js new file mode 100644 index 0000000..db61365 --- /dev/null +++ b/lib/nodes/grabber.js @@ -0,0 +1,29 @@ +import { Node } from '@serpentity/serpentity'; + +import BodyComponent from '../components/body'; +import GrabAreaComponent from '../components/grab_area'; +import GrabComponent from '../components/grab'; + +/** + * Node identifying an entity that can grab another + * + * @extends {external:Serpentity.Node} + * @class GrabberNode + */ +export default class GrabberNode extends Node { + +}; + +/** + * Holds the types that are used to identify a grabber entity + * + * @property {object} types + * @name types + * @memberof GrabberNode + */ +GrabberNode.types = { + body: BodyComponent, + grabArea: GrabAreaComponent, + grab: GrabComponent +}; + diff --git a/lib/sumo.js b/lib/sumo.js index 4785087..d39c055 100644 --- a/lib/sumo.js +++ b/lib/sumo.js @@ -9,7 +9,9 @@ import CreateCouplingLineSystem from './systems/create_coupling_line'; import ControlMapperSystem from './systems/control_mapper'; import DashSystem from './systems/dash'; import DrawDashSystem from './systems/draw_dash'; +import DrawGrabSystem from './systems/draw_grab'; import ElasticSystem from './systems/elastic'; +import GrabSystem from './systems/grab'; import PhysicsWorldControlSystem from './systems/physics_world_control'; import PhysicsToAttributesSystem from './systems/physics_to_attributes'; import RenderSystem from './systems/render'; @@ -197,6 +199,10 @@ internals.Sumo = class Sumo { this._engine.addSystem(new DashSystem()); + this._engine.addSystem(new GrabSystem({ + engine: this._matterJs + })); + this._engine.addSystem(new ApplyForceSystem()); this._engine.addSystem(new PhysicsWorldControlSystem({ @@ -213,6 +219,8 @@ internals.Sumo = class Sumo { this._engine.addSystem(new DrawDashSystem()); + this._engine.addSystem(new DrawGrabSystem()); + this._engine.addSystem(new RenderSystem({ application: this._pixi })); diff --git a/lib/systems/draw_dash.js b/lib/systems/draw_dash.js index 118ce1c..5ce9043 100644 --- a/lib/systems/draw_dash.js +++ b/lib/systems/draw_dash.js @@ -2,10 +2,6 @@ import { System } from '@serpentity/serpentity'; import DrawnDasherNode from '../nodes/drawn_dasher'; -const internals = { - kBlushRadius: 25 -}; - /** * Shows a different graphic during the duration of lock * diff --git a/lib/systems/draw_grab.js b/lib/systems/draw_grab.js new file mode 100644 index 0000000..21ffdcd --- /dev/null +++ b/lib/systems/draw_grab.js @@ -0,0 +1,115 @@ +import { System } from '@serpentity/serpentity'; + +import DrawnGrabberNode from '../nodes/drawn_grabber'; + +/** + * Shows a different graphic during the duration of lock + * + * @extends {external:Serpentity.System} + * @class DrawGrabSystem + * @param {object} config a configuration object to extend. + */ +export default class DrawGrabSystem extends System { + + constructor(config = {}) { + + super(); + + /** + * The node collection of grabbers + * + * @property {external:Serpentity.NodeCollection} drawnGrabbers + * @instance + * @memberof DrawGrabSystem + */ + this.drawnGrabbers = null; + } + + /** + * Initializes system when added. Requests drawn grabber nodes + * + * @function added + * @memberof DrawGrabSystem + * @instance + * @param {external:Serpentity.Engine} engine the serpentity engine to + * which we are getting added + */ + added(engine) { + + this.drawnGrabbers = engine.getNodes(DrawnGrabberNode); + } + + /** + * Clears system resources when removed. + * + * @function removed + * @instance + * @memberof DrawGrabSystem + */ + removed() { + + this.drawnGrabbers = null; + } + + /** + * Runs on every update of the loop. Updates image depending on if + * grab is locked and active + * + * @function update + * @instance + * @param {Number} currentFrameDuration the duration of the current + * frame + * @memberof DrawGrabSystem + */ + update(currentFrameDuration) { + + for (const drawnGrabber of this.drawnGrabbers) { + + const grab = drawnGrabber.grab; + const container = drawnGrabber.container.container; + + if (grab.locked && grab.constraint) { + this._drawGrabFace(container); + continue; + } + + if (grab.locked) { + this._drawGrabCooldownFace(container); + continue; + } + + this._removeGrabFace(container); + } + } + + // Draws the grab face + + _drawGrabFace(container) { + + const effort = container.getChildByName('effort'); + const shadow = container.getChildByName('shadow'); + effort.visible = true; + shadow.visible = false; + } + + // Draws the grab cooldown face + + _drawGrabCooldownFace(container) { + + const effort = container.getChildByName('effort'); + const shadow = container.getChildByName('shadow'); + effort.visible = false; + shadow.visible = true; + } + + // Removes the dash face + + _removeGrabFace(container) { + + const effort = container.getChildByName('effort'); + const shadow = container.getChildByName('shadow'); + effort.visible = false; + shadow.visible = false; + } +}; + diff --git a/lib/systems/grab.js b/lib/systems/grab.js new file mode 100644 index 0000000..43e6246 --- /dev/null +++ b/lib/systems/grab.js @@ -0,0 +1,183 @@ +import { System } from '@serpentity/serpentity'; +import { Body, Constraint, SAT, World } from 'matter-js'; + +import GrabberNode from '../nodes/grabber'; +import GrabbableNode from '../nodes/grabbable'; + +import Config from '../config'; + +const internals = { + kGrabRadius: 50, + kNoEngine: 'No matter js physics engine found. Make sure you set the `engine` key in the config object when initializing.' +}; + +/** + * Handles grabbing between entities + * + * @extends {external:Serpentity.System} + * @class GrabSystem + * @param {object} config a configuration object to extend. + */ +export default class GrabSystem extends System { + + constructor(config = {}) { + + super(); + + /** + * The node collection of grabbers + * + * @property {external:Serpentity.NodeCollection} grabbers + * @instance + * @memberof GrabSystem + */ + this.grabbers = null; + + /** + * The node collection of grabbables + * + * @property {external:Serpentity.NodeCollection} grabbables + * @instance + * @memberof GrabSystem + */ + this.grabbables = null; + + /** + * The matter-js engine we will use to add and remove constraints + * + * @property {external:MatterJs.Engine} engine + * @instance + * @memberof GrabSystem + */ + this.engine = config.engine; + + if (!this.engine) { + throw new Error(internals.kNoEngine); + } + } + + /** + * Initializes system when added. Requests grabber and grabbable nodes + * + * @function added + * @memberof GrabSystem + * @instance + * @param {external:Serpentity.Engine} engine the serpentity engine to + * which we are getting added + */ + added(engine) { + + this.grabbers = engine.getNodes(GrabberNode); + this.grabbables = engine.getNodes(GrabbableNode); + } + + /** + * Clears system resources when removed. + * + * @function removed + * @instance + * @memberof GrabSystem + */ + removed() { + + this.grabbers = null; + this.grabbables = null; + } + + /** + * Runs on every update of the loop. Triggers grab and manages cooldown + * + * @function update + * @instance + * @param {Number} currentFrameDuration the duration of the current + * frame + * @memberof GrabSystem + */ + update(currentFrameDuration) { + + for (const grabber of this.grabbers) { + + const grab = grabber.grab; + + if (grab.grabbing && !grab.locked) { + this._grab(grabber); + } + + const isGrabReleased = !grab.grabbing || grab.currentCooldown >= grab.cooldown; + if (grab.constraint && isGrabReleased) { + this._release(grabber); + } + + if (!grab.grabbing && grab.locked && grab.currentCooldown >= grab.cooldown) { + this._unlock(grabber); + } + + if (grab.locked) { + grab.currentCooldown += currentFrameDuration; + } + + grab.grabbing = 0; + } + } + + // Executes the dash action + + _grab(grabber) { + + const grab = grabber.grab; + + grab.locked = true; + grab.currentCooldown = 0; + + Body.setPosition(grabber.grabArea.area, grabber.body.body.position); + + console.log('Grab!'); + + for (const grabbable of this.grabbables) { + + if (grabbable.entity === grabber.entity) { + continue; + } + + const collision = SAT.collides(grabber.grabArea.area, grabbable.body.body); + if (collision.collided) { + grab.constraint = this._createConstraint(grabber.body.body, grabbable.body.body); + console.log('Grabbing', grab.constraint); + } + } + } + + // Executes the unlock action + + _unlock(grabber) { + + grabber.grab.locked = false; + } + + // Releases a constraint + + _release(grabber) { + + console.log('Releasing', grabber.grab.constraint); + World.remove(this.engine.world, grabber.grab.constraint); + grabber.grab.currentCooldown = 0; + grabber.grab.constraint = null; + } + + // Performs a grab between two entities + + _createConstraint(grabber, grabbable) { + + const constraint = Constraint.create({ // Attach the sensor to the body + bodyA: grabber, + bodyB: grabbable, + damping: 0, + length: internals.kGrabRadius / Config.meterSize, + stiffness: 1 + }); + + World.add(this.engine.world, [constraint]); + + return constraint; + } +}; diff --git a/lib/systems/physics_world_control.js b/lib/systems/physics_world_control.js index a658d5c..370701e 100644 --- a/lib/systems/physics_world_control.js +++ b/lib/systems/physics_world_control.js @@ -2,6 +2,7 @@ import { System } from '@serpentity/serpentity'; import { Engine, World } from 'matter-js'; import PhysicalNode from '../nodes/physical'; +import GrabAreaNode from '../nodes/grab_area'; const internals = { kNoEngine: 'No matter js physics engine found. Make sure you set the `engine` key in the config object when initializing.' @@ -56,6 +57,8 @@ export default class PhysicsWorldControlSystem extends System { added(engine) { this.physicalEntities = engine.getNodes(PhysicalNode); + this.grabAreaEntities = engine.getNodes(GrabAreaNode); + this.physicalEntities.on('nodeAdded', (event) => { World.add(this.engine.world, [event.node.body.body]); @@ -64,6 +67,15 @@ export default class PhysicsWorldControlSystem extends System { World.remove(this.engine.world, [event.node.body.body]); }); + + this.grabAreaEntities.on('nodeAdded', (event) => { + + World.add(this.engine.world, [event.node.grabArea.area]); + }); + this.grabAreaEntities.on('nodeRemoved', (event) => { + + World.remove(this.engine.world, [event.node.grabArea.area]); + }); } /** @@ -78,6 +90,10 @@ export default class PhysicsWorldControlSystem extends System { this.physicalEntities.removeAllListeners('nodeAdded'); this.physicalEntities.removeAllListeners('nodeRemoved'); this.physicalEntities = null; + + this.grabAreaEntities.removeAllListeners('nodeAdded'); + this.grabAreaEntities.removeAllListeners('nodeRemoved'); + this.grabAreaEntities = null; } /** @@ -95,4 +111,3 @@ export default class PhysicsWorldControlSystem extends System { } }; -