diff --git a/resources/js/Config.js b/resources/js/Config.js index e6f0219..e9cc5dc 100644 --- a/resources/js/Config.js +++ b/resources/js/Config.js @@ -1,6 +1,6 @@ var Config = { GAZE_SERVER_URL: "ws://localhost:8001/gaze", - TARGET_FPS: 120, + TARGET_FPS: 60, SHOW_DEBUG_INFO: true, USE_MOUSE_INPUT_AS_FAKE_GAZE_DATA: true, USE_LOGGER: true, diff --git a/resources/js/game/Enemy.js b/resources/js/game/Enemy.js new file mode 100644 index 0000000..1711ea1 --- /dev/null +++ b/resources/js/game/Enemy.js @@ -0,0 +1,50 @@ +const DEFAULT_SPEED = 1, + DEFAULT_HEALTH = 100, + DEFAULT_WIDTH = 20, + DEFAULT_HEIGHT = 20, + DEFAULT_DAMAGE = 10, + DEFAULT_HIT_BOX_RADIUS = 30; + +class Enemy { + + constructor(x, y, direction) { + this.x = x; + this.y = y; + this.width = DEFAULT_WIDTH; + this.height = DEFAULT_HEIGHT; + this.speed = DEFAULT_SPEED; + this.health = DEFAULT_HEALTH; + this.damage = DEFAULT_DAMAGE; + this.direction = direction; + } + + update(target, adjustment) { + this.direction = Math.atan2(target.y - this.y, target.x - this.x); + this.x = this.x + Math.cos(this.direction) * this.speed * adjustment; + this.y = this.y + Math.sin(this.direction) * this.speed * adjustment; + } + + isHitBy(pos) { + let distance = this.distanceTo(pos); + if (distance <= DEFAULT_HIT_BOX_RADIUS) { + return true; + } + return false; + } + + distanceTo(target) { + let deltaX = Math.abs(this.x - target.x), + deltaY = Math.abs(this.y - target.y); + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); + } + + static createEnemy(area) { + let xPos = parseInt(Math.random() * area.width), + yPos = parseInt(Math.random() * area.height), + direction = Math.atan2(area.center.y - yPos, area.center.x - xPos); + return new Enemy(xPos, yPos, direction); + } + +} + +export default Enemy; \ No newline at end of file diff --git a/resources/js/game/GameConfig.js b/resources/js/game/GameConfig.js new file mode 100644 index 0000000..b4a3116 --- /dev/null +++ b/resources/js/game/GameConfig.js @@ -0,0 +1,19 @@ +var Config = { + MAX_GAZE_POINT_AGE: 500, + MAX_NUMBER_OF_ENEMIES: 10, + TARGET_RADIUS: 100, + DEFAULT_PLAYER_DAMAGE: 5, + ENEMEY_SPAWN_DELAY: 1000, + FPS_BUFFER_LENGTH: 50, + BACKGROUND_COLOR: "#333", + DEFAULT_ENEMEY_COLOR: "#dd3939", + DEFAULT_GAZE_POINT_RADIUS: 15, + DEFAULT_GAZE_POINT_COLOR: "#4cd494", + DEBUG_POSITION_OFFSET: 10, + DEBUG_FONT: "16px ShareTech", + DEBUG_COLOR: "#ffffff", +}; + +Object.freeze(Config); + +export default Config; \ No newline at end of file diff --git a/resources/js/game/GameManager.js b/resources/js/game/GameManager.js index 7ae9c49..7ee2184 100644 --- a/resources/js/game/GameManager.js +++ b/resources/js/game/GameManager.js @@ -1,14 +1,122 @@ +import Logger from "../utils/Logger.js"; +import Config from "./GameConfig.js"; +import Time from "../utils/Time.js"; import GameRenderer from "./GameRenderer.js"; +import Enemy from "./Enemy.js"; + +var gameLoop, + gameArea, + lastUpdate, + lastEnemySpawn, + options, + enemies, + gazePoints; + +function onTick() { + let now = Date.now(), + delta = now - lastUpdate, + adjustment = (Time.ONE_SECOND_IN_MS / options.frameRate) / delta; + updateEnemies(now, adjustment); + updateGazePoints(now); + GameRenderer.render(getCurrentState(now)); + lastUpdate = Date.now(); +} + +function getCurrentState(now) { + return { + delta: now - lastUpdate, + frameRate: options.frameRate, + enemies: enemies, + gazePoints: gazePoints, + debug: options.debug, + }; +} + +function updateEnemies(now, adjustment) { + let currentGazePosition = gazePoints[gazePoints.length - 1]; + if ((now - lastEnemySpawn) >= Config.ENEMEY_SPAWN_DELAY) { + if (enemies.length < Config.MAX_NUMBER_OF_ENEMIES) { + Logger.log("Add new enemy", "Game"); + enemies.push(Enemy.createEnemy(gameArea)); + lastEnemySpawn = now; + } + } + for (let i = enemies.length - 1; i >= 0; i--) { + // Update enemy's position + enemies[i].update(gameArea.center, adjustment); + // Check if enemy has hit target + if (enemies[i].distanceTo(gameArea.center) < Config.TARGET_RADIUS) { + onEnemyHitsTarget(enemies[i].damage); + enemies.splice(i, 1); + } + // Skip other checks if no gaze data is available + if (currentGazePosition === undefined) { + continue; + } + // Check if enemy was hit + if (enemies[i].isHitBy({ + x: currentGazePosition.targetX, + y: currentGazePosition.targetY, + })) { + // Update enemy's health + enemies[i].health -= Config.DEFAULT_PLAYER_DAMAGE; + // Remove enemy if destroyed + if (enemies[i].health <= 0) { + Logger.log(`Enemy destroyed by player`, "Game"); + enemies.splice(i, 1); + // TODO: Add exposion at last position + } + } + } +} + +function onEnemyHitsTarget(damage) { + Logger.log(`Enemy hit target for ${damage} dmg`, "Game"); +} + +function updateGazePoints(now) { + let lastIndexForRemoval = -1; + for (let i = 0; i < gazePoints.length; i++) { + gazePoints[i].age = now - gazePoints[i].createdAt; + gazePoints[i].inversedAge = 1 - (gazePoints[i].age / Config.MAX_GAZE_POINT_AGE); + if (gazePoints[i].age > Config.MAX_GAZE_POINT_AGE) { + lastIndexForRemoval = i; + } + } + if (lastIndexForRemoval !== -1) { + gazePoints.splice(0, lastIndexForRemoval + 1); + } +} class GameManager { - setOptions(options) { + constructor() { + enemies = []; + gazePoints = []; + lastEnemySpawn = 0; + } + + setOptions(gameOptions) { + options = gameOptions; + gameArea = { + width: options.width, + height: options.height, + center: { + x: options.width / 2, + y: options.height / 2, + }, + }; + } - } + start() { + GameRenderer.init(options.canvas, options.width, options.height, options.version); + lastUpdate = Date.now(); + gameLoop = setInterval(onTick, (Time.ONE_SECOND_IN_MS / options.frameRate)); + } - start() { - - } + addGazePoint(point) { + gazePoints.push(point); + } } diff --git a/resources/js/game/GameRenderer.js b/resources/js/game/GameRenderer.js index 6a55a9b..f6ff569 100644 --- a/resources/js/game/GameRenderer.js +++ b/resources/js/game/GameRenderer.js @@ -1,121 +1,98 @@ -const BACKGROUND_COLOR = "#333", - DEFAULT_GAZE_POINT_RADIUS = 15, - DEFAULT_GAZE_POINT_COLOR = "#3f0d76", - DEBUG_POSITION_OFFSET = 10, - DEBUG_FONT = "16px ShareTech", - DEBUG_COLOR = "#ffffff", - MAX_GAZE_POINT_AGE = 1000; +import Config from "./GameConfig.js"; +import Time from "../utils/Time.js"; -var lastUpdate, - lastDelta, - fpsBuffer, - gazePoints, - version, - debug, - frameRate, - canvas, +var canvas, context, - loop; + gameVersion, + fpsBuffer, + currentFPS; -function onTick() { - setDelta(); - updateGazePoints(); +function draw(state) { + updateFPS(state.delta); context.clearRect(0, 0, canvas.width, canvas.height); - drawGazePoints(); - if (debug === true) { + drawEnemies(state.enemies); + drawGazePoints(state.gazePoints); + if (state.debug === true) { drawDebugInfo(); } - setUpdateTime(); } -function setUpdateTime() { - lastUpdate = Date.now(); -} - -function setDelta() { - lastDelta = Date.now() - lastUpdate; +// Move fps callculation to manager since we need it there to use delta +function updateFPS(delta) { + let fps = Time.ONE_SECOND_IN_MS / delta; + fpsBuffer.push(fps); + if (fpsBuffer.length === Config.FPS_BUFFER_LENGTH) { + currentFPS = parseInt(fpsBuffer.reduce((a, b) => a + b, 0) / + fpsBuffer.length); + fpsBuffer.shift(); + } } -function addGazePoint(point) { - gazePoints.set(point.id, point); +function drawEnemies(enemies) { + context.fillStyle = Config.DEFAULT_ENEMEY_COLOR; + for (let i = 0; i < enemies.length; i++) { + drawSingleEnemey(enemies[i]); + } } -function removeGazePoint(point) { - gazePoints.delete(point.id); +function drawSingleEnemey(enemy) { + context.save(); + context.beginPath(); + context.translate(enemy.x, enemy.y + enemy.height/2); + context.rotate(enemy.direction); + context.moveTo(0,-enemy.height/2); + context.lineTo(-(enemy.width/2), enemy.height/2); + context.lineTo(enemy.width/2, enemy.height/2); + context.closePath(); + context.fill(); + context.restore(); } -function updateGazePoints() { - let now = Date.now(); - for (let item of gazePoints) { - let point = item[1]; - if (now - point.createdAt > MAX_GAZE_POINT_AGE) { - removeGazePoint(point); - } +function drawGazePoints(gazePoints) { + context.fillStyle = Config.DEFAULT_GAZE_POINT_COLOR; + for (let i = 0; i < gazePoints.length; i++) { + drawSingleGazePoint(gazePoints[i]); } } -function drawGazePoints() { - let now = Date.now(); - context.fillStyle = DEFAULT_GAZE_POINT_COLOR; - for (let item of gazePoints) { - let relativeAge = 1 - ((now - item[1].createdAt) / - MAX_GAZE_POINT_AGE); - context.save(); - context.globalAlpha = relativeAge; - context.beginPath(); - context.ellipse(item[1].targetX, item[1].targetY, - relativeAge * DEFAULT_GAZE_POINT_RADIUS, relativeAge * - DEFAULT_GAZE_POINT_RADIUS, Math.PI / 4, - 0, 2 * Math.PI); - context.fill(); - context.closePath(); - context.restore(); - } +function drawSingleGazePoint(gazePoint) { + let radius = gazePoint.inversedAge * Config.DEFAULT_GAZE_POINT_RADIUS; + context.save(); + context.globalAlpha = gazePoint.inversedAge; + context.beginPath(); + context.ellipse(gazePoint.targetX, gazePoint.targetY, radius, radius, Math.PI / + 4, 0, 2 * Math.PI); + context.closePath(); + context.fill(); + context.restore(); } function drawDebugInfo() { - let fps = parseInt(1000 / lastDelta), - averageFps; - fpsBuffer.push(fps); - if (fpsBuffer.length === frameRate) { - averageFps = parseInt(fpsBuffer.reduce((a, b) => a + b, 0) / fpsBuffer.length); - fpsBuffer.shift(); - } context.beginPath(); - context.font = DEBUG_FONT; - context.fillStyle = DEBUG_COLOR; - context.fillText(`${version} | ${averageFps}fps`, DEBUG_POSITION_OFFSET, canvas.height-DEBUG_POSITION_OFFSET); + context.font = Config.DEBUG_FONT; + context.fillStyle = Config.DEBUG_COLOR; + context.fillText(`${gameVersion} | ${currentFPS}fps`, Config.DEBUG_POSITION_OFFSET, + canvas.height - Config.DEBUG_POSITION_OFFSET); context.closePath(); } class GameRenderer { - constructor() { + init(gameCanvas, width, height, version) { fpsBuffer = []; - gazePoints = new Map(); - } - - start(options) { - canvas = options.canvas; - canvas.width = options.width; - canvas.height = options.height; - canvas.style.width = `${options.width}px`; - canvas.style.height = `${options.height}px`; + currentFPS = 0; + gameVersion = version; + canvas = gameCanvas; + canvas.width = width; + canvas.height = height; + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; context = canvas.getContext("2d", { alpha: false }); - canvas.style.backgroundColor = BACKGROUND_COLOR; - frameRate = options.frameRate; - version = options.version; - debug = options.debug; - setUpdateTime(); - loop = setInterval(onTick, (1000 / frameRate)); - } - - addGazePoint(point) { - addGazePoint(point); + canvas.style.backgroundColor = Config.BACKGROUND_COLOR; } - removeGazePoint(point) { - removeGazePoint(point); + render(state) { + draw(state); } } diff --git a/resources/js/game/StarGazer.js b/resources/js/game/StarGazer.js index 01168d4..4673c07 100644 --- a/resources/js/game/StarGazer.js +++ b/resources/js/game/StarGazer.js @@ -1,13 +1,12 @@ import Logger from "../utils/Logger.js"; import GameManager from "./GameManager.js"; -import GameRenderer from "./GameRenderer.js"; var canvas, provider; function init(options) { - Logger.log("Starting StarGazer game"); + Logger.log("Starting StarGazer game", "Game"); canvas = options.canvas; - GameRenderer.start({ + GameManager.setOptions({ canvas: canvas, frameRate: options.fps, version: options.version, @@ -15,6 +14,7 @@ function init(options) { width: options.width, height: options.height, }); + GameManager.start(); provider = options.gazeDataProvider; provider.addEventListener("dataavailable", onGazeUpdate); } @@ -23,7 +23,7 @@ function onGazeUpdate(event) { let gazePoint = event.data; gazePoint.linkTo(canvas); if (gazePoint.hasLink) { - GameRenderer.addGazePoint(gazePoint); + GameManager.addGazePoint(gazePoint); } } diff --git a/resources/js/utils/Logger.js b/resources/js/utils/Logger.js index c11278e..3124e2a 100644 --- a/resources/js/utils/Logger.js +++ b/resources/js/utils/Logger.js @@ -2,24 +2,37 @@ class Logger { - constructor() { - this.enabled = false; - } + constructor() { + this.enabled = false; + } - enable() { - this.enabled = true; - } + enable() { + this.enabled = true; + } - disable() { - this.enabled = false; - } + disable() { + this.enabled = false; + } - log(msg) { - if(this.enabled === false) { - return; - } - console.log(msg); - } + log(msg, label) { + let time = new Date(), + hours = time.getUTCHours(), + minutes = time.getUTCMinutes(), + seconds = time.getUTCSeconds(); + if (hours < 10) { + hours = "0" + hours; + } + if (minutes < 10) { + minutes = "0" + minutes; + } + if (seconds < 10) { + seconds = "0" + seconds; + } + if (this.enabled === false) { + return; + } + console.log(`[${label}]\t${hours}:${minutes}:${seconds} - ${msg}`); + } } diff --git a/resources/js/utils/Time.js b/resources/js/utils/Time.js new file mode 100644 index 0000000..3f11251 --- /dev/null +++ b/resources/js/utils/Time.js @@ -0,0 +1,7 @@ +var Time = { + ONE_SECOND_IN_MS: 1000, +}; + +Object.freeze(Time); + +export default Time; \ No newline at end of file