diff --git a/resources/css/default.css b/resources/css/default.css index 7e50b95..199eaf3 100644 --- a/resources/css/default.css +++ b/resources/css/default.css @@ -43,8 +43,8 @@ body { border-radius: 5px; border-width: 5px; border-style: solid; - background-color: #1b744a; - border-color: #aaffd8; + background-color: #3f0d76; + border-color: #d2ccf3; text-align: center; line-height: 10vh; font-size: 5vh; @@ -53,9 +53,9 @@ body { #startButton:hover { cursor: pointer; - background-color: #aaffd8; - border-color: #1b744a; - color: #1b744a; + background-color: #d2ccf3; + border-color: #3f0d76; + color: #3f0d76; } canvas { diff --git a/resources/js/Config.js b/resources/js/Config.js index c084139..e9cc5dc 100644 --- a/resources/js/Config.js +++ b/resources/js/Config.js @@ -1,11 +1,12 @@ var Config = { GAZE_SERVER_URL: "ws://localhost:8001/gaze", - TARGET_FPS: 120, - SHOW_FPS: true, + TARGET_FPS: 60, + SHOW_DEBUG_INFO: true, USE_MOUSE_INPUT_AS_FAKE_GAZE_DATA: true, USE_LOGGER: true, SCREEN_WIDTH: screen.width, SCREEN_HEIGHT: screen.height, + GAME_VERSION: 0.1, }; Object.freeze(Config); 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 45ba562..7ee2184 100644 --- a/resources/js/game/GameManager.js +++ b/resources/js/game/GameManager.js @@ -1,121 +1,123 @@ -const BACKGROUND_COLOR = "#333", - DEFAULT_GAZE_POINT_RADIUS = 15, - DEFAULT_GAZE_POINT_COLOR = "#ff007b", - FPS_FONT = "16px ShareTech", - FPS_COLOR = "#ffffff", - MAX_GAZE_POINT_AGE = 500; - -var lastUpdate, - lastDelta, - fpsBuffer = []; +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(); +} -class GameManger { +function getCurrentState(now) { + return { + delta: now - lastUpdate, + frameRate: options.frameRate, + enemies: enemies, + gazePoints: gazePoints, + debug: options.debug, + }; +} - constructor() { - this.gazePoints = new Map(); +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; + } } - - setCanvas(canvas) { - this.canvas = canvas; - this.context = this.canvas.getContext("2d", { alpha: false }); - this.canvas.style.backgroundColor = BACKGROUND_COLOR; + 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 + } + } } +} - setFrameRate(fps) { - this.fps = fps; - } +function onEnemyHitsTarget(damage) { + Logger.log(`Enemy hit target for ${damage} dmg`, "Game"); +} - showFPS() { - this.shouldDisplayFrameRate = true; +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; + } } - - hideFPS() { - this.shouldDisplayFrameRate = false; + if (lastIndexForRemoval !== -1) { + gazePoints.splice(0, lastIndexForRemoval + 1); } +} - setSize(width, height) { - this.canvas.width = width; - this.canvas.height = height; - this.canvas.style.width = `${width}px`; - this.canvas.style.height = `${height}px`; - } +class GameManager { - start() { - let targetFrameTime = 1000 / this.fps; - this.setUpdateTime(); - this.loop = setInterval(this.onTick.bind(this), targetFrameTime); + constructor() { + enemies = []; + gazePoints = []; + lastEnemySpawn = 0; } - setUpdateTime() { - lastUpdate = Date.now(); + setOptions(gameOptions) { + options = gameOptions; + gameArea = { + width: options.width, + height: options.height, + center: { + x: options.width / 2, + y: options.height / 2, + }, + }; } - setDelta() { - lastDelta = Date.now() - lastUpdate; + start() { + GameRenderer.init(options.canvas, options.width, options.height, options.version); + lastUpdate = Date.now(); + gameLoop = setInterval(onTick, (Time.ONE_SECOND_IN_MS / options.frameRate)); } addGazePoint(point) { - this.gazePoints.set(point.id, point); - } - - removeGazePoint(point) { - this.gazePoints.delete(point.id); - } - - onTick() { - this.setDelta(); - this.updateGazePoints(); - this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.drawGazePoints(); - if (this.shouldDisplayFrameRate === true) { - this.drawFrameRate(); - } - this.setUpdateTime(); - } - - updateGazePoints() { - let now = Date.now(); - for (let item of this.gazePoints) { - let point = item[1]; - if (now - point.createdAt > MAX_GAZE_POINT_AGE) { - this.removeGazePoint(point); - } - } - } - - drawGazePoints() { - let now = Date.now(); - this.context.fillStyle = DEFAULT_GAZE_POINT_COLOR; - for (let item of this.gazePoints) { - let relativeAge = 1 - ((now - item[1].createdAt) / - MAX_GAZE_POINT_AGE); - this.context.save(); - this.context.globalAlpha = relativeAge; - this.context.beginPath(); - this.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); - this.context.fill(); - this.context.closePath(); - this.context.restore(); - } - } - - drawFrameRate() { - let fps = parseInt(1000 / lastDelta), - averageFps; - fpsBuffer.push(fps); - if(fpsBuffer.length === this.fps) { - averageFps = parseInt(fpsBuffer.reduce((a,b) => a + b, 0) / fpsBuffer.length); - fpsBuffer.shift(); - } - this.context.beginPath(); - this.context.font = FPS_FONT; - this.context.fillStyle = FPS_COLOR; - this.context.fillText(`FPS: ${averageFps}`, 30, 30); - this.context.closePath(); + gazePoints.push(point); } } -export default GameManger; \ No newline at end of file +export default new GameManager(); \ No newline at end of file diff --git a/resources/js/game/GameRenderer.js b/resources/js/game/GameRenderer.js new file mode 100644 index 0000000..f6ff569 --- /dev/null +++ b/resources/js/game/GameRenderer.js @@ -0,0 +1,100 @@ +import Config from "./GameConfig.js"; +import Time from "../utils/Time.js"; + +var canvas, + context, + gameVersion, + fpsBuffer, + currentFPS; + +function draw(state) { + updateFPS(state.delta); + context.clearRect(0, 0, canvas.width, canvas.height); + drawEnemies(state.enemies); + drawGazePoints(state.gazePoints); + if (state.debug === true) { + drawDebugInfo(); + } +} + +// 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 drawEnemies(enemies) { + context.fillStyle = Config.DEFAULT_ENEMEY_COLOR; + for (let i = 0; i < enemies.length; i++) { + drawSingleEnemey(enemies[i]); + } +} + +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 drawGazePoints(gazePoints) { + context.fillStyle = Config.DEFAULT_GAZE_POINT_COLOR; + for (let i = 0; i < gazePoints.length; i++) { + drawSingleGazePoint(gazePoints[i]); + } +} + +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() { + context.beginPath(); + 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 { + + init(gameCanvas, width, height, version) { + fpsBuffer = []; + 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 = Config.BACKGROUND_COLOR; + } + + render(state) { + draw(state); + } + +} + +export default new GameRenderer(); \ No newline at end of file diff --git a/resources/js/game/StarGazer.js b/resources/js/game/StarGazer.js index c55e384..4673c07 100644 --- a/resources/js/game/StarGazer.js +++ b/resources/js/game/StarGazer.js @@ -1,20 +1,21 @@ import Logger from "../utils/Logger.js"; import GameManager from "./GameManager.js"; -var canvas, gm, provider; +var canvas, provider; -function init(config) { - Logger.log("Starting StarGazer game"); - canvas = config.canvas; - gm = new GameManager(); - gm.setCanvas(canvas); - gm.setFrameRate(config.fps); - if (config.showFPS === true) { - gm.showFPS(); - } - gm.setSize(config.width, config.height); - gm.start(); - provider = config.gazeDataProvider; +function init(options) { + Logger.log("Starting StarGazer game", "Game"); + canvas = options.canvas; + GameManager.setOptions({ + canvas: canvas, + frameRate: options.fps, + version: options.version, + debug: options.showDebug, + width: options.width, + height: options.height, + }); + GameManager.start(); + provider = options.gazeDataProvider; provider.addEventListener("dataavailable", onGazeUpdate); } @@ -22,7 +23,7 @@ function onGazeUpdate(event) { let gazePoint = event.data; gazePoint.linkTo(canvas); if (gazePoint.hasLink) { - gm.addGazePoint(gazePoint); + GameManager.addGazePoint(gazePoint); } } diff --git a/resources/js/index.js b/resources/js/index.js index 5eebe76..e31f8bb 100644 --- a/resources/js/index.js +++ b/resources/js/index.js @@ -20,7 +20,8 @@ function prepareGame() { StarGazer.init({ canvas: canvas, fps: Config.TARGET_FPS, - showFPS: Config.SHOW_FPS, + version: `Star Gazer, build ${Config.GAME_VERSION}`, + showDebug: Config.SHOW_DEBUG_INFO, width: Config.SCREEN_WIDTH, height: Config.SCREEN_HEIGHT, gazeDataProvider: dataProvider, 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