Signed-off-by: Avior <github@avior.me>
This commit is contained in:
Florian Bouillon 2022-06-14 18:27:20 +02:00
parent 8952b61651
commit 79b74d16d5
Signed by: Florian Bouillon
GPG Key ID: BEEAF3722D0EBF64
23 changed files with 344 additions and 5028 deletions

68
docs/classModel.puml Normal file
View File

@ -0,0 +1,68 @@
@startuml classes
!theme toy
class GameEngine {
- scenes: Record<string, Scene>
+ addScene(scene: Scene): void
+ setScene(id: string): void
+ removeScene(scene: string | Scene): void
+ start(): void
+ pause(): void
+ destroy(): void
}
class Scene {
+ id: string
+ setCamera(id: String): void
+ addComponents(components: ...Component2D): void
+ removeComponents(components: ...(Component2D | string)): void
}
GameEngine "1" -- "0-*" Scene : "contains"
class Component {
+ id: string
+ position: Vector2D
+ renderer: Renderer
+ position: Vector2D
+ scale: Vector2D
+ collider: Collider
+ origin: Vector2D
+ childs: Array<Component2D>
+ debug: boolean
+ init(): void
+ update(): void
}
Scene "1" -- "0-*" Component : "contains"
class Camera extends Component {
}
class Pointer extends Component {}
class Asset {}
class Sound {}
class Tileset {}
Component -- Component
Tileset -- Asset
class Vector2D {
+ x: number
+ y: number
}
class Collider {}
Component "1" -- "0-1" Collider
Component "1" -- "0-*" Vector2D
@enduml

24
package-lock.json generated
View File

