Compare commits

...

13 Commits

Author SHA1 Message Date
Nathan Kunicki
18acbd96b2 Fixed addition of velocity to take into account delta 2016-02-14 01:56:23 +00:00
Nathan Kunicki
14ad28dd29 Particle system appears to be working 2016-02-14 01:40:00 +00:00
Nathan Kunicki
4d6a9561ae Particle system appears to be working 2016-02-14 01:19:07 +00:00
Nathan Kunicki
1316c814ec Particle system appears to be working 2016-02-14 01:16:11 +00:00
Nathan Kunicki
2d701de8d1 Particles now emit at a specified rate and correctly obey parent positions 2016-02-13 23:22:21 +00:00
Nathan Kunicki
9f53e28f35 First pass at particle system 2016-02-13 16:32:26 +00:00
Nathan Kunicki
e620ff9c94 Delta is now passed as param to update methods rather than game property, tidied up sprite and entity 2015-12-24 18:29:00 +00:00
Nathan Kunicki
5446079fe6 Seperated out stepper from start function, allows manual stepping of world 2015-12-23 17:32:44 +00:00
Nathan Kunicki
de6b327837 Fixed Safari Rect rendering issue 2015-12-23 16:41:46 +00:00
Nathan Kunicki
18df8c17ec Added image classes, new example (snowflakes) 2015-12-22 18:08:33 +00:00
Nathan Kunicki
2c88c9ce20 Tidied up method names 2015-12-21 16:56:24 +00:00
Nathan Kunicki
2eade7a21b Fixed bug in pong bounds handling 2015-12-21 16:48:55 +00:00
Nathan Kunicki
ae48ce9de2 Committed dists 2015-12-21 16:40:40 +00:00
26 changed files with 562 additions and 68 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
.idea/
.DS_Store
dist/
node_modules/

2
dist/es5.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/es5.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
examples/particles/dist/particles.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Particles - MomentumEngine</title>
<script type="application/javascript" src="./dist/particles.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

View File

@ -0,0 +1,76 @@
"use strict";
import MomentumEngine from "../../src/es6";
let KeyConsts = MomentumEngine.Consts.Input.Keys;
class BlueParticle extends MomentumEngine.Classes.Rect {
constructor (x, y) {
super(x, y, 1, 1, new MomentumEngine.Classes.Color(255, 255, 255, 0));
this.timeToLive = 5000;
}
update (delta) {
this.color.a = this.color.a - (delta * 0.00025);
}
}
window.onload = function () {
let width = 640,
height = 360,
baseSize = width / 64;
let particles = new MomentumEngine.Classes.Game({
canvas: document.getElementById("canvas"),
width: width,
height: height,
fixRatio: true,
desiredFps: 60,
inputs: {
keyboard: true
}
});
let black = new MomentumEngine.Classes.Color(0, 0, 0),
red = new MomentumEngine.Classes.Color(255, 0, 0);
let mainScene = new MomentumEngine.Classes.Rect(0, 0, width, height, black);
particles.addChildEntity(mainScene);
let rect = new MomentumEngine.Classes.Rect(width / 2 - baseSize, height / 2 - baseSize, baseSize * 2, baseSize * 2, red),
emitter = new MomentumEngine.Classes.Emitter(baseSize, baseSize, 4, new MomentumEngine.Classes.Vector2D(0, 0.05), BlueParticle);
mainScene.addChildEntity(rect);
rect.addChildEntity(emitter);
rect.update = function (delta) {
if (particles.inputs.keyboard.isPressed(KeyConsts.UP)) {
rect.pos.y -= (0.2 * delta);
}
if (particles.inputs.keyboard.isPressed(KeyConsts.DOWN)) {
rect.pos.y += (0.2 * delta);
}
if (particles.inputs.keyboard.isPressed(KeyConsts.LEFT)) {
rect.pos.x -= (0.2 * delta);
}
if (particles.inputs.keyboard.isPressed(KeyConsts.RIGHT)) {
rect.pos.x += (0.2 * delta);
}
};
emitter.setParticleParent(mainScene);
emitter.emitting = true;
particles.start();
};

View File

