@moderrkowo/jsgl
Version:
Client-side JavaScript library for creating web 2D games. Focusing at objective game.
764 lines (763 loc) • 29.3 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = void 0;
var Vector2_1 = require("./structs/Vector2");
var Signals_1 = require("./events/Signals");
var Renderer_1 = require("./drawing/Renderer");
var GameObject_1 = require("./gameobjects/GameObject");
var ClickableGameObject_1 = require("./gameobjects/ClickableGameObject");
var DrawableGameObject_1 = require("./gameobjects/DrawableGameObject");
var MathUtils_1 = require("./utils/math/MathUtils");
var GameSettings_1 = require("./structs/GameSettings");
var Input_1 = require("./Input");
/**
* @group Important Classes
*/
var Game = /** @class */ (function () {
// Constructor
/**
* Constructs new Game instance with given settings.
* @param gameSettings The settings
*/
function Game(gameSettings) {
var _this = this;
this.renderer = new Renderer_1.Renderer(this);
// Game Loop Management
this._isPlaying = false;
this._unscaledTime = 0;
this._deltaTime = 0;
this.timeScale = 1;
this.isNeedToUpdate = true;
this._gameLoop = function (time) {
_this._gameLoopUpdate(time);
if (!document.hasFocus() && !_this.gameSettings.refreshWhenUnfocused) {
if (_this._isPlaying)
window.requestAnimationFrame(_this._gameLoop);
return;
}
_this._gameLoopDraw();
// Continue loop
if (_this._isPlaying)
window.requestAnimationFrame(_this._gameLoop);
};
// Signals
this._signals = new Signals_1.Signals();
this.emit = function (channel, event) {
return _this._signals.emit(channel, event);
};
/**
* Appends listener to event channel.
* @method
* @param channel - The listener event channel.
* @param callback - Executed function after receiving a signal on given channel.
* @example
* game.on('channel', () => { console.log('received!') });
*/
this.on = function (channel, callback) {
return _this._signals.on(channel, callback);
};
// Resources
this.resources = new Map();
this._isLoadedAllResources = false;
// GameObjects
/**
* Sorted game object array
*/
this.gameObjects = [];
this._tempMouseWheelDeltaY = 0;
this.gameSettings = __assign(__assign({}, GameSettings_1.defaultGameSettings), gameSettings);
if (this.gameSettings.canvas !== undefined) {
this.canvas = this.gameSettings.canvas;
}
else {
throw new Error('Cannot asign canvas.');
}
if (this.gameSettings.grid !== undefined) {
this.grid = this.gameSettings.grid;
}
else {
throw new Error('Cannot asign grid.');
}
this.input = new Input_1.Input();
this._registerCanvasEvents();
if (this.gameSettings.autoResize) {
window.addEventListener('resize', function () {
_this.renderer.resizeCanvas();
_this.Update();
});
}
this.renderer.ctx.imageSmoothingEnabled = true;
if (this.gameSettings.canvasImageQuality !== undefined)
this.renderer.ctx.imageSmoothingQuality =
this.gameSettings.canvasImageQuality;
this.canvasViewOffset = new Vector2_1.Vector2();
}
// Canvas
/**
* Scaling canvas size on page to percentage of parent element.
* @param percentage The decimal midpoint
* @returns The final canvas size
*/
Game.prototype.RescaleCanvasToParentElement = function (percentage) {
this.renderer.canvasParentSize = percentage;
this.renderer.resizeCanvas();
return this.renderer.canvasSize;
};
Game.prototype._registerCanvasEvents = function () {
var _this = this;
this.canvas.addEventListener('mousemove', function (event) {
_this.mouseMoveHandler(_this, event);
});
document.addEventListener('mouseup', function () {
_this.mouseUpHandler(_this);
});
document.addEventListener('mousedown', function () {
_this.mouseDownHandler(_this);
});
this.canvas.addEventListener('click', function () {
_this.mouseClickHandler(_this);
});
// Appends mouse wheel listener to canvas
this.canvas.addEventListener('wheel', function (event) {
_this.mouseWheelHandler(_this, event);
}, { passive: false });
document.addEventListener('keydown', function (event) {
if (event.defaultPrevented) {
return;
}
event.preventDefault();
var code = event.code.toLowerCase().replace('key', '');
_this.input.keysDown.add(code);
});
document.addEventListener('keyup', function (event) {
if (event.defaultPrevented) {
return;
}
event.preventDefault();
var code = event.code.toLowerCase().replace('key', '');
_this.input.keysUp.add(code);
});
};
Game.prototype._gameLoopUpdate = function (time) {
// Calculation deltaTime
this._deltaTime = time - this._unscaledTime;
this._unscaledTime = time;
// Update
var tickEvent = {
unscaledDeltaTime: this._deltaTime / 1000,
unscaledTime: this._unscaledTime / 1000,
deltaTime: (this._deltaTime / 1000) * this.timeScale,
timeScale: this.timeScale,
game: this,
};
// Events update
var hoveredGameObject = this.mouseHoveredGameObject;
var mouseEvent = this.constructMouseEvent();
var lastHoveredGameObject = this.lastMouseHoveredGameObject;
if (lastHoveredGameObject !== hoveredGameObject) {
if (lastHoveredGameObject !== undefined)
lastHoveredGameObject.OnMouseHoverEnd(mouseEvent);
hoveredGameObject === null || hoveredGameObject === void 0 ? void 0 : hoveredGameObject.OnMouseHoverStart(mouseEvent);
this.lastMouseHoveredGameObject = hoveredGameObject;
}
this.input.mouseScrollDelta = new Vector2_1.Vector2(0, this._tempMouseWheelDeltaY);
if (this._tempMouseWheelDeltaY !== 0) {
this.emit('mouseScroll', mouseEvent);
}
this._tempMouseWheelDeltaY = 0;
var keyEvent = {
game: this,
input: this.input,
};
if (this.input.keysDown.size > 0)
this.emit('keyDown', keyEvent);
if (this.input.keysUp.size > 0)
this.emit('keyUp', keyEvent);
// Update
for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) {
var gameObject = _a[_i];
if (!gameObject.enabled)
continue;
try {
gameObject.Update(tickEvent);
}
catch (e) {
console.warn("Problem with executing Update @ ".concat(gameObject.constructor.name, " "), gameObject);
if (e instanceof Error)
console.error(e.message);
}
}
// Fixed Update
for (var _b = 0, _c = this.gameObjects; _b < _c.length; _b++) {
var gameObject = _c[_b];
if (!gameObject.enabled)
continue;
try {
gameObject.FixedUpdate(tickEvent);
}
catch (e) {
console.warn("Problem with executing FixedUpdate @ ".concat(gameObject.constructor.name, " "), gameObject);
if (e instanceof Error)
console.error(e.message);
}
}
//
this.input.keysDown.clear();
this.input.keysUp.clear();
};
Game.prototype._gameLoopDraw = function () {
if (this.isNeedToUpdate || this.gameSettings.drawAlways) {
this.renderer.clearFrame();
var drawEvent = {
renderer: this.renderer,
game: this,
};
this.emit('draw', drawEvent);
for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) {
var gameObject = _a[_i];
if (!gameObject.enabled)
continue;
if (!(gameObject instanceof DrawableGameObject_1.DrawableGameObject))
continue;
try {
gameObject.OnDraw(drawEvent);
if (gameObject instanceof ClickableGameObject_1.ClickableGameObject) {
var clickable = gameObject;
this.renderer.drawHitbox(clickable);
}
}
catch (e) {
console.warn("Problem with executing draw @ ".concat(gameObject.constructor.name, " "), gameObject);
if (e instanceof Error)
console.error(e.message);
}
}
this.isNeedToUpdate = false;
}
};
/**
* Tells the game it needs to be redrawn.
* Call it when some visible objects has been changed.
*/
Game.prototype.Update = function () {
this.isNeedToUpdate = true;
};
/**
* Starts new game loop
*/
Game.prototype.StartGameLoop = function () {
var _this = this;
if (this._isPlaying) {
console.warn('Cannot start new game loop when the game loop exists.');
return;
}
window.requestAnimationFrame(function () {
window.requestAnimationFrame(_this._gameLoop.bind(_this));
_this._isPlaying = true;
});
};
/**
* WARNING! Stops the game loop
*/
Game.prototype.StopGameLoop = function () {
console.warn('Stopped the game loop! Restarting the game loop will cause a time skip.');
this._isPlaying = false;
};
/**
* Starts the game loop and emit `start` ({@link GameEvent}) event.
*/
Game.prototype.Start = function () {
this.StartGameLoop();
this.emit('start', { game: this });
};
/**
* Starts loading game resources and returns promise.
* @method
* @returns The Promise
* @example
* game.LoadGameAndStart().then((e) => {
* console.log('Game sucessfully loaded!');
* }).catch((error) => {
* console.error('Error');
* }).finally(() => {
* console.log("Finally");
* });
*/
Game.prototype.LoadGameAndStart = function () {
var _this = this;
return new Promise(function (resolve, reject) {
if (_this._isPlaying)
reject(new Error('Cannot load and start game because the game loop currently exists!'));
var whenLoaded = function () {
_this.Start();
resolve({ game: _this });
};
_this.on('loadAllResources', whenLoaded);
try {
setTimeout(function () {
_this.LoadAllResources();
}, 1);
}
catch (error) {
reject(error);
}
});
};
/**
* Loads resource with resource manager.
* @method
* @param type - The resource type
* @param uid - The resource unique key
* @param path - The resource path
* @example
* game.LoadResource('image', 'player', './resources/img/player.png');
*/
Game.prototype.LoadResource = function (type, uid, path) {
if (type === 'image') {
this.resources.set(uid, {
uid: uid,
type: type,
path: path,
object: undefined,
loaded: false,
});
}
else {
throw new Error('Unknown resource type.');
}
this.LoadAllResources();
};
/**
* Creates {@link HTMLImageElement} from path.
* @method
* @param path - The image path
* @returns The image element
* @example
* game.CreateImage('./resources/img/player.png');
*/
Game.prototype.CreateImage = function (path) {
var img = new Image();
img.src = path;
return img;
};
/**
* Gets the resource by unique resource key.
* @method
* @param uid - The unique resource key.
* @returns The resource
* @example
* const resource = game.GetResource('player');
*/
Game.prototype.GetResource = function (uid) {
var res = this.resources.get(uid);
if (res === undefined)
throw new Error('Resource not loaded!');
return res;
};
/**
* Gets the image resource by unique resource key.
* @method
* @param uid - The unique resource key.
* @returns The image
* @example
* const image = game.GetImage('player');
*/
Game.prototype.GetImage = function (uid) {
var res = this.GetResource(uid);
if (res.type !== 'image')
return undefined;
if (!res.loaded)
return undefined;
return res.object;
};
/**
* Starts loading all resources which is not loaded. Emits {@link GameEvent} at `loadAllResources` channel.
*/
Game.prototype.LoadAllResources = function () {
var _this = this;
if (this.resources.size === 0) {
this._signals.emit('loadAllResources', { game: this });
this._isLoadedAllResources = true;
return;
}
var resourcesCount = 0;
var resourceLoaded = function () {
resourcesCount -= 1;
if (resourcesCount === 0) {
if (!_this._isLoadedAllResources) {
_this._signals.emit('loadAllResources', { game: _this });
_this._isLoadedAllResources = true;
}
}
};
this.resources.forEach(function (resource) {
if (resource.loaded)
return;
if (resource.type === 'image') {
// Loading image
var image_1 = new Image();
image_1.src = resource.path;
image_1.addEventListener('error', function () {
resource.loaded = false;
console.error("Cannot load ".concat(resource.uid, " resource."));
resourceLoaded();
});
image_1.addEventListener('load', function () {
resource.object = image_1;
resource.loaded = true;
resourceLoaded();
});
}
else {
// Unknown type
console.warn("Type \"".concat(resource.type, "\" is unknown resources type."));
return;
}
resourcesCount += 1;
});
};
/**
* Sorts all game objects by sorting order property.
*/
Game.prototype.SortGameObjects = function () {
this.gameObjects.sort(function (a, b) { return a.sortingOrder - b.sortingOrder; });
};
/**
* Adds unique game object to game.
* @method
* @param gameObject - The unique game object
* @returns The added game object
* @example
* game.AddGameObject(new JSGL.GameObject());
*/
Game.prototype.AddGameObject = function (gameObject) {
if (!(gameObject instanceof GameObject_1.GameObject))
throw new Error('Cannot add not GameObject!');
for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) {
var otherGameObjects = _a[_i];
if (gameObject.id === otherGameObjects.id)
throw new Error('Cannot add this same GameObject!');
}
if (gameObject.name === undefined)
gameObject.name = gameObject.constructor.name;
this.gameObjects.push(gameObject);
this.SortGameObjects();
var gameObjectSpawnEvent = {
game: this,
gameObjectId: gameObject.id,
};
gameObject.Start(gameObjectSpawnEvent);
this.emit('spawnedGameObject', gameObjectSpawnEvent);
return gameObject;
};
/**
* Destroys existed game object by reference
* @method
* @param gameObject - The game object reference
* @example
* const gameObject = ...
* game.DestroyGameObjectByRef(gameObject);
*/
Game.prototype.DestroyGameObjectByRef = function (gameObject) {
if (!(gameObject instanceof GameObject_1.GameObject))
throw new Error('Param gameObject must be an GameObject object!');
var index = this.gameObjects.indexOf(gameObject);
if (index === -1)
return;
var onDestroyEvent = {
game: this,
};
gameObject.Destroy(onDestroyEvent);
this.gameObjects.splice(index, 1);
this.SortGameObjects();
};
/**
* Destroys existed game object by unique id
* @method
* @param id - The unique id
* @example
* game.DestroyGameObjectById('51870300-4187221613-3012590175-3461657014');
*/
Game.prototype.DestroyGameObjectById = function (id) {
if (typeof id !== 'string')
throw new Error('Param id must be string!');
var gameObject = this.GetGameObjectById(id);
if (gameObject === undefined)
return;
var index = this.gameObjects.findIndex(function (element) { return element.id === gameObject.id; });
if (index === -1)
return;
var onDestroyEvent = {
game: this,
};
this.gameObjects[index].Destroy(onDestroyEvent);
this.gameObjects.splice(index, 1);
this.SortGameObjects();
};
/**
* Destroys existed game object by index in game objects array
* @method
* @param index - The index
* @example
* game.DestroyGameObjectByIndex(0);
*/
Game.prototype.DestroyGameObjectByIndex = function (index) {
if (typeof index !== 'number')
throw new Error('Param index must be number!');
if (index < 0)
throw new Error('Index cannot be lower than 0!');
if (index >= this.gameObjects.length)
throw new Error('Index cannot be bigger than maximum index!');
var onDestroyEvent = {
game: this,
};
this.gameObjects[index].Destroy(onDestroyEvent);
this.gameObjects.splice(index, 1);
this.SortGameObjects();
};
/**
* Gets all existed game objects with type equal to param.
* @param type - The type
* @returns Array of selected game objects
* @example
* game.GetGameObjectsByType(JSGL.Shape);
*/
Game.prototype.GetGameObjectsByType = function (type) {
if (typeof type != 'function' || !(type instanceof Object))
throw new Error('Type must be an object!');
var result = [];
for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) {
var gameObject = _a[_i];
if (gameObject instanceof type)
result.push(gameObject);
}
return result;
};
/**
* Gets all existed game objects by given name.
* @method
* @param name - The name.
* @returns Array of game objects with given name.
* @example
* game.GetGameObjectsByName('exampleName');
*/
Game.prototype.GetGameObjectsByName = function (name) {
if (typeof name != 'string')
throw new Error('Name of object must be string!');
var result = [];
for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) {
var gameObject = _a[_i];
if (gameObject.name === name)
result.push(gameObject);
}
return result;
};
/**
* Gets all existed game objects by given tag.
* @method
* @param tag The - tag.
* @returns The array of game objects with given tag.
* @example
* game.GetGameObjectsByTag('exampleTag');
*/
Game.prototype.GetGameObjectsByTag = function (tag) {
if (typeof tag != 'string')
throw new Error('Name of object must be string!');
var result = [];
for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) {
var gameObject = _a[_i];
if (gameObject.tag === tag)
result.push(gameObject);
}
return result;
};
/**
* Gets game object by unique id.
* @method
* @param id - The unique id
* @returns The game object
* @example
* game.GetGameObjectById('51870300-4187221613-3012590175-3461657014');
*/
Game.prototype.GetGameObjectById = function (id) {
if (typeof id !== 'string')
throw new Error('Param id must be a string!');
for (var _i = 0, _a = this.gameObjects; _i < _a.length; _i++) {
var gameObject = _a[_i];
if (gameObject.id === id)
return gameObject;
}
return undefined;
};
// Sounds Management
/**
* Plays sound at the page.
* @method
* @param path - The path to sound file
* @param loop - is looped?
* @param volume - The volume decimal midpoint
* @example
* game.PlaySound('./resources/sounds/death.mp3', false, 0.8);
*/
Game.prototype.PlaySound = function (path, loop, volume) {
if (loop === void 0) { loop = false; }
if (volume === void 0) { volume = 1; }
var audio = new Audio();
var src = document.createElement('source');
src.type = 'audio/mpeg';
src.src = path;
audio.appendChild(src);
audio.loop = loop;
audio.volume = volume;
audio.play().then();
};
Object.defineProperty(Game.prototype, "mousePos", {
/**
* Use `input.mouseWorldPosition` instead.
* @deprecated since version 1.0.8
*/
get: function () {
return this.input.mouseWorldPosition;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Game.prototype, "mouseClientPos", {
/**
* Use `input.mouseClientPosition` instead.
* @deprecated since version 1.0.8
*/
get: function () {
return this.input.mouseClientPosition;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Game.prototype, "mousePrecisePos", {
/**
* Use `input.mousePreciseWorldPosition` instead.
* @deprecated since version 1.0.8
*/
get: function () {
return this.input.mousePreciseWorldPosition;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Game.prototype, "isMousePrimaryButtonDown", {
/**
* Use `input.isMousePrimaryButtonDown` instead.
* @deprecated since version 1.0.8
*/
get: function () {
return this.input.isMousePrimaryButtonDown;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Game.prototype, "mouseHoveredGameObject", {
get: function () {
for (var i = this.gameObjects.length - 1; i >= 0; i -= 1) {
var gameObject = this.gameObjects[i];
if (!gameObject.enabled)
continue;
if (!(gameObject instanceof ClickableGameObject_1.ClickableGameObject))
continue;
var clickableObj = gameObject;
if (clickableObj.ignoreRaycast)
continue;
if (clickableObj.transform.scale.x <= 0 ||
clickableObj.transform.scale.y <= 0)
continue;
var minX = clickableObj.transform.position.x;
var maxX = clickableObj.transform.position.x + clickableObj.transform.scale.x;
var minY = clickableObj.transform.position.y;
var maxY = clickableObj.transform.position.y + clickableObj.transform.scale.y;
var isInRange = (0, MathUtils_1.IsInRange)(this.input.mousePreciseWorldPosition.x, minX, maxX) &&
(0, MathUtils_1.IsInRange)(this.input.mousePreciseWorldPosition.y, minY, maxY);
if (isInRange) {
return clickableObj;
}
}
return undefined;
},
enumerable: false,
configurable: true
});
Game.prototype.constructMouseEvent = function () {
var gameMouseEvent = {
game: this,
mousePos: this.input.mouseWorldPosition,
mousePrecisePos: this.input.mousePreciseWorldPosition,
mouseClientPos: this.input.mouseClientPosition,
isMouseDown: this.input.isMousePrimaryButtonDown,
mouseWorldPosition: this.input.mouseWorldPosition,
mousePreciseWorldPositon: this.input.mousePreciseWorldPosition,
mouseLocalPosition: this.input.mouseLocalPosition,
mousePreciseLocalPosition: this.input.mousePreciseLocalPosition,
mouseScrollDelta: this.input.mouseScrollDelta,
};
return gameMouseEvent;
};
Game.prototype.mouseMoveHandler = function (game, event) {
game.input.mouseClientPosition = new Vector2_1.Vector2(event.offsetX, event.offsetY);
var worldPosition = game.input.mouseClientPosition.clone();
var gridSize = game.renderer.gridSize;
worldPosition.divide(gridSize);
var localPosition = worldPosition.clone();
game.input.mousePreciseLocalPosition = localPosition.clone().floor();
game.input.mouseLocalPosition = localPosition.clone().floor();
worldPosition.add(this.canvasViewOffset);
game.input.mousePreciseWorldPosition = worldPosition.clone();
worldPosition.floor();
game.input.mouseWorldPosition = worldPosition;
var mouseEvent = game.constructMouseEvent();
game.emit('mouseMove', mouseEvent);
};
Game.prototype.mouseDownHandler = function (game) {
var gameMouseEvent = game.constructMouseEvent();
game.input.isMousePrimaryButtonDown = true;
var hoveredGameObject = this.mouseHoveredGameObject;
if (hoveredGameObject !== undefined)
hoveredGameObject.OnMouseDown(gameMouseEvent);
game.emit('mouseDown', gameMouseEvent);
};
Game.prototype.mouseUpHandler = function (game) {
var gameMouseEvent = game.constructMouseEvent();
game.input.isMousePrimaryButtonDown = false;
var hoveredGameObject = this.mouseHoveredGameObject;
if (hoveredGameObject !== undefined)
hoveredGameObject.OnMouseUp(gameMouseEvent);
game.emit('mouseUp', gameMouseEvent);
};
Game.prototype.mouseClickHandler = function (game) {
var gameMouseEvent = game.constructMouseEvent();
var hoveredGameObject = this.mouseHoveredGameObject;
if (hoveredGameObject !== undefined)
hoveredGameObject.OnMouseClick(gameMouseEvent);
game.emit('mouseClick', gameMouseEvent);
};
Game.prototype.mouseWheelHandler = function (game, event) {
event.preventDefault();
this._tempMouseWheelDeltaY = -event.deltaY;
};
Game.prototype.GetRandomPosition = function () {
return new Vector2_1.Vector2((0, MathUtils_1.floor)(Math.random() * this.grid.x + this.canvasViewOffset.x), (0, MathUtils_1.floor)(Math.random() * this.grid.y + this.canvasViewOffset.y));
};
Game.prototype.GetRandomPositionIn = function (min, max) {
return new Vector2_1.Vector2((0, MathUtils_1.RandomInRange)(min.x, max.x), (0, MathUtils_1.RandomInRange)(min.y, max.x));
};
return Game;
}());
exports.Game = Game;