├── .npmignore
├── .gitignore
├── background.png
├── games-background.png
├── src
├── Point.js
├── EngineObject.js
└── index.js
├── package.json
├── webpack.config.js
├── LICENSE.md
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | webpack.config.js
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/space2pacman/elpy/HEAD/background.png
--------------------------------------------------------------------------------
/games-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/space2pacman/elpy/HEAD/games-background.png
--------------------------------------------------------------------------------
/src/Point.js:
--------------------------------------------------------------------------------
1 | class Point {
2 | distance(x1, y1, x2, y2) {
3 | return Math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2));
4 | }
5 | }
6 |
7 | module.exports = new Point();
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elpy",
3 | "version": "1.1.16",
4 | "description": "2D JavaScript game engine.",
5 | "author": "space2pacman",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/space2pacman/elpy.git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/space2pacman/elpy/issues"
13 | },
14 | "keywords": [
15 | "html5",
16 | "game",
17 | "rendering",
18 | "engine",
19 | "2d",
20 | "canvas",
21 | "web",
22 | "javascript"
23 | ],
24 | "main": "src/index.js",
25 | "scripts": {
26 | "build": "webpack",
27 | "serve": "webpack serve"
28 | },
29 | "devDependencies": {
30 | "webpack": "^5.64.2",
31 | "webpack-cli": "^4.9.1",
32 | "webpack-dev-server": "^4.10.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | mode: 'production',
5 | entry: {
6 | main: path.resolve(__dirname, './src/index.js')
7 | },
8 | output: {
9 | path: path.resolve(__dirname, './dist'),
10 | filename: 'elpy.min.js',
11 | library: 'Elpy'
12 | },
13 | devServer: {
14 | static: [
15 | {
16 | directory: path.join(__dirname, './public')
17 | },
18 | {
19 | directory: path.join(__dirname, './dist'),
20 | publicPath: '/dist'
21 | },
22 | {
23 | directory: path.join(__dirname, './examples'),
24 | publicPath: '/examples'
25 | }
26 | ],
27 | port: 8080,
28 | }
29 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Yaroslav Ivanov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/EngineObject.js:
--------------------------------------------------------------------------------
1 | const point = require('./Point');
2 |
3 | class EngineObject {
4 | constructor(name, x, y, width, height, options = {}) {
5 | this._name = name;
6 | this._x = x;
7 | this._y = y;
8 | this._width = width;
9 | this._height = height;
10 | this._events = {};
11 | this._collision = {};
12 | this._isJumping = false;
13 | this._isFalling = false;
14 | this._isFlying = false;
15 | this._isStopped = false;
16 | this._state = null;
17 | this._ghost = false;
18 | this._animate = false;
19 | this._added = false,
20 | this._exist = true;
21 | this._track = {
22 | x: null,
23 | y: null
24 | };
25 | this._dest = {
26 | x: null,
27 | y: null
28 | };
29 | this._offset = {
30 | x: 0,
31 | y: 0,
32 | object: null,
33 | rotate: {
34 | x: 0,
35 | y: 0
36 | }
37 | };
38 | this._positions = {
39 | start: {
40 | x: null,
41 | y: null
42 | }
43 | }
44 | this._degrees = 0;
45 | this._options = {
46 | obstacle: typeof options.obstacle === 'boolean' ? options.obstacle : true,
47 | pushing: typeof options.pushing === 'boolean' ? options.pushing : false,
48 | disabledEvents: typeof options.disabledEvents === 'boolean' ? options.disabledEvents : false,
49 | type: options.type || null,
50 | custom: options.custom || null,
51 | color: options.color || 'black',
52 | image: {
53 | path: (typeof options.image === 'object' && options.image !== null ? options.image.path : options.image) || null,
54 | repeat: (typeof options.image === 'object' && options.image !== null ? options.image.repeat : false) || false,
55 | rendering: false,
56 | cached: null
57 | },
58 | images: {
59 | list: options.images || null,
60 | rendering: false,
61 | cached: {}
62 | },
63 | fixedCamera: {
64 | x: false,
65 | y: false
66 | }
67 | };
68 | this._params = {
69 | movement: {
70 | acceleration: 0
71 | },
72 | jump: {
73 | multiplier: 0
74 | },
75 | fall: {
76 | multiplier: 0
77 | }
78 | }
79 | this._MAX_ACCELERATION = 10;
80 |
81 | this._init();
82 | }
83 |
84 | run(step = 1) {
85 | const x = this._x + Math.cos((this._degrees + 90) * Math.PI / 180) * step;
86 | const y = this._y + Math.sin((this._degrees + 90) * Math.PI / 180) * step;
87 |
88 | this.move(x, y);
89 | }
90 |
91 | move(x, y) {
92 | this._dest.x = x;
93 | this._dest.y = y;
94 | this._track.x = this._x;
95 | this._track.y = this._y;
96 |
97 | for(const name in this._collision) {
98 | const object = this._collision[name];
99 |
100 | if (this !== object
101 | && this.isExist
102 | && object.isExist
103 | && !object.ghost
104 | && (x + this._width) > object.x
105 | && x < (object.x + object.width)
106 | && (y + this._height) > object.y
107 | && y < (object.y + object.height)) {
108 |
109 | const side = this._getCollisionSide(x, y, object);
110 |
111 | this._dispatchEvent('collision', object, side);
112 |
113 | if (object.options.obstacle) {
114 | return false;
115 | }
116 | }
117 | }
118 |
119 | if (this._options.fixedCamera.x && x > this._track.x) {
120 | this._offset.x += Math.abs(this._track.x - x);
121 | }
122 |
123 | if (this._options.fixedCamera.x && x < this._track.x) {
124 | this._offset.x -= Math.abs(this._track.x - x);
125 | }
126 |
127 | if (this._options.fixedCamera.y && y > this._track.y) {
128 | this._offset.y += Math.abs(this._track.y - y);
129 | }
130 |
131 | if (this._options.fixedCamera.y && y < this._track.y) {
132 | this._offset.y -= Math.abs(this._track.y - y);
133 | }
134 |
135 | this._x = x;
136 | this._y = y;
137 |
138 | this._dispatchEvent('move');
139 | }
140 |
141 | fly(degrees = 0, distance = 0, step = 1) {
142 | const event = this._getEventObject();
143 |
144 | this._positions.start.x = this._x;
145 | this._positions.start.y = this._y;
146 |
147 | this._nextTick(() => {
148 | this._isStopped = false;
149 | this._isFlying = true;
150 | this._tick(this._onFly.bind(this, event, degrees, distance, step));
151 | });
152 | }
153 |
154 | jump(height = 0, multiplier = 0.1, forced = false) {
155 | const event = this._getEventObject();
156 |
157 | if (forced) {
158 | this._isJumping = false;
159 | }
160 |
161 | if (this._isJumping) {
162 | return false;
163 | }
164 |
165 | this._isFalling = false;
166 | this._isJumping = true;
167 | this._params.movement.acceleration = this._getMaxJumpAccelerationValue(height, multiplier);
168 | this._params.jump.multiplier = multiplier;
169 |
170 | this._nextTick(() => {
171 | this._isStopped = false;
172 | this._tick(this._onJump.bind(this, event));
173 | });
174 | }
175 |
176 | fall(multiplier = 0.1) {
177 | const event = this._getEventObject();
178 |
179 | this._isFalling = true;
180 | this._params.fall.multiplier = multiplier;
181 |
182 | this._nextTick(() => {
183 | this._isStopped = false;
184 | this._tick(this._onFall.bind(this, event));
185 | });
186 | }
187 |
188 | push(pusher) {
189 | let direction;
190 | let distance;
191 |
192 | if (pusher.dest.y < pusher.y) {
193 | direction = 'up';
194 | distance = Math.abs(pusher.dest.y - pusher.y);
195 | }
196 |
197 | if (pusher.dest.y > pusher.y) {
198 | direction = 'down';
199 | distance = Math.abs(pusher.dest.y - pusher.y);
200 | }
201 |
202 | if (pusher.dest.x > pusher.x) {
203 | direction = 'right';
204 | distance = Math.abs(pusher.dest.x - pusher.x);
205 | }
206 |
207 | if (pusher.dest.x < pusher.x) {
208 | direction = 'left';
209 | distance = Math.abs(pusher.dest.x - pusher.x);
210 | }
211 |
212 | switch (direction) {
213 | case 'up':
214 | this.move(this.x, this.y - distance);
215 |
216 | if (this.track.y !== this.y) {
217 | pusher.move(pusher.x, pusher.y - distance);
218 | }
219 |
220 | break;
221 | case 'down':
222 | this.move(this.x, this.y + distance);
223 |
224 | if (this.track.y !== this.y) {
225 | pusher.move(pusher.x, pusher.y + distance);
226 | }
227 |
228 | break;
229 | case 'right':
230 | this.move(this.x + distance, this.y);
231 |
232 | if (this.track.x !== this.x) {
233 | pusher.move(pusher.x + distance, pusher.y);
234 | }
235 |
236 | break;
237 | case 'left':
238 | this.move(this.x - distance, this.y);
239 |
240 | if (this.track.x !== this.x) {
241 | pusher.move(pusher.x - distance, pusher.y);
242 | }
243 |
244 | break;
245 | }
246 | }
247 |
248 | rotate(degrees = 0, x = 0, y = 0) {
249 | this._offset.rotate.x = x;
250 | this._offset.rotate.y = y;
251 | this._degrees = degrees;
252 |
253 | this._dispatchEvent('rotate');
254 | }
255 |
256 | stop() {
257 | this._isStopped = true;
258 | }
259 |
260 | destroy() {
261 | delete this._collision[this._name];
262 | this._exist = false;
263 |
264 | this._dispatchEvent('destroy');
265 | }
266 |
267 | collision(object) {
268 | if (Array.isArray(object)) {
269 | object.forEach(item => {
270 | if (item.isExist) {
271 | this._collision[item.name] = item;
272 | }
273 | });
274 | } else {
275 | if (object.isExist) {
276 | this._collision[object.name] = object;
277 | }
278 | }
279 | }
280 |
281 | on(name, callback) {
282 | if (!this._events[name]) {
283 | this._events[name] = [];
284 | }
285 |
286 | this._events[name].push(callback);
287 | }
288 |
289 | setOffsetObject(object) {
290 | this._offset.object = object;
291 | }
292 |
293 | removeCollision(object) {
294 | delete this._collision[object.name];
295 | }
296 |
297 | get name() {
298 | return this._name;
299 | }
300 |
301 | get options() {
302 | return this._options;
303 | }
304 |
305 | get obstacles() {
306 | return Object.values(this._collision);
307 | }
308 |
309 | get track() {
310 | return this._track;
311 | }
312 |
313 | get dest() {
314 | return this._dest;
315 | }
316 |
317 | get offset() {
318 | return this._offset;
319 | }
320 |
321 | get isPushing() {
322 | return this._options.pushing;
323 | }
324 |
325 | get isJumping() {
326 | return this._isJumping;
327 | }
328 |
329 | get isFalling() {
330 | return this._isFalling;
331 | }
332 |
333 | get isFlying() {
334 | return this._isFlying;
335 | }
336 |
337 | get isExist() {
338 | return this._exist;
339 | }
340 |
341 | get x() {
342 | return this._x;
343 | }
344 |
345 | set x(value) {
346 | this._x = value;
347 | }
348 |
349 | get y() {
350 | return this._y;
351 | }
352 |
353 | set y(value) {
354 | this._y = value;
355 | }
356 |
357 | get width() {
358 | return this._width;
359 | }
360 |
361 | set width(value) {
362 | return this._width = value;
363 | }
364 |
365 | get height() {
366 | return this._height;
367 | }
368 |
369 | set height(value) {
370 | return this._height = value;
371 | }
372 |
373 | get state() {
374 | return this._state;
375 | }
376 |
377 | set state(state) {
378 | this._state = state;
379 |
380 | this._dispatchEvent('state');
381 | }
382 |
383 | get animate() {
384 | return this._animate;
385 | }
386 |
387 | set animate(value) {
388 | if (value) {
389 | if (Array.isArray(this.options.images.list) && this.options.images.list.length > 0) {
390 | requestAnimationFrame(this._animation.bind(this));
391 | }
392 | }
393 |
394 | this._animate = value;
395 | }
396 |
397 |
398 | get ghost() {
399 | return this._ghost;
400 | }
401 |
402 | set ghost(value) {
403 | this._ghost = value;
404 | }
405 |
406 | get degrees() {
407 | return this._degrees;
408 | }
409 |
410 | get added() {
411 | return this._added;
412 | }
413 |
414 | set added(value) {
415 | return this._added = value;
416 | }
417 |
418 | get _isAccelerationMovementStopped() {
419 | return this._params.movement.acceleration <= 0;
420 | }
421 |
422 | _dispatchEvent(name, ...data) {
423 | if (this._events[name] && Array.isArray(this._events[name]) && this._events[name].length > 0) {
424 | this._events[name].forEach(callback => {
425 | callback(...data);
426 | });
427 | }
428 | }
429 |
430 | _animation() {
431 | if (this._animate) {
432 | this._dispatchEvent('state');
433 |
434 | requestAnimationFrame(this._animation.bind(this));
435 | }
436 | }
437 |
438 | _takeoff() {
439 | const acceleration = Math.floor(this._params.movement.acceleration * 10);
440 | const multiplier = this._params.jump.multiplier * 10;
441 |
442 | this._params.movement.acceleration = (acceleration - multiplier) / 10;
443 |
444 | this.move(this._x, Math.floor(this._y - this._params.movement.acceleration));
445 | }
446 |
447 | _landing() {
448 | if (this._params.movement.acceleration <= this._MAX_ACCELERATION) {
449 | const acceleration = Math.floor(this._params.movement.acceleration * 10);
450 | const multiplier = this._params.fall.multiplier * 10;
451 |
452 | this._params.movement.acceleration = (acceleration + multiplier) / 10;
453 | }
454 |
455 | const moving = this.move(this._x, Math.floor(this._y + this._params.movement.acceleration));
456 |
457 | if (moving === false) {
458 | this._params.movement.acceleration = 0;
459 |
460 | return true;
461 | }
462 | }
463 |
464 | _getCollisionSide(x, y, object) {
465 | let side = null;
466 |
467 | if ((x + this._width) > object.x
468 | && x < (object.x + object.width)
469 | && ((this._y + this._height) <= object.y || this._y >= (object.y + object.height))) {
470 | const top = object.y - (y + this._height);
471 | const bottom = y - (object.y + object.height);
472 |
473 | if (top > bottom) {
474 | side = 'top';
475 | } else {
476 | side = 'bottom';
477 | }
478 | }
479 |
480 | if ((y + this._height) > object.y
481 | && y < (object.y + object.height)
482 | && ((this._x + this._width) <= object.x || this._x >= (object.x + object.width))) {
483 | const left = object.x - (x + this._width);
484 | const right = x - (object.x + object.width);
485 |
486 | if (left > right) {
487 | side = 'left';
488 | } else {
489 | side = 'right';
490 | }
491 | }
492 |
493 | return side;
494 | }
495 |
496 | _getMaxJumpAccelerationValue(max, multiplier) {
497 | let min = 0;
498 | let acceleration = 0;
499 |
500 | while(min <= max) {
501 | min += acceleration;
502 | acceleration = (Math.floor(acceleration * 10) + (multiplier * 10)) / 10;
503 | }
504 |
505 | return acceleration;
506 | }
507 |
508 | _onCollisionSide(object, side) {
509 | if (object.options.obstacle && side === 'bottom') {
510 | this._params.movement.acceleration = 0;
511 | }
512 | }
513 |
514 | _tick(callback) {
515 | const response = callback();
516 |
517 | if (response !== false) {
518 | requestAnimationFrame(this._tick.bind(this, callback));
519 | }
520 | }
521 |
522 | _nextTick(callback) {
523 | requestAnimationFrame(callback);
524 | }
525 |
526 | _getEventObject() {
527 | const event = {
528 | _stopped: false,
529 | _paused: false,
530 | get stopped() {
531 | return this._stopped;
532 | },
533 | get paused() {
534 | return this._paused;
535 | },
536 | stop() {
537 | this._stopped = true;
538 | },
539 | pause() {
540 | this._paused = true;
541 | },
542 | resume() {
543 | this._paused = false;
544 | }
545 | }
546 |
547 | return event;
548 | }
549 |
550 | _onJump(event) {
551 | if (this._isStopped) {
552 | return false;
553 | }
554 |
555 | if (event.paused) {
556 | this._dispatchEvent('jump', event);
557 |
558 | return;
559 | }
560 |
561 | if (event.stopped) {
562 | this._isFalling = true;
563 |
564 | return false;
565 | }
566 |
567 | if (this._isAccelerationMovementStopped) {
568 | this._isFalling = true;
569 |
570 | return false;
571 | } else {
572 | this._takeoff();
573 | this._dispatchEvent('jump', event);
574 | }
575 | }
576 |
577 | _onFall(event) {
578 | if (this._isStopped) {
579 | return false;
580 | }
581 |
582 | if (event.paused) {
583 | this._dispatchEvent('fall', event);
584 |
585 | return;
586 | }
587 |
588 | if (event.stopped) {
589 | this._isJumping = false;
590 |
591 | return false;
592 | }
593 |
594 | if (this._isFalling) {
595 | const landed = this._landing();
596 |
597 | if (landed === true) {
598 | this._isJumping = false;
599 | }
600 |
601 | this._dispatchEvent('fall', event);
602 | }
603 | }
604 |
605 | _onFly(event, degrees, distance, step) {
606 | if (this._isStopped) {
607 | this._isFlying = false;
608 |
609 | return false;
610 | }
611 |
612 | if (event.paused) {
613 | this._dispatchEvent('fly', event);
614 |
615 | return;
616 | }
617 |
618 | if (event.stopped) {
619 | this._isFlying = false;
620 |
621 | return false;
622 | }
623 |
624 | if (distance > 0 && point.distance(this._positions.start.x, this._positions.start.y, this._x, this._y) > distance) {
625 | this.destroy();
626 |
627 | return false;
628 | }
629 |
630 | let x = this._x + parseFloat(Math.cos(degrees * Math.PI / 180).toFixed(10)) * step;
631 | let y = this._y + parseFloat(Math.sin(degrees * Math.PI / 180).toFixed(10)) * step;
632 |
633 | this.move(x, y);
634 | this._dispatchEvent('fly', event);
635 | }
636 |
637 | _init() {
638 | this.on('collision', this._onCollisionSide.bind(this));
639 | }
640 | }
641 |
642 | module.exports = EngineObject;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Elpy.js - 2D JavaScript game engine.
2 |
3 | 
4 |
5 | [](https://github.com/space2pacman/elpy/blob/master/LICENSE.md)
6 | [](https://en.wikipedia.org/wiki/JavaScript)
7 | [](https://img.shields.io/bundlephobia/min/elpy?color=brightgreen&label=Size)
8 | [](https://img.shields.io/npm/dt/elpy?label=Downloads)
9 | [](https://www.npmjs.com/package/elpy)
10 |
11 | | [Demo](https://space2pacman-misc.github.io/elpy-examples/docs/) | [Game examples](https://space2pacman-misc.github.io/elpy-examples/docs/examples.html) |
12 | | :---: | :---: |
13 |
14 | ## Docs
15 | - Install
16 | - Basic usage example
17 | - Engine
18 | - create()
19 | - add()
20 | - key()
21 | - keydown()
22 | - keyup()
23 | - mousemove()
24 | - click()
25 | - tick()
26 | - nextTick()
27 | - checkObjectInViewport()
28 | - fixingCamera()
29 | - unfixingCamera()
30 | - destroy()
31 | - on()
32 | - Event `'load'`
33 | - Event `'animation'`
34 | - load()
35 | - Engine getters
36 | - Object
37 | - run()
38 | - move()
39 | - fly()
40 | - jump()
41 | - fall()
42 | - push()
43 | - rotate()
44 | - stop()
45 | - destroy()
46 | - collision()
47 | - on()
48 | - Event `'collision'`
49 | - Event `'move'`
50 | - Event `'rotate'`
51 | - Event `'destroy'`
52 | - Event `'state'`
53 | - Event `'jump'`
54 | - Event `'fall'`
55 | - Event `'fly'`
56 | - Event object
57 | - removeCollision()
58 | - Object getters
59 | - Object setters
60 | - Development
61 | - License
62 |
63 | 
64 |
65 | ## Install
66 | #### Download
67 | Latest builds are available in the project [releases page](https://github.com/space2pacman/elpy/releases/latest).
68 | #### CDN
69 | ```js
70 | https://unpkg.com/elpy/dist/elpy.min.js
71 | ```
72 | #### NPM
73 | ```js
74 | npm install elpy
75 | ```
76 | ## Basic usage example
77 | ```html
78 |
79 |
80 |
81 | Elpy.js
82 |
83 |
84 |
85 |
86 |
110 |
111 |
112 | ```
113 | ## Engine
114 | ### Create engine instance
115 | ```js
116 | const elpy = new Elpy(
117 | "#element", // id element canvas or HTML object element get by document.querySelector().
118 | 500, // width.
119 | 500, // height.
120 | // options.
121 | {
122 | preload, // default - true, enable / disable preloader.
123 | favicon // default - true, enable / disable favicon.
124 | }
125 | )
126 | ```
127 | ---
128 | ### create(name, x, y, width, height, options)
129 | | name | type | description |
130 | | :---: | :---: | :--- |
131 | | **`name`** | `` | The object name must be unique. |
132 | | **`x`** | `` | Position of the object along the x-axis. |
133 | | **`y`** | `` | Position of the object along the y-axis. |
134 | | **`width`** | `` | Object width in pixels. |
135 | | **`height`** | `` | Object height in pixels. |
136 | | **`options`** | `