mirror of
https://github.com/Aviortheking/games.git
synced 2025-04-23 03:12:09 +00:00
feat: Updated engine
Signed-off-by: Avior <f.bouillon@aptatio.com>
This commit is contained in:
parent
8952b61651
commit
0a613fbdd6
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import Vector2D from './2D/Vector2D'
|
|
||||||
|
|
||||||
export default class Camera {
|
|
||||||
public topLeft = new Vector2D(0.5, 0.5)
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
45
src/GameEngine/Components/Camera.ts
Normal file
45
src/GameEngine/Components/Camera.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
162
src/GameEngine/Components/Cursor.ts
Normal file
162
src/GameEngine/Components/Cursor.ts
Normal 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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
52
src/GameEngine/Components/FPSCounter.ts
Normal file
52
src/GameEngine/Components/FPSCounter.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
464
src/GameEngine/Controller.ts
Normal file
464
src/GameEngine/Controller.ts
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
src/GameEngine/Renderer/ImageRenderer.ts
Normal file
69
src/GameEngine/Renderer/ImageRenderer.ts
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
66
src/GameEngine/Renderer/TextRenderer.ts
Normal file
66
src/GameEngine/Renderer/TextRenderer.ts
Normal 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
|
@ -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?.()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user