@ -6,6 +6,6 @@
<script type="application/javascript" src="./pong.js"></script>
</head>
<body>
<canvas id="game"></canvas>
<canvas id="canvas"></canvas>
</body>
</html>

View File

@ -12,7 +12,7 @@ window.onload = function () {
var pong = new MomentumEngine.Classes.Game({
canvas: document.getElementById("game"),
canvas: document.getElementById("canvas"),
width: width,
height: height,
fixRatio: true,
@ -28,7 +28,7 @@ window.onload = function () {
black = new MomentumEngine.Classes.Color(0, 0, 0);
// All of these are instances of MomentumEngine.Entity;
// All of these are instances of MomentumEngine.Classes.Entity
var mainScene = new MomentumEngine.Classes.Rect(0, 0, width, height, black),
ball = new MomentumEngine.Classes.Rect((width / 2) - (baseSize / 2), (height / 2) - (baseSize / 2), baseSize, baseSize, white),
leftPaddle = new MomentumEngine.Classes.Rect(baseSize, baseSize, baseSize, baseSize * 7, white),
@ -42,33 +42,33 @@ window.onload = function () {
mainScene.addChildEntity(rightPaddle);
// Update and render the ball
// Update the ball position
ball.state.speed = new MomentumEngine.Classes.Vector2D(0.1, 0.05); // Current ball speed
ball.update = function () {
ball.update = function (delta) {
this.pos.add(this.state.speed.clone().multiply(pong.lastFrameDelta));
this.pos.add(this.state.speed.clone().multiply(delta));
if ((this.pos.x + baseSize > width) || (this.pos.x < 0)) {
if ((this.pos.x + baseSize > width && this.state.speed.x > 0) || (this.pos.x < 0 && this.state.speed.x < 0)) {
this.state.speed.x = -this.state.speed.x;
}
if ((this.pos.y + baseSize > height) || (this.pos.y < 0)) {
if ((this.pos.y + baseSize > height && this.state.speed.y > 0) || (this.pos.y < 0 && this.state.speed.y < 0)) {
this.state.speed.y = -this.state.speed.y;
}
};
// Update and render the left paddle
leftPaddle.update = function () {
// Update the left paddle according to keyboard input
leftPaddle.update = function (delta) {
if (pong.inputs.keyboard.isPressed(KeyConsts.CHAR_Q) || pong.inputs.keyboard.isPressed(KeyConsts.UP)) {
leftPaddle.pos.y -= (0.5 * pong.lastFrameDelta);
leftPaddle.pos.y -= (0.5 * delta);
}
if (pong.inputs.keyboard.isPressed(KeyConsts.CHAR_A) || pong.inputs.keyboard.isPressed(KeyConsts.DOWN)) {
leftPaddle.pos.y += (0.5 * pong.lastFrameDelta);
leftPaddle.pos.y += (0.5 * delta);
}
if (leftPaddle.pos.y > height - (baseSize * 8)) {
@ -79,23 +79,22 @@ window.onload = function () {
leftPaddle.pos.y = baseSize;
}
if (leftPaddle.isColliding(ball) && ball.state.speed.x < 0) {
if (leftPaddle.isCollidingWith(ball) && ball.state.speed.x < 0) {
ball.state.speed.x = -ball.state.speed.x;
console.log(ball.state.speed.length());
}
};
// Render the right paddle
rightPaddle.update = function () {
// Update the right paddle according to keyboard input
rightPaddle.update = function (delta) {
if (pong.inputs.keyboard.isPressed(KeyConsts.CHAR_O)) {
rightPaddle.pos.y -= (0.5 * pong.lastFrameDelta);
rightPaddle.pos.y -= (0.5 * delta);
}
if (pong.inputs.keyboard.isPressed(KeyConsts.CHAR_L)) {
rightPaddle.pos.y += (0.5 * pong.lastFrameDelta);
rightPaddle.pos.y += (0.5 * delta);
}
if (rightPaddle.pos.y > height - (baseSize * 8)) {
@ -106,7 +105,7 @@ window.onload = function () {
rightPaddle.pos.y = baseSize;
}
if (rightPaddle.isColliding(ball) && ball.state.speed.x > 0) {
if (rightPaddle.isCollidingWith(ball) && ball.state.speed.x > 0) {
ball.state.speed.x = -ball.state.speed.x;
}

View File

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<title>Snowflakes - MomentumEngine</title>
<script type="application/javascript" src="../../dist/es5.js"></script>
<script type="application/javascript" src="./snowflakes.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -0,0 +1,68 @@
"use strict";
window.onload = function () {
var KeyConsts = MomentumEngine.Consts.Input.Keys;
var width = 640,
height = 360;
var snowflakes = new MomentumEngine.Classes.Game({
canvas: document.getElementById("canvas"),
width: width,
height: height,
fixRatio: true,
desiredFps: 60
});
// Colors
var blue = new MomentumEngine.Classes.Color(204, 255, 255);
// All of these are instances of MomentumEngine.Classes.Entity
var mainScene = new MomentumEngine.Classes.Rect(0, 0, width, height, blue);
// Load images
var snowflakeImg = new MomentumEngine.Classes.ImageLoader("./snowflake.png");
// Create scene graph
snowflakes.addChildEntity(mainScene);
mainScene.update = function (delta) {
if ((snowflakes.frameCounter % 120) == 0) { // Every two seconds or so, add a new snowflake
var startPos = (Math.random() * width) - 50;
var newSnowflake = new MomentumEngine.Classes.Sprite(startPos, -100, 100, 100, snowflakeImg);
newSnowflake.update = function () {
this.pos.y = this.pos.y + (delta * 0.06);
};
mainScene.addChildEntity(newSnowflake);
mainScene.children.forEach(function (oldSnowflake) {
if (oldSnowflake.pos.y > height) {
// Clean up snowflakes that are no longer visible
mainScene.detachChildEntity(oldSnowflake);
}
});
}
};
snowflakes.start();
};

View File

@ -26,7 +26,8 @@ var build = function (options, callback) {
webpack({
entry: {
"es5": path.join(__dirname, "src", "es5.js")
"es5": path.join(__dirname, "src", "es5.js"),
"particles": path.join(__dirname, "examples/particles", "particles.js")
},
bail: !options.watch,
watch: options.watch,
@ -41,7 +42,8 @@ var build = function (options, callback) {
loader: "babel-loader",
test: /\.js$/,
include: [
path.join(__dirname, "src")
path.join(__dirname, "src"),
path.join(__dirname, "examples")
],
query: {
plugins: ["transform-runtime"],
@ -76,6 +78,15 @@ var build = function (options, callback) {
};
gulp.task("move", () => {
gulp.src([
"./dist/particles.*"
], {
base: "./dist"
}).pipe(gulp.dest("examples/particles/dist"));
});
gulp.task("build-dev", (callback) => {
build({
watch: false,

View File

@ -12,15 +12,15 @@
"node": "^4.2.3"
},
"devDependencies": {
"babel-core": "6.3.17",
"babel-loader": "6.2.0",
"babel-plugin-transform-runtime": "6.3.13",
"babel-preset-es2015": "6.3.13",
"babel-preset-stage-0": "6.3.13",
"babel-runtime": "6.3.19",
"gulp": "3.9.0",
"babel-core": "6.5.2",
"babel-loader": "6.2.2",
"babel-plugin-transform-runtime": "6.5.2",
"babel-preset-es2015": "6.5.0",
"babel-preset-stage-0": "6.5.0",
"babel-runtime": "6.5.0",
"gulp": "3.9.1",
"gulp-util": "3.0.7",
"webpack": "1.12.9"
"webpack": "1.12.13"
},
"dependencies": {}
}

View File

@ -1,3 +1,5 @@
"use strict";
class Color {

94
src/classes/emitter.js Normal file
View File

@ -0,0 +1,94 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
class Emitter extends Entity {
constructor (x, y, rate, velocity, particle) {
super(x, y);
this.particleVelocity = velocity;
this.particleClass = particle;
this.rate = rate;
this.emitting = false;
this.spread = Math.PI;
this._lastEmitTime = this._creationTime;
this._wasEmitting = false;
this._particles = [];
}
setParticleParent (entity) {
this._particleParent = entity;
}
_emit () {
let ParticleClass = this.particleClass,
parent = this._particleParent || this._parent;
let angle = this.particleVelocity.angle() + this.spread - (Math.random() * this.spread * 2),
magnitude = this.particleVelocity.length(),
velocity = Vector2D.fromAngle(angle, magnitude);
let particle = new ParticleClass(this._calculatedPos.x, this._calculatedPos.y);
particle.velocity = velocity;
//this._particles.push(particle);
parent.addChildEntity(particle);
}
_triggerEmissions () {
// We prematurely call preprocess so that child particles can spawn from the emitters permission but be children of a different parent
// NK: This might cause a bug where child renders have an incorrect position because preprocess should normally be called after the update function but before the render, here it is before update. We'll see.
this._preprocess();
if (this.emitting) {
let currentTime = +(new Date());
if (!this._wasEmitting) {
this._wasEmitting = true;
this._lastEmitTime = currentTime;
}
let emitDelta = currentTime - this._lastEmitTime;
if (emitDelta > this.rate) {
let emissions = ~~(emitDelta / this.rate);
this._lastEmitTime = currentTime + (emitDelta - (this.rate * emissions));
for (let i = 0; i < emissions; i++) {
this._emit();
}
}
} else {
this._wasEmitting = false;
}
}
update () {
this._triggerEmissions();
}
}
export default Emitter;

View File

@ -8,6 +8,8 @@ class Entity {
constructor (x, y) {
this.pos = new Vector2D(x || 0, y || 0);
this.velocity = new Vector2D(0, 0);
this.acceleration = new Vector2D(0, 0);
this.state = {};
this.children = [];
@ -17,6 +19,32 @@ class Entity {
this._game = null;
this._parent = null;
this._creationTime = +(new Date());
}
setVelocity (x, y) {
if (x instanceof Vector2D) {
this.velocity = x;
} else {
this.velocity.x = x;
this.velocity.y = y;
}
}
setAcceleration (x, y) {
if (x instanceof Vector2D) {
this.acceleration = x;
} else {
this.acceleration.x = x;
this.acceleration.y = y;
}
}
@ -44,25 +72,39 @@ class Entity {
}
detachChildEntity (entity) {
// Not implemented
detachChildEntity (child) {
for (let i = 0; i < this.children.length; i++) {
if (this.children[i] == child) {
this.children.splice(i, 1);
return true;
}
}
return false;
}
_recalculatePos () {
_preprocess () {
// NK: This should be called within "render", not "update". The purpose of this function is to calculate the true position of the entity relative to all its parents. It does this recursively, calling the _recalculatePos method all the way back up the tree and continuously adding the results together.
// NK: The purpose of this function is to calculate the true position of the entity relative to all its parents. It does this recursively, calling the _preprocess method all the way back up the tree and continuously adding the results together.
// Note there is a limiter, where the last calculated frame is stored, so that if the position has already been calculated for that node in this particular frame, the cached result is used rather than recalculating.
// When rendering, the draw calls should use this._calculatedPos rather than this.pos in order for the position to be correct.
if (this._game && this._lastCalculated < this._game.frameCounter) {
if (this._parent) {
let parentPos = this._parent._recalculatePos();
let parentPos = this._parent._preprocess();
this._calculatedPos.x = this.pos.x + parentPos.x;
this._calculatedPos.y = this.pos.y + parentPos.y;
} else {
this._calculatedPos.x = this.pos.x;
this._calculatedPos.y = this.pos.y;
@ -88,12 +130,32 @@ class Entity {
}
_updateEntity () {
_updateEntity (delta) {
if ((this.update && this.update()) || (typeof this.update === "undefined")) {
if (this.timeToLive) {
if (+(new Date()) - this._creationTime > this.timeToLive) {
this._parent.detachChildEntity(this);
}
}
// Calculate new position based on velocity and acceleration if there's one set
if (this.velocity) {
if (this.acceleration) {
this.velocity.add(this.acceleration);
}
this.pos.add(this.velocity.clone().multiply(delta));
}
// If there's an update method, call it
let updated = this.update && this.update(delta);
if (updated || (typeof updated == "undefined") || (typeof this.update === "undefined")) {
this.children.forEach((child) => {
child._updateEntity();
child._updateEntity(delta);
});
}
@ -103,7 +165,11 @@ class Entity {
_renderEntity () {
if ((this.render && this.render()) || (typeof this.render === "undefined")) {
this._preprocess();
let rendered = this.render && this.render();
if (rendered || (typeof rendered == "undefined") || (typeof this.render === "undefined")) {
this.children.forEach((child) => {
child._renderEntity();

View File

@ -17,19 +17,19 @@ class Game extends Entity {
if (config.canvas) {
this.canvas = config.canvas;
} else {
throw new Error("MomentumEngine.Game must be constructed with a canvas");
throw new Error("MomentumEngine.Classes.Game must be constructed with a canvas");
}
if (config.width) {
this.width = config.width;
} else {
throw new Error("MomentumEngine.Game must be constructed with canvas width");
throw new Error("MomentumEngine.Classes.Game must be constructed with canvas width");
}
if (config.height) {
this.height = config.height;
} else {
throw new Error("MomentumEngine.Game must be constructed with canvas height");
throw new Error("MomentumEngine.Classes.Game must be constructed with canvas height");
}
@ -86,7 +86,6 @@ class Game extends Entity {
// Initialize defaults
this.lastFrameDelta = 0;
this.frameCounter = 0;
this.inputs = {};
@ -101,6 +100,16 @@ class Game extends Entity {
}
step (delta) {
this.frameCounter++;
this._updateEntity(delta);
this._renderEntity();
}
start () {
var self = this; // NK: Hate doing this...better way plz?
@ -108,13 +117,13 @@ class Game extends Entity {
if (self._wantPause) {
self._wantPause = false;
} else {
console.log("MomentumEngine.Game.start called, game instance is already started");
console.log("MomentumEngine.Classes.Game.start called, game instance is already started");
return false; // Game is already running
}
self._wantPause = false;
var requestFrame = (() => {
let requestFrame = (() => {
return (window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
@ -126,27 +135,21 @@ class Game extends Entity {
})();
self._lastFrameTimestamp = +(new Date());
self.startTime = self._lastFrameTimestamp;
var loop = function () {
self.frameCounter++;
let currentTimestamp = +(new Date());
self.lastFrameDelta = currentTimestamp - self._lastFrameTimestamp;
self._lastFrameTimestamp = currentTimestamp;
self.lastFrameDelta = Math.min(self.lastFrameDelta, 1000 / self.desiredFps);
if (self._wantPause) {
return;
}
self._updateEntity.bind(self);
self._updateEntity();
let currentTimestamp = +(new Date()),
delta = currentTimestamp - self._lastFrameTimestamp;
self._renderEntity.bind(self);
self._renderEntity();
delta = Math.min(delta, 1000 / self.desiredFps);
self._lastFrameTimestamp = currentTimestamp;
self.step(delta);
requestFrame(loop);
@ -164,7 +167,7 @@ class Game extends Entity {
this._wantPause = true;
return true;
} else {
console.log("MomentumEngine.Game.pause called, game instance is already paused");
console.log("MomentumEngine.Classes.Game.pause called, game instance is already paused");
return false;
}
}

View File

@ -0,0 +1,52 @@
"use strict";
class ImageLoader {
constructor (src) {
this._loaded = false; // Default is true, set it to false until the image has loaded
this._error = false; // If the image fails to load, this will contain the reason
this._imageObj = new Image();
this._imageObj.addEventListener("load", () => {
this._loaded = true;
this._error = false;
});
this._imageObj.addEventListener("_error", (err) => {
this._loaded = false;
this._error = err;
});
this._imageObj.src = src;
}
getImageObj () {
if (this._loaded) {
return this._imageObj;
} else {
return false;
}
}
isLoaded () {
return this._loaded;
}
isError () {
return this._error;
}
}
export default ImageLoader;

View File

@ -119,7 +119,7 @@ class KeyboardInput {
isPressed (keyCode) {
return !!this._keyState[keyCode];
return !!this._keyState[keyCode];
}

View File

@ -19,7 +19,7 @@ class Rect extends Entity {
}
isColliding (entity) {
isCollidingWith (entity) {
if (entity instanceof Rect) {
return CollisionMethods.AABB(this, entity);
@ -30,11 +30,9 @@ class Rect extends Entity {
render () {
this._recalculatePos();
if (this._game) {
this._game.context.fillStyle = this.color;
this._game.context.fillStyle = this.color.toString();
this._game.context.fillRect(this._calculatedPos.x, this._calculatedPos.y, this.size.x, this.size.y);
return true;

78
src/classes/sprite.js Normal file
View File

@ -0,0 +1,78 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
import ImageLoader from "./imageloader.js";
import CollisionMethods from "../libs/collisionmethods.js";
class Sprite extends Entity {
constructor (x, y, width, height, image) {
if (!image instanceof ImageLoader) {
throw new Error("MomentumEngine.Classes.Sprite must be instantiated with an ImageLoader instance");
}
super(x, y);
this.size = new Vector2D(width || 0, height || 0);
this._image = image;
this._imagePos = new Vector2D(0, 0);
this._imageSize = new Vector2D(0, 0);
}
setImageCoords (x, y, width, height) {
this._imagePos.x = x;
this._imagePos.y = y;
this._imageSize.x = width || 0;
this._imageSize.y = height || 0;
}
isReady () {
return (this._image.isLoaded() && !this._image.isError());
}
render () {
if (this.isReady() && this._game) {
let imageObj = this._image.getImageObj();
let subWidth = imageObj.width - this._imagePos.x,
subHeight = imageObj.height - this._imagePos.y;
this._game.context.drawImage(
imageObj,
this._imagePos.x,
this._imagePos.y,
this._imageSize.x || subWidth,
this._imageSize.y || subHeight,
this._calculatedPos.x,
this._calculatedPos.y,
this.size.x || subWidth,
this.size.y || subHeight
);
return true;
} else {
return false;
}
}
}
export default Sprite;

View File

@ -101,6 +101,11 @@ class Vector2D {
}
angle () {
return Math.atan2(this.x, this.y);
}
toArray () {
return [this.x, this.y];
}
@ -116,6 +121,11 @@ class Vector2D {
}
static fromAngle (angle, length) {
return new Vector2D(length * Math.cos(angle), length * Math.sin(angle));
}
}

View File

@ -1,20 +1,26 @@
"use strict";
import Game from "./classes/game.js";
import Emitter from "./classes/emitter.js";
import Entity from "./classes/entity.js";
import Vector2D from "./classes/vector2d.js";
import Sprite from "./classes/sprite.js";
import Rect from "./classes/rect.js";
import Color from "./classes/color.js";
import ImageLoader from "./classes/imageloader.js";
import {KeyConsts} from "./classes/keyboardinput.js";
const Classes = {
Game: Game,
Emitter: Emitter,
Entity: Entity,
Sprite: Sprite,
Rect: Rect,
Vector2D: Vector2D,
Color: Color
Color: Color,
ImageLoader: ImageLoader
};

View File

@ -1,20 +1,26 @@
"use strict";
import Game from "./classes/game.js";
import Emitter from "./classes/emitter.js";
import Entity from "./classes/entity.js";
import Vector2D from "./classes/vector2d.js";
import Sprite from "./classes/sprite.js";
import Rect from "./classes/rect.js";
import Color from "./classes/color.js";
import ImageLoader from "./classes/imageloader.js";
import {KeyConsts} from "./classes/keyboardinput.js";
const Classes = {
Game: Game,
Emitter: Emitter,
Entity: Entity,
Sprite: Sprite,
Rect: Rect,
Vector2D: Vector2D,
Color: Color
Color: Color,
ImageLoader: ImageLoader
};
@ -25,7 +31,7 @@ const Consts = {
};
export {
export default {
Classes,
Consts
};

View File

@ -1,5 +1,4 @@
import Rect from "../classes/rect.js";
import Vector2D from "../classes/vector2d.js";
class CollisionMethods {