feat: Updated engine

Signed-off-by: Avior <f.bouillon@aptatio.com>
This commit is contained in:
Florian Bouillon 2022-11-24 12:03:00 +01:00
parent 8952b61651
commit 0a613fbdd6
Signed by: Florian Bouillon
GPG Key ID: E05B3A94178D3A7C
22 changed files with 1499 additions and 188 deletions

View File

@ -1,7 +1,8 @@
import GameEngine from 'GameEngine'
import Component2D from 'GameEngine/Component2D' import Component2D from 'GameEngine/Component2D'
import Vector2D from '../Vector2D' import Vector2D from '../Vector2D'
type BuiltinCollisionTypes = 'click' type BuiltinCollisionTypes = 'click' | 'pointerDown' | 'pointerUp'
export default class BoxCollider2D { export default class BoxCollider2D {
public constructor( public constructor(
@ -22,7 +23,12 @@ export default class BoxCollider2D {
public pos(): [Vector2D, Vector2D] { public pos(): [Vector2D, Vector2D] {
const scale = this.scale.multiply(this.component.scale) const scale = this.scale.multiply(this.component.scale)
const positionCenter = this.component.origin.sub( const positionCenter = GameEngine.getGameEngine().currentScene?.position.sum(this.component.origin.sub(
new Vector2D(
this.component.position.x,
this.component.position.y
)
)) ?? this.component.origin.sub(
new Vector2D( new Vector2D(
this.component.position.x, this.component.position.x,
this.component.position.y this.component.position.y
@ -31,12 +37,22 @@ export default class BoxCollider2D {
const center = this.center.sum(positionCenter) const center = this.center.sum(positionCenter)
return [new Vector2D( return [new Vector2D(
center.x - scale.x / 2, center.x,
center.y - scale.y / 2 center.y
), ),
new Vector2D( new Vector2D(
center.x + scale.x / 2, center.x + scale.x,
center.y + scale.y / 2 center.y + scale.y
)] )]
} }
public collideWith(collider: BoxCollider2D) {
const selfPos = this.pos()
const otherPos = collider.pos()
return selfPos[1].x >= otherPos[0].x && // self bottom higher than other top
selfPos[0].x <= otherPos[1].x &&
selfPos[1].y >= otherPos[0].y &&
selfPos[0].y <= otherPos[1].y
}
} }

View File

@ -4,6 +4,7 @@ import BoxCollider2D from '../Collision/BoxCollider2D'
import Vector2D from '../Vector2D' import Vector2D from '../Vector2D'
export default class ColliderDebugger extends Component2D { export default class ColliderDebugger extends Component2D {
public readonly name = 'ColliderDebugger'
public constructor(component: Component2D, collider: BoxCollider2D) { public constructor(component: Component2D, collider: BoxCollider2D) {
super() super()
this.collider = collider this.collider = collider

View File

@ -1,14 +1,20 @@
import Component2D from 'GameEngine/Component2D' import Component2D from 'GameEngine/Component2D'
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
import Vector2D from '../Vector2D' import Vector2D from '../Vector2D'
import PointDebugger from './PointDebugger'
export default class ComponentDebug extends Component2D { export default class ComponentDebug extends Component2D {
public readonly name = 'ComponentDebug'
public constructor(component: Component2D) { public constructor(component: Component2D) {
super() super()
this.position = component.position this.position = new Vector2D(0, 0)
this.origin = component.origin // this.origin = component.origin
this.scale = new Vector2D(.1, .1) this.scale = component.scale
console.log('Position of the origin point', this.position) console.log('Position of the origin point', this.position)
this.renderer = new RectRenderer(this, {material: 'red'}) // this.renderer = new RectRenderer(this, {material: 'red'})
this.childs = [
new PointDebugger(new Vector2D(0, 0), 'aqua'),
new PointDebugger(this.origin, 'green'),
new PointDebugger(component.position.sum(component.scale), 'aqua')
]
} }
} }

View File

@ -3,12 +3,13 @@ import RectRenderer from 'GameEngine/Renderer/RectRenderer'
import Vector2D from '../Vector2D' import Vector2D from '../Vector2D'
export default class PointDebugger extends Component2D { export default class PointDebugger extends Component2D {
public constructor(point: Vector2D) { public readonly name = 'PointDebugger'
public constructor(point: Vector2D, color = 'red') {
super() super()
this.scale = new Vector2D(.1, .1) this.scale = new Vector2D(1, 1)
this.position = point this.position = point
console.log('Debugging point at location', point) // console.log('Debugging point at location', point)
// this.origin = component.origin // this.origin = component.origin
this.renderer = new RectRenderer(this, {material: 'red'}) this.renderer = new RectRenderer(this, {material: color})
} }
} }

View File

@ -1,9 +1,10 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import Component2D, { ComponentState } from 'GameEngine/Component2D' import Component2D from 'GameEngine/Component2D'
import RectRenderer from 'GameEngine/Renderer/RectRenderer' import RectRenderer from 'GameEngine/Renderer/RectRenderer'
import Vector2D from '../Vector2D' import Vector2D from '../Vector2D'
export default class TilingDebugger extends Component2D { export default class TilingDebugger extends Component2D {
public readonly name = 'TilingDebugger'
public constructor() { public constructor() {
super() super()
for (let i0 = 0; i0 < 10; i0++) { for (let i0 = 0; i0 < 10; i0++) {
@ -22,6 +23,7 @@ export default class TilingDebugger extends Component2D {
} }
class CaseDebugger extends Component2D { class CaseDebugger extends Component2D {
public readonly name = 'CaseDebugger'
public renderer: RectRenderer = new RectRenderer(this, {stroke: 'black'}) public renderer: RectRenderer = new RectRenderer(this, {stroke: 'black'})
public constructor(pos: Vector2D) { public constructor(pos: Vector2D) {
super() super()

View File

@ -1,30 +0,0 @@
/* eslint-disable no-underscore-dangle */
export default class Size2D {
private _width: number
private _height?: number
public constructor(
width: number,
height?: number
) {
this._width = width
this._height = height
}
public get width() {
return this._width
}
public set width(v: number) {
this._width = v
}
public get height() {
return this._height ?? this._width
}
public set height(v: number) {
this._height = v
}
}

View File

@ -1,9 +1,20 @@
/* eslint-disable id-length */
export default class Vector2D { export default class Vector2D {
public constructor(
public x: number,
public y: number
) {}
public x: number
public y: number
public constructor(
x: number,
y?: number
) {
this.x = x
if (typeof y === 'number') {
this.y = y
} else {
this.y = x
}
}
public multiply(v: Vector2D): Vector2D { public multiply(v: Vector2D): Vector2D {
return new Vector2D( return new Vector2D(
@ -12,7 +23,10 @@ export default class Vector2D {
) )
} }
public sum(v: Vector2D): Vector2D { public sum(v: Vector2D | number, y?: number): Vector2D {
if (typeof v === 'number') {
return new Vector2D(this.x + v, this.y + (y ?? v))
}
return new Vector2D( return new Vector2D(
v.x + this.x, v.x + this.x,
v.y + this.y v.y + this.y
@ -26,10 +40,73 @@ export default class Vector2D {
) )
} }
public div(v: number): Vector2D {
return new Vector2D(
this.x / v,
this.y / v
)
}
public isIn(topLeft: Vector2D, bottomRight: Vector2D): boolean { public isIn(topLeft: Vector2D, bottomRight: Vector2D): boolean {
return this.x >= topLeft.x && return this.x >= topLeft.x &&
this.y >= topLeft.y && this.y >= topLeft.y &&
this.x <= bottomRight.x && this.x <= bottomRight.x &&
this.y <= bottomRight.y this.y <= bottomRight.y
} }
public decimalCount(nDecimal: number) {
return new Vector2D(
parseFloat(this.x.toFixed(nDecimal)),
parseFloat(this.y.toFixed(nDecimal))
)
}
/**
* return a new Vector with the this minus the other vector/x,y
*/
public minus(x: Vector2D | number, y?: number) {
return this.sum(
typeof x === 'number' ? -x : -x.x,
y? -y : typeof x === 'number' ? -x : -x.y,
)
}
public set(x: number | Vector2D, y?: number) {
if (typeof x === 'object') {
this.x = x.x
this.y = x.y
return this
}
this.x = x
if (!y) {
this.y = x
} else {
this.y = y
}
return this
}
public setY(y: number) {
this.y = y
return this
}
public setX(x: number) {
this.x = x
return this
}
public clone() {
return new Vector2D(this.x, this.y)
}
public toString() {
return `${this.x}, ${this.y}`
}
public equal(vector: Vector2D): boolean {
return vector.x === this.x && vector.y === this.y
}
} }

View File

@ -1,8 +1,22 @@
import Vector2D from './2D/Vector2D'
// eslint-disable-next-line no-shadow
export enum AssetStatus {
NOT_LOADED,
LOADING,
LOADED,
ERROR
}
/**
* Asset management Class
*/
export default class Asset { export default class Asset {
public static assets: Record<string, Asset> = {} public static assets: Record<string, Asset> = {}
public isLoaded = false public isLoaded = false
public status: AssetStatus = AssetStatus.NOT_LOADED
private image!: HTMLImageElement private image!: HTMLImageElement
@ -18,21 +32,37 @@ export default class Asset {
} }
public async load() { public async load() {
if (this.status === AssetStatus.LOADED || this.status === AssetStatus.LOADING) {
return
}
this.status = AssetStatus.LOADING
return new Promise<void>((res, rej) => { return new Promise<void>((res, rej) => {
this.image = new Image() this.image = new Image()
this.image.src = this.path this.image.src = this.path
this.image.onload = () => { this.image.onload = () => {
this.isLoaded = true this.isLoaded = true
this.status = AssetStatus.LOADED
res() res()
} }
this.image.onerror = rej this.image.onerror = () => {
console.error('Error loading image')
this.status = AssetStatus.ERROR
rej()
}
}) })
} }
public async get() { public get() {
if (!this.isLoaded) { if (this.status !== AssetStatus.LOADED) {
await this.load() throw new Error('Can\'t get an unloaded asset, please load it before')
} }
return this.image return this.image
} }
public size(): Vector2D {
if (this.status !== AssetStatus.LOADED) {
throw new Error('Can\'t get an unloaded asset, please load it before')
}
return new Vector2D(this.image.width, this.image.height)
}
} }

View File

@ -1,5 +0,0 @@
import Vector2D from './2D/Vector2D'
export default class Camera {
public topLeft = new Vector2D(0.5, 0.5)
}

View File

@ -1,35 +1,156 @@
import BoxCollider2D from './2D/Collision/BoxCollider2D' import BoxCollider2D from './2D/Collision/BoxCollider2D'
import Vector2D from './2D/Vector2D' import Vector2D from './2D/Vector2D'
import Renderer from './Renderer' import Renderer from './Renderer'
import Scene from './Scene'
export interface ComponentState { export interface ComponentState {
mouseHovering: boolean mouseHovering?: boolean
/** /**
* is it is collinding return the type of collision * is it is collinding return the type of collision
*/ */
isColliding?: string isColliding?: string
collideWith?: Array<Component2D>
scene?: Scene
isInitialized?: boolean
} }
export default abstract class Component2D { export type StaticComponent<
// eslint-disable-next-line @typescript-eslint/ban-types
T extends {} | void = {} | void
> =
new (params: T | undefined) => Component2D<T>
/**
* 2D Component
*/
export default abstract class Component2D<
// eslint-disable-next-line @typescript-eslint/ban-types
T extends {} | void = {} | void
> {
private static components = 0
public state: ComponentState = {}
/**
* unique number for the component
*
* maximum number of components is equal to Number.MAX_VALUE or 1.7976931348623157e+308
*/
public id: number = Component2D.components++
/**
* Indicate how the component is rendered
*
* @type {Renderer}
* @memberof Component2D
*/
public renderer?: Renderer public renderer?: Renderer
/**
* Component position relative to the parent position and to the component origin
*
* (see also: Component2D.origin)
*
* @type {Vector2D}
* @memberof Component2D
*/
public position: Vector2D = new Vector2D(0, 0) public position: Vector2D = new Vector2D(0, 0)
/**
* Component scale relative to 1 case size
*
* (see also: GameEngine.caseSize)
*
* @type {Vector2D}
* @memberof Component2D
*/
public scale: Vector2D = new Vector2D(1, 1) public scale: Vector2D = new Vector2D(1, 1)
/**
* Component collider for events
*
* @type {BoxCollider2D}
* @memberof Component2D
*/
public collider?: BoxCollider2D public collider?: BoxCollider2D
/** /**
* Change the origin point (default to middle) * Parent component of self is any
* this value is automatically set
*
* @type {Component2D}
* @memberof Component2D
*/
public parent?: Component2D
/**
* Change the origin point (default to top left)
*/ */
public origin: Vector2D = new Vector2D(0 , 0) public origin: Vector2D = new Vector2D(0 , 0)
/**
* Component Child Components
*
* @type {Array<Component2D>}
* @memberof Component2D
*/
public childs: Array<Component2D> = [] public childs: Array<Component2D> = []
/**
* Component in debug mode
* It will display more informations depending on the Collider and other items
*
* note: Does not apply to childs components
*
* @type {boolean}
* @memberof Component2D
*/
public debug?: boolean public debug?: boolean
protected params: T = {} as T
/**
* the name of the component
* used to detect components
*/
public abstract readonly name: string
public constructor(it: T) {
if (it) {
this.params = it
}
}
/**
* Function run when the component is initialized
*/
public init?(): Promise<void> | void public init?(): Promise<void> | void
/**
* Function run on each game ticks
* @param state the component state
*/
public update?(state: ComponentState): Promise<void> | void public update?(state: ComponentState): Promise<void> | void
public destroy?(): Promise<void> | void
public getAbsolutePosition(): Vector2D {
const realPosition = this.position.sum(
this.scale.multiply(this.origin)
)
if (!this.parent) {
return realPosition
}
return realPosition.sum(this.parent.getAbsolutePosition())
}
public setState(key: keyof ComponentState, value: any): void {
(this.state[key] as typeof value) = value
}
public updateParam(key: keyof T, value: any): void {
this.params[key] = value
}
} }

View File

@ -0,0 +1,45 @@
import GameEngine from 'GameEngine'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D from 'GameEngine/Component2D'
import Cursor from './Cursor'
/**
* Currently not working Camera implementation
*/
export default class Camera extends Component2D {
public name = 'Camera'
public position: Vector2D = new Vector2D(0)
public zoom = 1
public update() {
let needCursorUpdate = false
const scene = GameEngine.getGameEngine().currentScene
if (!scene) {
return
}
if (scene.scale !== this.zoom) {
scene.scale = this.zoom
needCursorUpdate = true
}
if (!scene.position.equal(this.position)) {
scene.position.set(this.position)
needCursorUpdate = true
}
if (needCursorUpdate) {
const cursor = scene.getComponents().find((it) => it.name === 'Cursor') as Cursor | undefined
cursor?.triggerUpdate()
}
}
/**
*
* @param value zoom with 1 being the base
*/
public setZoom(value: number) {
this.zoom = value
}
}

View File

@ -0,0 +1,162 @@
import GameEngine from 'GameEngine'
import BoxCollider2D from 'GameEngine/2D/Collision/BoxCollider2D'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D from 'GameEngine/Component2D'
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
export default class Cursor extends Component2D<{
debug?: boolean
} | void> {
public name = 'Cursor'
/**
* cursor position
*/
public position: Vector2D = new Vector2D(0,0)
/**
* is the cursor down
*/
public isDown = false
/**
* was the cursor down on previous frame
*/
public wasDown = false
public origin: Vector2D = new Vector2D(0)
public scale: Vector2D = new Vector2D(1)
public collider: BoxCollider2D = new BoxCollider2D(this)
/**
* event handled down event
* (can be updated between frames while the isDown/wasDown are updated with the other components)
*/
private eventDown = false
private oldPosition: [number, number] = [0, 0]
public init(): void | Promise<void> {
this.debug = this.params?.debug
const canvas = GameEngine.getGameEngine().canvas
canvas.addEventListener('mousemove', this.onMouseMove)
canvas.addEventListener('mousedown', this.onMouseDown)
canvas.addEventListener('touchmove', this.onTouchMove)
canvas.addEventListener('touchstart', this.onTouchStart)
// add up events on document so they are catched everywhere
document.addEventListener('mouseup', this.onMouseUp)
document.addEventListener('touchend', this.onTouchEnd)
if (this.debug) {
this.renderer = new RectRenderer(this, {material: 'blue'})
}
}
public destroy(): void | Promise<void> {
const canvas = GameEngine.getGameEngine().canvas
canvas.removeEventListener('mousemove', this.onMouseMove)
canvas.removeEventListener('mousedown', this.onMouseDown)
canvas.removeEventListener('touchmove', this.onTouchMove)
canvas.removeEventListener('touchstart', this.onTouchStart)
// add up events on document so they are catched everywhere
document.removeEventListener('mouseup', this.onMouseUp)
document.removeEventListener('touchend', this.onTouchEnd)
}
public update(): void | Promise<void> {
this.wasDown = this.isDown
this.isDown = this.eventDown
}
public triggerUpdate() {
this.updatePosition(...this.oldPosition)
}
private onMouseMove = (ev: MouseEvent) => {
// console.log('onMouseMove')
this.onMove(ev)
}
private onMouseDown = (ev: MouseEvent) => {
// console.log('onMouseDown')
this.onDown(ev)
}
private onMouseUp = (ev: MouseEvent) => {
// console.log('onMouseUp')
this.onUp(ev)
}
private onTouchMove = (ev: TouchEvent) => {
// console.log('onTouchMove')
this.onMove(ev.touches.item(0) ?? undefined)
}
private onTouchStart = (ev: TouchEvent) => {
// console.log('onTouchStart')
this.onDown(ev.touches.item(0) ?? undefined)
}
private onTouchEnd = (ev: TouchEvent) => {
// console.log('onTouchEnd')
this.onUp(ev.touches.item(0) ?? undefined)
}
/**
* Catch the onMove events
*/
private onMove(ev?: MouseEvent | Touch) {
if (ev) {
this.updatePosition(
ev.clientX ?? 0,
ev.clientY ?? 0
)
}
}
/**
* Catch the onDown events
*/
private onDown(ev?: MouseEvent | Touch) {
console.log('cursor down')
if (ev) {
this.updatePosition(
ev.clientX ?? 0,
ev.clientY ?? 0
)
}
this.eventDown = true
}
/**
* catch the onUp events
*/
private onUp(ev?: MouseEvent | Touch) {
console.log('cursor up')
if (ev) {
this.updatePosition(
ev.clientX ?? 0,
ev.clientY ?? 0
)
}
this.eventDown = false
}
private updatePosition(clientX: number, clientY: number) {
const ge = GameEngine.getGameEngine()
this.oldPosition = [clientX, clientY]
this.position.set(
((clientX ?? 0) + window.scrollX - ge.canvas.offsetLeft) /
(ge.currentScene?.scale ?? 1) * ge.getXCaseCount() /
ge.canvas.offsetWidth + (ge.currentScene?.position?.x ?? 0),
((clientY ?? 0) + window.scrollY - ge.canvas.offsetTop) /
(ge.currentScene?.scale ?? 1) * ge.getYCaseCount() /
ge.canvas.offsetHeight + (ge.currentScene?.position?.y ?? 0)
)
}
}

View File

@ -0,0 +1,52 @@
import GameEngine from 'GameEngine'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D from 'GameEngine/Component2D'
import TextRenderer from 'GameEngine/Renderer/TextRenderer'
export default class FPSCounter extends Component2D<{size?: number}> {
public name = 'FPSCounter'
public position: Vector2D = new Vector2D(1)
public renderer: TextRenderer = new TextRenderer(this, {text: 'loading...', color: 'black', stroke: 'white'})
private length = 1
private previousFrameTimes: Array<number> = []
private lastUpdate = window.performance.now() * 1000
public init() {
const fps = GameEngine.getGameEngine().options?.goalFramerate
if (!fps || fps < 1) {
this.length = 60
} else {
this.length = fps
}
if (this.params.size) {
this.renderer.size = this.params.size
}
}
public update() {
const lastFrame = GameEngine.getGameEngine().lastFrame
// this.renderer.color = 'black'
// if (!t) {return}
// console.log(this.previousFrameTimes, t)
const diff = lastFrame - this.lastUpdate
this.lastUpdate = lastFrame
this.previousFrameTimes.push(diff)
if (this.previousFrameTimes.length > this.length) {
this.previousFrameTimes.shift()
}
// const time = (this.previousFrameTimes.reduce((p, c) => p + c, 0)) / this.previousFrameTimes.length
const time = this.previousFrameTimes.slice().sort()[Math.round(this.previousFrameTimes.length / 2)]
// this.renderer.text = this.previousFrameTimes.join(', ')
if (time === 0) {
this.renderer.text = 'a lot'
} else if (time < 0) {
this.renderer.text = 'Loading...'
} else {
this.renderer.text = (1000 / time).toFixed(0)
}
}
}

View File

@ -0,0 +1,464 @@
import Listener from '@dzeio/listener'
export interface ControllerConnectionEvent {
controller: ControllerInterface
}
/**
* Event containing a button state change
*/
export interface ControllerButtonEvent {
/**
* The key as a text
*
* can be the keyboard/gamepad character
*/
key: string
/**
* Warning: prefer the use of 'key'
*/
keyId: number
/**
* Say if the button is pressed or not
*/
pressed: boolean
/**
* Reference to the controller
*/
controller: ControllerInterface
}
export interface ControllerAxisEvent {
/**
* axis id that changed
*/
axis: string
/**
* axis new value as a value between 0 and 1
*
* @see Controller.setControllerDeadZone to setup the controller dead zone globally
*/
value: number
/**
* Reference to the controller
*/
controller: ControllerInterface
}
export interface ControllerStates {
/**
* timestamp of new the input changed value last time
*/
lastChange: number
/**
* if the key being repeated
*
* mostly for internal usage
*/
repeat?: boolean
/**
* the current value of the input
*/
value: number
}
interface GamepadControllerInterface {
/**
* The Gamepad ID (mostly the name/brand with some unique identifier somewhere in the text)
*/
id: string
/**
* the type of controller
*
* can be 'gamepad' | 'keyboard'
*/
type: 'gamepad'
/**
* the Browser gamepad class
*/
gamepad: Gamepad
/**
* the gamepad mapping
*/
mapping: GamepadMapping
/**
* the input states
*/
states: Record<string | number, ControllerStates>
}
type ControllerInterface = {
/**
* the type of controller
*
* can be 'gamepad' | 'keyboard'
*/
type: 'keyboard'
/**
* The Gamepad ID (mostly the name/brand with some unique identifier somewhere in the text)
*/
id: 'keyboard'
} | GamepadControllerInterface
/**
* Gamepad mapping of IDs into human readable keys
*/
type GamepadMapping = Array<string | null>
/**
* Nintendo Switch specific controls
*/
const SwitchMapping: GamepadMapping = [
'b',
'a',
'x',
'y',
'screen',
'l',
'r',
'zl',
'zr',
'select',
'start',
'home',
'leftthumb',
'rightthumb'
]
/**
* Xbox specific controls
*/
const XboxMapping: GamepadMapping = [
'a',
'b',
null,
'x',
'y',
null,
'lb',
'rb',
null,
null,
null,
'start',
null,
'leftthumb',
'rightthumb'
]
/**
* Default mapping made as a base
*
* to add a new mapping use this url
* https://luser.github.io/gamepadtest/
*/
const DefaultMapping: GamepadMapping = [
'a',
'b',
'y',
'x',
'l',
'r',
'zl',
'zr',
'select',
'start'
]
/**
* This class allows you to get the controller states and update
* your application using the different Controllers like a keyboard or a Gamepad
*
* Please use `Controller.destroy()` at the end of your
* usage to finish the event listeners setup by the class
*/
export default class Controller extends Listener<{
/**
* event sent when a new connection is established
*/
connected: (ev: ControllerConnectionEvent) => void
/**
* event sent when a connection is broken
*/
disconnected: (ev: ControllerConnectionEvent) => void
/**
* event sent when the key is down
*/
keyDown: (ev: ControllerButtonEvent) => void
/**
* event sent once the key is up
*/
keyUp: (ev: ControllerButtonEvent) => void
/**
* Event sent when a key is pressed
*/
keyPress: (ev: ControllerButtonEvent) => void
/**
* Event sent when an axe is moving
*
* Event is sent continiously until it goes back to 0
*
* @see Controller.setControllerDeadZone to setup the controller dead zone globally
*/
axisMove: (ev: ControllerAxisEvent) => void
all: (eventName: string, ...args: Array<any>) => void
}> {
/**
* List of external gamepads
*/
private gamepads: Array<ControllerInterface> = []
/**
* Gamepad key/axes loop
*/
private doLoop = false
/**
* Controller axes dead zone
*/
private controllerDeadZone = 0.5
public constructor() {
super()
// Add the gamepad event listeners
window.addEventListener('gamepadconnected', this.onGamepadConnect)
window.addEventListener('gamepaddisconnected', this.onGamepadDisconnect)
// add the keyboard event listeners
document.addEventListener('keydown', this.keyboardKeyDownEvent)
document.addEventListener('keyup', this.keyboardKeyPressEvent)
document.addEventListener('keypress', this.keyboardKeyUpEvent)
}
/**
* set the controller (Gamepads only) axis dead zone
* @param value value between 0 and 1
*/
public setControllerDeadZone(value: number) {
if (value < 0 || value >= 1) {
throw new Error(`Controller Dead Zone Out of bound (must respect 0 < value (${value}) < 1)`)
}
this.controllerDeadZone = value
}
/**
* terminate the class
*/
public destroy() {
this.doLoop = false
this.gamepads = []
window.removeEventListener('gamepadconnected', this.onGamepadConnect)
window.removeEventListener('gamepaddisconnected', this.onGamepadDisconnect)
document.removeEventListener('keydown', this.keyboardKeyDownEvent)
document.removeEventListener('keyup', this.keyboardKeyPressEvent)
document.removeEventListener('keypress', this.keyboardKeyUpEvent)
}
/**
* Browser keyboard event handler
*/
private keyboardKeyDownEvent = (ev: KeyboardEvent) => {
this.emit('keyDown', {
key: ev.key,
keyId: ev.keyCode,
pressed: true,
controller: {
type: 'keyboard',
id: 'keyboard'
}
})
}
/**
* Browser keyboard event handler
*/
private keyboardKeyPressEvent = (ev: KeyboardEvent) => {
this.emit('keyPress', {
key: ev.key,
keyId: ev.keyCode,
pressed: true,
controller: {
type: 'keyboard',
id: 'keyboard'
}
})
}
/**
* Browser keyboard event handler
*/
private keyboardKeyUpEvent = (ev: KeyboardEvent) => {
this.emit('keyUp', {
key: ev.key,
keyId: ev.keyCode,
pressed: false,
controller: {
type: 'keyboard',
id: 'keyboard'
}
})
}
/**
* Handle gamepad disconnection
*/
private onGamepadDisconnect = (ev: GamepadEvent) => {
const index = this.gamepads.findIndex((it) => it.id === ev.gamepad.id)
if (index < 0) {
return
}
const gamepad = this.gamepads.splice(index, 1)[0]
console.log('Controller disconnected', gamepad.id)
if (this.gamepads.length === 0) {
this.doLoop = false
}
this.emit('disconnected', {
controller: gamepad
})
}
/**
* Handle gamepad connection
*/
private onGamepadConnect = (ev: GamepadEvent) => {
const gp = ev.gamepad
// create it's interface
const gamepad: ControllerInterface = {
type: 'gamepad',
id: gp.id,
gamepad: gp,
mapping: gp.id.includes('Switch') ? SwitchMapping : gp.id.includes('Xbox') ? XboxMapping : DefaultMapping,
states: {}
}
// add buttons to states
for (let idx = 0; idx < gp.buttons.length; idx++) {
const button = gp.buttons[idx]
gamepad.states['button-' + idx] = {
lastChange: new Date().getTime(),
value: button.pressed ? 1 : 0
}
}
// add axis to states
for (let idx = 0; idx < gp.axes.length; idx++) {
const axe = gp.axes[idx]
gamepad.states['axe-' + idx] = {
lastChange: new Date().getTime(),
value: axe
}
}
console.log('New Controller connected', gamepad.id)
// add it to the global gamepads list
this.gamepads.push(gamepad)
this.emit('connected', {
controller: gamepad
})
// start gamepads polling for new states
if (!this.doLoop) {
this.doLoop = true
this.update()
}
}
/**
* Polling to check if the gamepad has changes with it's buttons or not
*/
private update() {
const now = new Date().getTime()
// loop through every gamepads
for (let gIdx = 0; gIdx < this.gamepads.length; gIdx++) {
const gamepad = this.gamepads[gIdx]
if (gamepad.type !== 'gamepad') {
continue
}
// Chromium specific as gamepad.gamepad is not updated
gamepad.gamepad = navigator.getGamepads()[gIdx] ?? gamepad.gamepad
// loop through each buttons
for (let idx = 0; idx < gamepad.gamepad.buttons.length; idx++) {
const button = gamepad.gamepad.buttons[idx]
const gs = gamepad.states['button-' + idx]
const repeatedPress = gs.repeat || gs.lastChange + 300 < now
// handle state change or press repetition
if (button.pressed !== !!gs.value || button.pressed && repeatedPress) {
if (button.pressed && gs.value && repeatedPress) {
gs.repeat = true
} else if (!button.pressed) {
gs.repeat = false
}
// send keypress event once
if (button.pressed && !gs.value) {
this.emit('keyPress', {
key: gamepad.mapping[idx] ?? idx.toString(),
keyId: idx,
pressed: button.pressed,
controller: gamepad
})
}
// send keydown/keyup event
gs.lastChange = now
gamepad.states['button-' + idx].value = button.pressed ? 1 : 0
this.emit(button.pressed ? 'keyDown' : 'keyUp', {
key: gamepad.mapping[idx] ?? idx.toString(),
keyId: idx,
pressed: button.pressed,
controller: gamepad
})
}
}
// loop through each axises
for (let idx = 0; idx < gamepad.gamepad.axes.length; idx++) {
let axe = gamepad.gamepad.axes[idx]
if (Math.abs(axe) < this.controllerDeadZone) {
axe = 0
}
// emit event when value is not a 0
if (axe !== gamepad.states['axe-' + idx].value || axe !== 0) {
gamepad.states['axe-' + idx].value = axe
this.emit('axisMove', {
axis: idx.toString(),
value: axe,
controller: gamepad
})
}
}
}
// ask for new loop when available
if (this.doLoop) {
requestAnimationFrame(() => this.update())
}
}
}

View File

@ -0,0 +1,69 @@
import { objectLoop } from '@dzeio/object-util'
import GameEngine from 'GameEngine'
import Asset, { AssetStatus } from 'GameEngine/Asset'
import Component2D from 'GameEngine/Component2D'
import Renderer from '.'
interface Params {
asset?: Asset
stream?: boolean
debug?: boolean
}
/**
* TODO: Add origin support
*/
export default class ImageRenderer extends Renderer implements Params {
public asset?: Asset
public stream = true
public debug = false
public constructor(component: Component2D, params?: Params) {
super(component)
objectLoop(params ?? {}, (value, key) => {this[key as keyof Params] = value})
}
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
if (!this.asset) {
return
}
if (this.asset.status !== AssetStatus.LOADED) {
if (this.stream) {
// load asset but do not stop threads
this.asset.load()
return
} else {
await this.asset.load()
}
}
if (this.asset.status === AssetStatus.LOADING && this.stream) {
return
}
const globalScale = ge.currentScene?.scale ?? 1
const size = this.asset.size()
const position = this.getPosition()
const final: [number, number, number, number] = [
position.x * ge.caseSize.x * globalScale,
position.y * ge.caseSize.y * globalScale,
(this.component.scale.x ?? ge.caseSize.x) * ge.caseSize.x * globalScale,
(this.component.scale.y ?? ge.caseSize.y) * ge.caseSize.y * globalScale
]
if (this.debug || this.component.debug) {
ctx.fillStyle = 'red'
ctx.fillRect(...final)
}
ctx.drawImage(
this.asset.get(),
0,
0,
size.x,
size.y,
...final
)
}
}

View File

@ -1,51 +1,60 @@
import { objectLoop } from '@dzeio/object-util' import { objectLoop } from '@dzeio/object-util'
import GameEngine from 'GameEngine' import GameEngine from 'GameEngine'
import Asset from 'GameEngine/Asset'
import Component2D from 'GameEngine/Component2D' import Component2D from 'GameEngine/Component2D'
import Renderer from '.' import Renderer from '.'
interface Params { interface Params {
material?: string | Asset material?: string
stroke?: string stroke?: string | {color: string, width: number}
debug?: boolean
} }
export default class RectRenderer extends Renderer implements Partial<Params> { export default class RectRenderer extends Renderer implements Params {
public material?: string | Asset public material?: string
public stroke?: string public stroke?: string | {color: string, width: number}
public debug?: boolean | undefined
public constructor(component: Component2D, params?: Params) { public constructor(component: Component2D, params?: Params) {
super(component) super(component)
objectLoop(params ?? {}, (v, k) => {this[k as 'material'] = v}) objectLoop(params ?? {}, (value, key) => {this[key as 'material'] = value})
} }
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) { public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
const position = this.getPosition() const position = this.getPosition()
const globalScale = ge.currentScene?.scale ?? 1
const item: [number, number, number, number] = [ const item: [number, number, number, number] = [
// source x // source x
// 0 - 1.5 - -1.5 position.x * ge.caseSize.x * globalScale,
position.x * (ge.caseSize.x),
// source y // source y
position.y * (ge.caseSize.y), position.y * ge.caseSize.y * globalScale,
// source end X // size X
this.component.scale.x * (ge.caseSize.x), this.component.scale.x * ge.caseSize.x * globalScale,
// source end Y // size Y
this.component.scale.y * (ge.caseSize.y) this.component.scale.y * ge.caseSize.y * globalScale
] ]
if (this.material instanceof Asset) {
ctx.drawImage(
await this.material.get(),
...item
)
return
}
if (this.material) { if (this.material) {
ctx.fillStyle = this.material ctx.fillStyle = this.material
ctx.fillRect(...item) ctx.fillRect(...item)
} }
if (this.stroke) { if (this.stroke) {
ctx.strokeStyle = this.stroke if (typeof this.stroke === 'string') {
ctx.strokeStyle = this.stroke
} else {
ctx.strokeStyle = this.stroke.color
ctx.lineWidth = this.stroke.width * (ge.currentScene?.scale ?? 1)
}
ctx.strokeRect(...item)
}
if (this.debug) {
if (typeof this.stroke === 'string') {
ctx.strokeStyle = this.stroke
} else {
ctx.strokeStyle = 'red'
ctx.lineWidth = 1 * (ge.currentScene?.scale ?? 1)
}
ctx.strokeRect(...item) ctx.strokeRect(...item)
} }
} }

View File

@ -0,0 +1,66 @@
import { objectLoop } from '@dzeio/object-util'
import GameEngine from 'GameEngine'
import Component2D from 'GameEngine/Component2D'
import Renderer from '.'
interface Params {
text?: string
size?: number
weight?: 'bold'
stroke?: string | {color: string, width: number}
color?: string
debug?: boolean
}
export default class TextRenderer extends Renderer implements Params {
public text?: string
public size?: number
public weight?: 'bold'
public color?: string
public stroke?: string | {color: string, width: number}
public debug?: boolean
public constructor(component: Component2D, params?: Params) {
super(component)
objectLoop(params ?? {}, (v, k) => {this[k as 'text'] = v})
}
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
const position = this.getPosition()
const globalScale = ge.currentScene?.scale ?? 1
const item: [number, number] = [
// source x
// 0 - 1.5 - -1.5
position.x * ge.caseSize.x * globalScale,
// source y
position.y * ge.caseSize.y * globalScale
]
const size = this.component.scale.y * ge.caseSize.y
// console.log
if (this.text) {
ctx.textBaseline = 'top'
ctx.textAlign = 'left'
ctx.font = `${this.weight ? `${this.weight} ` : ''}${(this.size ?? size) / 16 * ge.caseSize.x * globalScale * 3}px sans-serif`
if (this.color) {
ctx.fillStyle = this.color ?? 'black'
ctx.fillText(this.text, ...item)
}
if (this.stroke) {
if (typeof this.stroke === 'string') {
ctx.strokeStyle = this.stroke
ctx.lineWidth = ge.currentScene?.scale ?? 1
} else {
ctx.strokeStyle = this.stroke.color
ctx.lineWidth = this.stroke.width * (ge.currentScene?.scale ?? 1)
}
ctx.strokeText(this.text, ...item)
}
} else if (this.debug) {
console.warn('no text, no display')
}
}
}

View File

@ -1,6 +1,5 @@
import { objectLoop } from '@dzeio/object-util' import { objectLoop } from '@dzeio/object-util'
import GameEngine from 'GameEngine' import GameEngine from 'GameEngine'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D from 'GameEngine/Component2D' import Component2D from 'GameEngine/Component2D'
import Tileset from 'GameEngine/Tileset' import Tileset from 'GameEngine/Tileset'
import Renderer from '.' import Renderer from '.'
@ -20,7 +19,7 @@ export default class TileRenderer extends Renderer implements Params {
public constructor(component: Component2D, params?: Params) { public constructor(component: Component2D, params?: Params) {
super(component) super(component)
objectLoop(params ?? {}, (v, k) => {this[k as 'id'] = v}) objectLoop(params ?? {}, (value, key) => {this[key as 'id'] = value})
} }
public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) { public async render(ge: GameEngine, ctx: CanvasRenderingContext2D) {
@ -29,14 +28,15 @@ export default class TileRenderer extends Renderer implements Params {
} }
const {sx, sy} = this.tileset.getSourceData(this.id) const {sx, sy} = this.tileset.getSourceData(this.id)
const position = this.getPosition() const position = this.getPosition()
await this.tileset.asset.load()
ctx.drawImage( ctx.drawImage(
await this.tileset.asset.get(), this.tileset.asset.get(),
sx, sx,
sy, sy,
this.tileset.width(this.id), this.tileset.width(this.id),
this.tileset.height(this.id), this.tileset.height(this.id),
position.x * (ge.caseSize.x), position.x * ge.caseSize.x,
position.y * (ge.caseSize.y), position.y * ge.caseSize.y,
(this.component.scale.x ?? ge.caseSize.x) * ge.caseSize.x, (this.component.scale.x ?? ge.caseSize.x) * ge.caseSize.x,
(this.component.scale.y ?? ge.caseSize.y) * ge.caseSize.y (this.component.scale.y ?? ge.caseSize.y) * ge.caseSize.y
) )

View File

@ -10,11 +10,12 @@ export default abstract class Renderer {
protected getPosition(): Vector2D { protected getPosition(): Vector2D {
const ge = GameEngine.getGameEngine() const ge = GameEngine.getGameEngine()
const realPosition = ge.currentScene.camera.topLeft.sum(this.component.position) const realPosition = this.component.getAbsolutePosition().sum(
return new Vector2D( -(ge.currentScene?.position?.x ?? 0),
realPosition.x - this.component.scale.x / 2 - this.component.origin.x, -(ge.currentScene?.position?.y ?? 0)
realPosition.y - this.component.scale.y / 2 - this.component.origin.y
) )
return realPosition
} }
public abstract render(ge: GameEngine, ctx: CanvasRenderingContext2D): Promise<void> public abstract render(ge: GameEngine, ctx: CanvasRenderingContext2D): Promise<void>

View File

@ -1,7 +1,8 @@
import GameEngine from 'GameEngine' import GameEngine from 'GameEngine'
import AssetsManager from './Asset'
import Camera from './Camera'
import Component2D, { ComponentState } from './Component2D' import Component2D, { ComponentState } from './Component2D'
import BoxCollider2D from './2D/Collision/BoxCollider2D'
import Vector2D from './2D/Vector2D'
import { ComponentType } from 'react'
export default class Scene { export default class Scene {
public static scenes: Record<string, Scene> = {} public static scenes: Record<string, Scene> = {}
@ -9,11 +10,13 @@ export default class Scene {
public background?: string public background?: string
public id: string public id: string
public camera: Camera = new Camera() public position: Vector2D = new Vector2D(0)
public scale = 1
private components: Array<Component2D> = [] private components: Array<Component2D> = []
private componentsInitialized: Array<boolean> = []
private ge!: GameEngine private ge!: GameEngine
private hasClickedComponent: number | undefined
public constructor(sceneId: string) { public constructor(sceneId: string) {
Scene.scenes[sceneId] = this Scene.scenes[sceneId] = this
@ -21,69 +24,200 @@ export default class Scene {
} }
public addComponent(...cp: Array<Component2D>) { public addComponent(...cp: Array<Component2D>) {
this.componentsInitialized.push(...cp.map(() => false))
return this.components.push(...cp) return this.components.push(...cp)
} }
public getComponents(): Array<Component2D> {
return this.components
}
/**
* delete the component
* @param component the component
*/
public removeComponent(component: Component2D): Scene
/**
* delete the component by it's index
* @param component the component index
*/
public removeComponent(component: number | Component2D): Scene {
if (typeof component === 'number') {
this.components.splice(component, 1)
this.componentsInitialized.splice(component, 1)
return this
}
const index = this.components.findIndex((it) => it.id === component.id)
if (index > -1) {
this.components.splice(index, 1)
this.componentsInitialized.splice(index, 1)
}
return this
}
public setGameEngine(ge: GameEngine) { public setGameEngine(ge: GameEngine) {
this.ge = ge this.ge = ge
} }
public async init() { public async init() {
this.components.forEach((v) => { for await (const component of this.components) {
if (v.init) { await this.initComponent(component)
v.init() }
}
})
} }
// private count = 0
public async update() { public async update() {
for (const component of this.components) { // console.log('new scene frame', this.count++)
for await (const component of this.components) {
await this.initComponent(component)
await this.updateComponent(component) await this.updateComponent(component)
} }
for await (const component of this.components) {
await this.renderComponent(component)
}
} }
private async updateComponent(v: Component2D) { public async destroy() {
const debug = v.debug for await (const component of this.components) {
if (debug) { await this.destroyComponent(component)
console.log('Processing Component', v)
} }
const state: Partial<ComponentState> = {} }
// const width = (v.width() ?? 1) * this.ge.caseSize[0]
// const height = (v.height() ?? 1) * this.ge.caseSize[1] public checkColisions(component: Component2D, exclusion?: Array<Component2D>): Array<Component2D> {
if (v.collider && v.collider.type === 'click' && this.ge.cursor.isDown && !this.ge.cursor.wasDown) { const list: Array<Component2D> = []
if (v.collider.pointColliding(this.ge.cursor.position, 'click')) { if (component.collider instanceof BoxCollider2D) {
state.isColliding = 'click' const [topLeft, bottomRight] = component.collider.pos()
for (const otherComponent of this.components) {
if (
otherComponent === undefined ||
otherComponent.id === component.id ||
!(otherComponent.collider instanceof BoxCollider2D)
) {
continue
}
// Exclude components from being checked
if (exclusion?.find((it) => it.id === otherComponent.id)) {
continue
}
// Check for collision
const otherCollider = otherComponent.collider.pos()
if (
bottomRight.x > otherCollider[0].x && // self bottom higher than other top
topLeft.x < otherCollider[1].x &&
bottomRight.y > otherCollider[0].y &&
topLeft.y < otherCollider[1].y
) {
list.push(otherComponent)
}
} }
} }
// if (v.pos) { return list
// const ax = v.pos.x * this.ge.caseSize[0] }
// const ay = v.pos.y * this.ge.caseSize[1]
// state.mouseHovering = private async initComponent(component: Component2D) {
// this.ge.cursor.x >= ax && this.ge.cursor.x < (ax + width) && if (component.state.isInitialized) {
// this.ge.cursor.y >= ay && this.ge.cursor.y < (ay + height) return
// state.mouseClicking = state.mouseHovering && this.ge.cursor.isDown
// state.mouseClicked = state.mouseClicking && !this.ge.cursor.wasDown
// }
if (v.renderer) {
if (debug) {
console.log('Rendering Component', v)
}
// console.log('is rendering new element')
await v.renderer.render(this.ge, this.ge.ctx)
} }
if (v.update) { if (component.init) {
if (debug) { await component?.init()
console.log('Updating Component', v)
}
v.update(state as ComponentState)
} }
if (v.childs) { component.setState('isInitialized', true)
if (debug) {
console.log('Processing childs', v) if (component.childs) {
} for await (const child of component.childs) {
for (const child of v.childs) { await this.initComponent(child)
await this.updateComponent(child)
} }
} }
} }
/**
* Update a specific component
*
* note: It first update the childs THEN the component
*
* @param component the component to update
* @returns the list of components to exclude in collision check
*/
private async updateComponent(component: Component2D): Promise<Array<Component2D>> {
const debug = component.debug
if (debug) {
console.group('updating:', component.name, component.id)
}
// update childs first
const toExclude: Array<Component2D> = []
if (component.childs && component.childs.length > 0) {
for await (const child of component.childs) {
child.parent = component
toExclude.push(...await this.updateComponent(child))
}
}
const state: Partial<ComponentState> = {
collideWith: [],
scene: this
}
state.collideWith = this.checkColisions(component, toExclude)
if (debug) {
console.debug('collider [collisions, excludedCollisions]', state.collideWith.length, toExclude.length)
}
if (this.hasClickedComponent === component.id && !state.isColliding) {
this.hasClickedComponent = undefined
}
if (component.update) {
if (debug) {
console.log('Updating component')
}
component.update(state as ComponentState)
} else if (debug) {
console.log('Component has no updater')
}
if (component.debug) {
console.groupEnd()
}
return toExclude
}
private async renderComponent(component: Component2D) {
const debug = component.debug
if (debug) {
console.group('rendering: ', component.name, component.id)
}
if (component.renderer) {
if (debug) {
console.log('rendering!')
}
// console.log('is rendering new element')
await component.renderer.render(this.ge, this.ge.ctx)
} else if (debug) {
console.log('component has no renderer')
}
if (component.childs && component.childs.length > 0) {
for await (const child of component.childs) {
child.parent = component
await this.renderComponent(child)
}
}
if (component.debug) {
console.groupEnd()
}
}
private async destroyComponent(component: Component2D) {
for await (const child of component.childs) {
await this.destroyComponent(child)
}
await component.destroy?.()
}
} }

View File

@ -32,7 +32,9 @@ export default class Tileset {
} }
// const {x, y} = this.getPosFromId(id) // const {x, y} = this.getPosFromId(id)
const cols = Math.trunc(this.declaration.fileSize.width / this.width(id)) const cols = Math.trunc(this.declaration.fileSize.width / this.width(id))
// eslint-disable-next-line id-length
const x = id % cols const x = id % cols
// eslint-disable-next-line id-length
const y = Math.trunc(id / cols) const y = Math.trunc(id / cols)
const sx = x * this.width(id) + x * (this.declaration.spacing ?? 0) const sx = x * this.width(id) + x * (this.declaration.spacing ?? 0)
const sy = y * this.height(id) + y * (this.declaration.spacing ?? 0) const sy = y * this.height(id) + y * (this.declaration.spacing ?? 0)

View File

@ -1,3 +1,4 @@
import { objectValues } from '@dzeio/object-util'
import Vector2D from './2D/Vector2D' import Vector2D from './2D/Vector2D'
import Scene from './Scene' import Scene from './Scene'
@ -12,26 +13,49 @@ export default class GameEngine {
public ctx: CanvasRenderingContext2D public ctx: CanvasRenderingContext2D
public canvas: HTMLCanvasElement public canvas: HTMLCanvasElement
public caseSize: Vector2D = new Vector2D(1, 1) public caseSize: Vector2D = new Vector2D(1, 1)
public cursor: { public componentId = 0
position: Vector2D
isDown: boolean public currentScene?: Scene
wasDown: boolean
} = { // last frame timestamp
position: new Vector2D(0, 0), public lastFrame = 0
isDown: false,
wasDown: false /**
} * last frame execution time in milliseconds
public currentScene!: Scene *
private isRunning = false * @memberof GameEngine
*/
public frameTime = 0
/**
* indicate if the engine is running
*/
public isRunning = false
// timer between frames
private timer = 0
private loopId?: NodeJS.Timer
public constructor( public constructor(
private id: string, id: string,
public options?: { public options?: {
caseCount?: number | [number, number] caseCount?: number | [number, number]
background?: string background?: string
debugColliders?: boolean debugColliders?: boolean
/**
* Maximum framerate you want to achieve
*
* note: -1 mean infinite
*/
goalFramerate?: number
} }
) { ) {
if (GameEngine.ge) {
this.destroy()
// throw new Error('GameEngine already init')
}
GameEngine.ge = this GameEngine.ge = this
const canvas = document.querySelector<HTMLCanvasElement>(id) const canvas = document.querySelector<HTMLCanvasElement>(id)
if (!canvas) { if (!canvas) {
@ -40,10 +64,12 @@ export default class GameEngine {
this.canvas = canvas this.canvas = canvas
if (this.options?.caseCount) { if (this.options?.caseCount) {
this.caseSize = new Vector2D( this.caseSize = new Vector2D(
// @ts-expect-error idc this.canvas.width / (
this.canvas.width / ((typeof this.options.caseCount) !== 'number' ? this.options.caseCount[0] : this.options.caseCount ), typeof this.options.caseCount !== 'number' ? this.options.caseCount[0] : this.options.caseCount
// @ts-expect-error idc2 lol ),
this.canvas.height / ((typeof this.options.caseCount) !== 'number' ? this.options.caseCount[1] : this.options.caseCount) this.canvas.height / (
typeof this.options.caseCount !== 'number' ? this.options.caseCount[1] : this.options.caseCount
)
) )
} }
@ -51,12 +77,17 @@ export default class GameEngine {
if (!ctx) { if (!ctx) {
throw new Error('Error, Context could not get found!') throw new Error('Error, Context could not get found!')
} }
ctx.imageSmoothingEnabled = false ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = 'high'
this.ctx = ctx this.ctx = ctx
if (options?.goalFramerate && options.goalFramerate >= 0) {
this.timer = 1000 / options.goalFramerate
}
} }
public static getGameEngine(): GameEngine { public static getGameEngine(): GameEngine {
return this.ge return this.ge as GameEngine
} }
public start() { public start() {
@ -65,51 +96,108 @@ export default class GameEngine {
return return
} }
this.isRunning = true this.isRunning = true
requestAnimationFrame(() => { this.currentScene?.init().then(() => this.update())
this.update()
})
document.addEventListener('mousemove', (ev) => {
this.cursor.position = new Vector2D(
ev.clientX / this.caseSize.x - this.currentScene.camera.topLeft.x,
ev.clientY / this.caseSize.y - this.currentScene.camera.topLeft.y
)
if (this.cursor.isDown) {
this.cursor.wasDown = true
}
})
document.addEventListener('mousedown', () => {
this.cursor.isDown = true
})
document.addEventListener('mouseup', () => {
this.cursor.isDown = false
this.cursor.wasDown = false
})
} }
public pause() { public pause() {
this.isRunning = false this.isRunning = false
} }
public setScene(scene: Scene | string) { public async destroy() {
this.isRunning = false
for await (const scene of objectValues(Scene.scenes)) {
await scene.destroy()
}
if (GameEngine.ge) {
// @ts-expect-error normal behavior
delete GameEngine.ge as any
}
if (this.loopId) {
clearInterval(this.loopId)
}
}
public getXCaseCount(): number {
const caseCount = this.options?.caseCount
if (caseCount) {
if (typeof caseCount === 'number') {
return caseCount
} else {
return caseCount[0]
}
}
return this.canvas.offsetWidth
}
public getYCaseCount(): number {
const caseCount = this.options?.caseCount
if (caseCount) {
if (typeof caseCount === 'number') {
return caseCount
} else {
return caseCount[1]
}
}
return this.canvas.offsetWidth
}
public async setScene(scene: Scene | string) {
console.log('Setting scene', typeof scene === 'string' ? scene : scene.id) console.log('Setting scene', typeof scene === 'string' ? scene : scene.id)
const wasRunning = this.isRunning
if (wasRunning) {
this.isRunning = false
}
await this.currentScene?.destroy()
await this.currentScene?.init()
if (wasRunning) {
this.isRunning = true
}
this.currentScene = typeof scene === 'string' ? Scene.scenes[scene] : scene this.currentScene = typeof scene === 'string' ? Scene.scenes[scene] : scene
this.currentScene.setGameEngine(this) this.currentScene.setGameEngine(this)
} }
private update() { private update() {
if (!this.isRunning) { if (this.loopId) {
console.error('Already initialized')
return return
} }
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) // console.log('update')
if (this.options?.background) { this.loopId = setInterval(async () => {
this.ctx.fillStyle = this.options.background
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height) // get current time
} const now = window.performance.now()
this.currentScene?.update()
setTimeout(() => { // game is not runnig, wait a frame
this.update() if (!this.isRunning) {
}, 0) // console.log('skip frame 1')
return
}
// game is running too fast, wait until necessary
if (this.lastFrame + this.timer > now) {
// console.log('skip frame 2')
return
}
// console.log('new frame')
// if a background need to be drawn
if (this.options?.background) {
this.ctx.fillStyle = this.options.background
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
} else {
// clear the previous frame
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
await this.currentScene?.update()
// calculate for next frame
this.lastFrame = window.performance.now()
this.frameTime = window.performance.now() - now
})
} }
} }
export interface GameState<UserState = any> { export interface GameState<UserState = any> {