@ -9191,7 +9191,8 @@
"@next/react-refresh-utils": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@next/react-refresh-utils/-/react-refresh-utils-11.0.0.tgz",
"integrity": "sha512-hi5eY+KBn4QGtUv7VL2OptdM33fI2hxhd7+omOFmAK+S0hDWhg1uqHqqGJk0W1IfqlWEzzL10WvTJDPRAtDugQ=="
"integrity": "sha512-hi5eY+KBn4QGtUv7VL2OptdM33fI2hxhd7+omOFmAK+S0hDWhg1uqHqqGJk0W1IfqlWEzzL10WvTJDPRAtDugQ==",
"requires": {}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
@ -9601,7 +9602,8 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true
"dev": true,
"requires": {}
},
"ajv": {
"version": "6.12.6",
@ -9617,7 +9619,8 @@
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"requires": {}
},
"alphanum-sort": {
"version": "1.0.2",
@ -11218,7 +11221,8 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz",
"integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==",
"dev": true
"dev": true,
"requires": {}
},
"eslint-scope": {
"version": "5.1.1",
@ -11715,7 +11719,8 @@
"icss-utils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
"requires": {}
},
"ieee754": {
"version": "1.2.1",
@ -12382,7 +12387,8 @@
"next-plausible": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-1.6.1.tgz",
"integrity": "sha512-eMVqZbzU6bJsBIHlC63lNKl0KLvx8NA+/YlUUqj2gvCviQ7C2X4PY59gEB+mO46JvKSy/IhAD8l85/YGFhCkDg=="
"integrity": "sha512-eMVqZbzU6bJsBIHlC63lNKl0KLvx8NA+/YlUUqj2gvCviQ7C2X4PY59gEB+mO46JvKSy/IhAD8l85/YGFhCkDg==",
"requires": {}
},
"next-pre-css": {
"version": "1.0.0",
@ -13407,7 +13413,8 @@
"postcss-modules-extract-imports": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw=="
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
"requires": {}
},
"postcss-modules-local-by-default": {
"version": "4.0.0",
@ -14865,7 +14872,8 @@
"stylis-rule-sheet": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz",
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw=="
"integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==",
"requires": {}
},
"stylus": {
"version": "0.54.8",

View File

@ -1,7 +1,7 @@
import Component2D from 'GameEngine/Component2D'
import Vector2D from '../Vector2D'
type BuiltinCollisionTypes = 'click'
type BuiltinCollisionTypes = 'click' | 'pointerDown' | 'pointerUp'
export default class BoxCollider2D {
public constructor(

View File

@ -1,14 +1,20 @@
import Component2D from 'GameEngine/Component2D'
import RectRenderer from 'GameEngine/Renderer/RectRenderer'
import Vector2D from '../Vector2D'
import PointDebugger from './PointDebugger'
export default class ComponentDebug extends Component2D {
public constructor(component: Component2D) {
super()
this.position = component.position
this.origin = component.origin
this.scale = new Vector2D(.1, .1)
this.position = new Vector2D(0, 0)
// this.origin = component.origin
this.scale = component.scale
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,12 @@ import RectRenderer from 'GameEngine/Renderer/RectRenderer'
import Vector2D from '../Vector2D'
export default class PointDebugger extends Component2D {
public constructor(point: Vector2D) {
public constructor(point: Vector2D, color = 'red') {
super()
this.scale = new Vector2D(.1, .1)
this.position = point
console.log('Debugging point at location', point)
// this.origin = component.origin
this.renderer = new RectRenderer(this, {material: 'red'})
this.renderer = new RectRenderer(this, {material: color})
}
}

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

@ -4,7 +4,6 @@ export default class Vector2D {
public y: number
) {}
public multiply(v: Vector2D): Vector2D {
return new Vector2D(
v.x * this.x,

View File

@ -1,3 +1,6 @@
/**
* Asset management Class
*/
export default class Asset {
public static assets: Record<string, Asset> = {}

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

@ -10,14 +10,45 @@ export interface ComponentState {
isColliding?: string
}
/**
* 2D Component
*/
export default abstract class Component2D {
/**
* Indicate how the component is rendered
*
* @type {Renderer}
* @memberof Component2D
*/
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)
/**
* Component scale relative to 1 case size
*
* (see also: GameEngine.caseSize)
*
* @type {Vector2D}
* @memberof Component2D
*/
public scale: Vector2D = new Vector2D(1, 1)
/**
* Component collider for events
*
* @type {BoxCollider2D}
* @memberof Component2D
*/
public collider?: BoxCollider2D
/**
@ -25,11 +56,35 @@ export default abstract class Component2D {
*/
public origin: Vector2D = new Vector2D(0 , 0)
/**
* Component Child Components
*
* @type {Array<Component2D>}
* @memberof 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
/**
* Function run when the component is initialized
*/
public init?(): Promise<void> | void
/**
* Function run on each game ticks
* @param state the component state
*/
public update?(state: ComponentState): Promise<void> | void
public destroy?(): Promise<void> | void
}

View File

@ -0,0 +1,8 @@
import Vector2D from '../2D/Vector2D'
/**
* Currently not working Camera implementation
*/
export default class Camera {
public topLeft = new Vector2D(0.5, 0.5)
}

View File

@ -0,0 +1,25 @@
import ComponentDebug from 'GameEngine/2D/Debug/ComponentDebug'
import Vector2D from 'GameEngine/2D/Vector2D'
import Component2D from 'GameEngine/Component2D'
import Renderer from 'GameEngine/Renderer'
import TextRenderer from 'GameEngine/Renderer/TextRenderer'
export default class FPSCounter extends Component2D {
public position: Vector2D = new Vector2D(0,0)
public scale: Vector2D = new Vector2D(1, 1)
public origin: Vector2D = new Vector2D(0, 0)
public childs: Array<Component2D> = [new ComponentDebug(this)]
public renderer: TextRenderer = new TextRenderer(this, {text: 'pouet'})
private lastUpdate: number = new Date().getTime()
public update() {
const now = new Date().getTime()
this.renderer.text = (1000 / (now - this.lastUpdate)).toFixed(2)
this.lastUpdate = now
}
}

View File

@ -0,0 +1,31 @@
import Event from '.'
export default class PointerEvents extends Event {
public override init(): void {
document.addEventListener('mousemove', this.basicEvent)
document.addEventListener('mousedown', this.mouseDown)
document.addEventListener('mouseup', this.mouseUp)
}
public update() {
// pouet
}
public override destroy() {
document.removeEventListener('mousemove', this.basicEvent)
document.removeEventListener('mousedown', this.mouseDown)
document.removeEventListener('mouseup', this.mouseUp)
}
private basicEvent = (ev: MouseEvent) => {
console.log('Mouse Event :D')
}
private mouseUp = (ev: MouseEvent) => {
this.basicEvent(ev)
}
private mouseDown = (ev: MouseEvent) => {
this.basicEvent(ev)
}
}

View File

@ -0,0 +1,11 @@
import GameEngine from 'GameEngine'
export default abstract class Event {
public constructor(
protected ge: GameEngine
) {}
abstract init(): void
abstract update(): void
abstract destroy(): void
}

View File

@ -9,7 +9,7 @@ interface Params {
stroke?: string
}
export default class RectRenderer extends Renderer implements Partial<Params> {
export default class RectRenderer extends Renderer implements Params {
public material?: string | Asset
public stroke?: string

View File

@ -0,0 +1,41 @@
import { objectLoop } from '@dzeio/object-util'
import GameEngine from 'GameEngine'
import Component2D from 'GameEngine/Component2D'
import Renderer from '.'
interface Params {
text?: string
}
export default class TextRenderer extends Renderer {
public text?: string
public size?: number
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 item: [number, number] = [
// source x
// 0 - 1.5 - -1.5
position.x * (ge.caseSize.x),
// source y
position.y * (ge.caseSize.y)
]
const size = this.component.scale.y * ge.caseSize.y
// console.log
if (this.text) {
ctx.fillStyle = 'black'
ctx.textBaseline = 'top'
ctx.font = `${size}px sans-serif`
ctx.fillText(this.text, ...item)
}
}
}

View File

@ -10,7 +10,7 @@ export default abstract class Renderer {
protected getPosition(): Vector2D {
const ge = GameEngine.getGameEngine()
const realPosition = ge.currentScene.camera.topLeft.sum(this.component.position)
const realPosition = ge.currentScene!.camera.topLeft.sum(this.component.position)
return new Vector2D(
realPosition.x - this.component.scale.x / 2 - this.component.origin.x,
realPosition.y - this.component.scale.y / 2 - this.component.origin.y

View File

@ -1,6 +1,6 @@
import GameEngine from 'GameEngine'
import AssetsManager from './Asset'
import Camera from './Camera'
import Camera from './Components/Camera'
import Component2D, { ComponentState } from './Component2D'
export default class Scene {
@ -42,6 +42,12 @@ export default class Scene {
}
}
public async destroy() {
for await (const component of this.components) {
await component.destroy?.()
}
}
private async updateComponent(v: Component2D) {
const debug = v.debug
if (debug) {
@ -64,6 +70,13 @@ export default class Scene {
// state.mouseClicking = state.mouseHovering && this.ge.cursor.isDown
// state.mouseClicked = state.mouseClicking && !this.ge.cursor.wasDown
// }
if (v.update) {
if (debug) {
console.log('Updating Component', v)
}
v.update(state as ComponentState)
}
if (v.renderer) {
if (debug) {
console.log('Rendering Component', v)
@ -71,17 +84,12 @@ export default class Scene {
// console.log('is rendering new element')
await v.renderer.render(this.ge, this.ge.ctx)
}
if (v.update) {
if (debug) {
console.log('Updating Component', v)
}
v.update(state as ComponentState)
}
if (v.childs) {
if (debug) {
console.log('Processing childs', v)
}
for (const child of v.childs) {
for await (const child of v.childs) {
await this.updateComponent(child)
}
}

View File

@ -21,15 +21,22 @@ export default class GameEngine {
isDown: false,
wasDown: false
}
public currentScene!: Scene
public currentScene?: Scene
private isRunning = false
private timer = 16.6
public constructor(
private id: string,
id: string,
public options?: {
caseCount?: number | [number, number]
background?: string
debugColliders?: boolean
/**
* Maximum framerate you want to achieve
*
* note: -1 mean infinite
*/
goalFramerate?: number
}
) {
GameEngine.ge = this
@ -53,6 +60,14 @@ export default class GameEngine {
}
ctx.imageSmoothingEnabled = false
this.ctx = ctx
if (options?.goalFramerate) {
if (options.goalFramerate === -1) {
this.timer = 0
} else {
this.timer = 1000 / options.goalFramerate
}
}
}
public static getGameEngine(): GameEngine {
@ -70,8 +85,8 @@ export default class GameEngine {
})
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
ev.clientX / this.caseSize.x - (this.currentScene?.camera?.topLeft?.x ?? 0),
ev.clientY / this.caseSize.y - (this.currentScene?.camera?.topLeft?.y ?? 0)
)
if (this.cursor.isDown) {
this.cursor.wasDown = true
@ -90,13 +105,15 @@ export default class GameEngine {
this.isRunning = false
}
public setScene(scene: Scene | string) {
public async setScene(scene: Scene | string) {
console.log('Setting scene', typeof scene === 'string' ? scene : scene.id)
await this.currentScene?.destroy()
this.currentScene = typeof scene === 'string' ? Scene.scenes[scene] : scene
this.currentScene.setGameEngine(this)
}
private update() {
const now = new Date().getTime()
if (!this.isRunning) {
return
}
@ -106,9 +123,19 @@ export default class GameEngine {
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
}
this.currentScene?.update()
setTimeout(() => {
this.update()
}, 0)
const diff = new Date().getTime() - now
if (diff > this.timer) {
requestAnimationFrame(() => {
this.update()
})
} else {
setTimeout(() => {
// this.update()
requestAnimationFrame(() => {
this.update()
})
}, this.timer - diff)
}
}
}

View File

@ -54,7 +54,7 @@ export default class Item extends Component2D {
console.log('item initialized')
}
public update(state: ComponentState) {
public async update(state: ComponentState) {
if (!globalState.isPlaying) {
return
}
@ -62,7 +62,7 @@ export default class Item extends Component2D {
const value: '' | 'X' | 'O' = globalState.gameState[this.x][this.y] as '' | 'X' | 'O'
if (state.isColliding === 'click' && value === '') {
// console.log('hovering')
this.onClick()
await this.onClick()
}
if (value === 'X') {
@ -75,7 +75,7 @@ export default class Item extends Component2D {
}
}
private onClick() {
private async onClick() {
const clickSound = new SoundManager('/assets/tictactoe/bip.wav')
clickSound.play()
globalState.gameState[this.x][this.y] = globalState.playerTurn
@ -88,7 +88,7 @@ export default class Item extends Component2D {
['', '', '']
]
console.log(globalState)
GameEngine.getGameEngine().setScene('Menu')
await GameEngine.getGameEngine().setScene('Menu')
return
}
globalState.playerTurn = globalState.playerTurn === 'X' ? 'O' : 'X'

View File

@ -13,11 +13,16 @@ export default class Start extends Component2D {
public collider: BoxCollider2D = new BoxCollider2D(this, 'click')
public childs: Array<Component2D> = [new ColliderDebugger(this, this.collider)]
public update(state: ComponentState) {
private hasCollided = false
public async update(state: ComponentState) {
if (state.isColliding === 'click') {
this.hasCollided = true
} else if (this.hasCollided) {
console.log('Start Game !')
GameEngine.getGameEngine().setScene('TicTacToe')
await GameEngine.getGameEngine().setScene('TicTacToe')
globalState.isPlaying = true
this.hasCollided = false
}
}
}

View File

@ -6,6 +6,8 @@ import Scene from 'GameEngine/Scene'
import Item from 'games/tictactoe/Item'
import Line from 'games/tictactoe/Line'
import Start from 'games/tictactoe/Menu/Start'
import FPSCounter from 'GameEngine/Components/FPSCounter'
import ComponentDebug from 'GameEngine/2D/Debug/ComponentDebug'
export default class Snake extends React.PureComponent {
@ -13,23 +15,26 @@ export default class Snake extends React.PureComponent {
const ge = new GameEngine('#test', {
caseCount: 3,
background: 'blue',
debugColliders: true
debugColliders: true,
goalFramerate: 30
})
const menuScene = new Scene('Menu')
menuScene.addComponent(
new Start()
new Start(),
new FPSCounter()
)
const scene = new Scene('TicTacToe')
scene.addComponent(
...Array.from(new Array(2)).map((_, index) => new Line(0, index)),
...Array.from(new Array(2)).map((_, index) => new Line(1, index)),
...Array.from(new Array(9)).map((_, index) => new Item(index)),
Item.explosion
Item.explosion,
new FPSCounter()
// new TilingDebugger()
)
await ge.setScene(menuScene)
ge.start()
ge.setScene(menuScene)
}
public render = () => (

4949
yarn.lock

File diff suppressed because it is too large Load Diff