From ff2dc1ad86e59feede6e50138c889641eb4173ae Mon Sep 17 00:00:00 2001 From: Jurien Meerlo Date: Sun, 15 Sep 2024 22:59:47 +0100 Subject: [PATCH] ecs and assets. --- src/jume/Jume.hx | 68 +++++++- src/jume/assets/AssetLoader.hx | 40 +++++ src/jume/assets/Assets.hx | 86 +++++++++ src/jume/assets/AtlasLoader.hx | 54 ++++++ src/jume/assets/BitmapFontLoader.hx | 53 ++++++ src/jume/assets/ImageLoader.hx | 53 ++++++ src/jume/assets/ShaderLoader.hx | 59 +++++++ src/jume/assets/SoundLoader.hx | 38 ++++ src/jume/assets/TextLoader.hx | 27 +++ src/jume/ecs/Component.hx | 31 ++++ src/jume/ecs/ComponentContainer.hx | 83 +++++++++ src/jume/ecs/Entities.hx | 89 ++++++++++ src/jume/ecs/Entity.hx | 89 ++++++++++ src/jume/ecs/Renderable.hx | 8 + src/jume/ecs/Scene.hx | 94 ++++++++++ src/jume/ecs/System.hx | 92 ++++++++++ src/jume/ecs/Systems.hx | 123 +++++++++++++ src/jume/ecs/Updatable.hx | 5 + src/jume/events/EventListener.hx | 11 ++ src/jume/events/Events.hx | 25 ++- src/jume/events/SceneEvent.hx | 9 + src/jume/tweens/Tweens.hx | 21 ++- src/jume/view/Camera.hx | 260 ++++++++++++++++++++++++++++ src/jume/view/View.hx | 5 + 24 files changed, 1420 insertions(+), 3 deletions(-) create mode 100644 src/jume/assets/AssetLoader.hx create mode 100644 src/jume/assets/Assets.hx create mode 100644 src/jume/assets/AtlasLoader.hx create mode 100644 src/jume/assets/BitmapFontLoader.hx create mode 100644 src/jume/assets/ImageLoader.hx create mode 100644 src/jume/assets/ShaderLoader.hx create mode 100644 src/jume/assets/SoundLoader.hx create mode 100644 src/jume/assets/TextLoader.hx create mode 100644 src/jume/ecs/Component.hx create mode 100644 src/jume/ecs/ComponentContainer.hx create mode 100644 src/jume/ecs/Entities.hx create mode 100644 src/jume/ecs/Entity.hx create mode 100644 src/jume/ecs/Renderable.hx create mode 100644 src/jume/ecs/Scene.hx create mode 100644 src/jume/ecs/System.hx create mode 100644 src/jume/ecs/Systems.hx create mode 100644 src/jume/ecs/Updatable.hx create mode 100644 src/jume/events/SceneEvent.hx create mode 100644 src/jume/view/Camera.hx diff --git a/src/jume/Jume.hx b/src/jume/Jume.hx index cf8bc6b..c404060 100644 --- a/src/jume/Jume.hx +++ b/src/jume/Jume.hx @@ -1,5 +1,16 @@ package jume; +import jume.events.SceneEvent; +import jume.math.Random; +import jume.audio.Audio; +import jume.graphics.RenderTarget; +import jume.view.View; +import jume.utils.TimeStep; +import jume.input.Input; +import jume.graphics.Graphics; +import jume.graphics.gl.Context; +import jume.ecs.Scene; + import haxe.Timer; import jume.events.FocusEvent; @@ -40,6 +51,20 @@ class Jume { */ final events: Events; + final context: Context; + + final graphics: Graphics; + + final input: Input; + + final timeStep: TimeStep; + + final view: View; + + var target: RenderTarget; + + var scene: Scene; + /** * Create a new Jume instance. * @param options The game options. @@ -77,6 +102,31 @@ class Jume { events = new Events(); Services.add(events); + context = new Context(options.canvasId, options.forceWebGL1); + Services.add(context); + + view = new View({ + width: options.designSize.widthi, + height: options.designSize.heighti, + pixelRatio: pixelRatio, + pixelFilter: options.pixelFilter, + isFullScreen: isFullScreen, + targetFps: options.targetFps, + canvasId: options.canvasId + }); + Services.add(view); + + input = new Input(options.canvasId); + Services.add(input); + + timeStep = new TimeStep(); + Services.add(timeStep); + + Services.add(new Audio()); + Services.add(new Random()); + + graphics = new Graphics(context, view); + #if !headless canvas.focus(); canvas.addEventListener('blur', () -> lostFocus()); @@ -85,7 +135,10 @@ class Jume { #end } - public function launch() { + public function launch(sceneType: Class) { + events.addListener({ type: SceneEvent.CHANGE, callback: onSceneChange }); + changeScene(sceneType); + #if headless prevTime = Timer.stamp(); Timer.delay(headlessLoop, Std.int(1.0 / 60.0 * 1000)); @@ -138,4 +191,17 @@ class Jume { } function render() {} + + function onSceneChange(event: SceneEvent) { + changeScene(event.sceneType); + event.canceled = true; + } + + function changeScene(sceneType: Class) { + if (scene != null) { + scene.destroy(); + } + + scene = Type.createInstance(sceneType, []); + } } diff --git a/src/jume/assets/AssetLoader.hx b/src/jume/assets/AssetLoader.hx new file mode 100644 index 0000000..e2ec493 --- /dev/null +++ b/src/jume/assets/AssetLoader.hx @@ -0,0 +1,40 @@ +package jume.assets; + +import jume.di.Injectable; + +import haxe.Exception; + +class AssetLoader implements Injectable { + public final assetType: Class; + + final loadedAssets: Map; + + public function new(assetType: Class) { + this.assetType = assetType; + loadedAssets = new Map(); + } + + public function load(id: String, path: String, callback: (asset: T)->Void, ?props: Dynamic, ?keep: Bool) {} + + public function add(id: String, instance: T) { + this.loadedAssets[id] = instance; + } + + public function get(id: String): T { + if (loadedAssets.exists(id)) { + return loadedAssets[id]; + } + + throw new Exception('Asset with id ${id} not loaded.'); + } + + public function unload(id: String): Bool { + if (loadedAssets.exists(id)) { + loadedAssets.remove(id); + + return true; + } + + return false; + } +} diff --git a/src/jume/assets/Assets.hx b/src/jume/assets/Assets.hx new file mode 100644 index 0000000..2482d1a --- /dev/null +++ b/src/jume/assets/Assets.hx @@ -0,0 +1,86 @@ +package jume.assets; + +import haxe.Exception; + +typedef AssetItem = { + var type: Class; + var id: String; + var path: String; + var ?props: Dynamic; +} + +typedef LoadParams = { + var assetType: Class; + var id: String; + var path: String; + var callback: (asset: T)->Void; + var ?props: Dynamic; + var ?keep: Bool; +} + +class Assets { + final loaders: Map>; + + public function new() { + loaders = new Map>(); + } + + public function registerLoader(loader: AssetLoader) { + final name = Type.getClassName(loader.assetType); + loaders[name] = loader; + } + + public function load(params: LoadParams) { + final name = Type.getClassName(params.assetType); + if (loaders.exists(name)) { + loaders[name].load(params.id, params.path, params.callback, params.props, params.keep); + } else { + throw new Exception('Loader is not registered for type ${name}'); + } + } + + public function loadAll(items: Array, callback: ()->Void) { + var loaded = 0; + for (item in items) { + load({ + assetType: item.type, + id: item.id, + path: item.path, + props: item.props, + callback: (asset) -> { + loaded++; + if (loaded == items.length) { + callback(); + } + } + }); + } + } + + public function add(assetType: Class, id: String, instance: T) { + final name = Type.getClassName(assetType); + if (loaders.exists(name)) { + loaders[name].add(id, instance); + } else { + throw new Exception('Loader is not registered for type ${name}'); + } + } + + public function get(assetType: Class, id): T { + final name = Type.getClassName(assetType); + if (loaders.exists(name)) { + return loaders[name].get(id); + } else { + throw new Exception('Loader is not registered for type ${name}'); + } + } + + public function unload(assetType: Class, id: String) { + final name = Type.getClassName(assetType); + if (loaders.exists(name)) { + loaders[name].unload(id); + } else { + throw new Exception('Loader is not registered for type ${name}'); + } + } +} diff --git a/src/jume/assets/AtlasLoader.hx b/src/jume/assets/AtlasLoader.hx new file mode 100644 index 0000000..62d8853 --- /dev/null +++ b/src/jume/assets/AtlasLoader.hx @@ -0,0 +1,54 @@ +package jume.assets; + +import jume.graphics.Image; +import jume.graphics.atlas.Atlas; + +class AtlasLoader extends AssetLoader { + @:inject + var assets: Assets; + + public function new() { + super(Atlas); + } + + public override function load(id: String, path: String, callback: (asset: Atlas)->Void, ?props: Dynamic, + ?keep: Bool) { + keep ??= true; + assets.load({ + assetType: Image, + id: 'jume_atlas_${id}', + path: '${path}.png', + keep: keep, + callback: (image) -> { + assets.load({ + assetType: String, + id: 'jume_atlas_${id}', + path: '${path}.json', + keep: keep, + callback: (text) -> { + if (image == null || text == null) { + callback(null); + } else { + final atlas = new Atlas(image, text); + if (keep) { + loadedAssets[id] = atlas; + } + callback(atlas); + } + } + }); + } + }); + } + + public override function unload(id: String): Bool { + if (loadedAssets.exists(id)) { + assets.unload(Image, 'jume_atlas_${id}'); + assets.unload(String, 'jume_atlas_${id}'); + + return super.unload(id); + } + + return false; + } +} diff --git a/src/jume/assets/BitmapFontLoader.hx b/src/jume/assets/BitmapFontLoader.hx new file mode 100644 index 0000000..45c81bb --- /dev/null +++ b/src/jume/assets/BitmapFontLoader.hx @@ -0,0 +1,53 @@ +package jume.assets; + +import jume.graphics.Image; +import jume.graphics.bitmapFont.BitmapFont; + +class BitmapFontLoader extends AssetLoader { + @:inject + var assets: Assets; + + public function new() { + super(BitmapFont); + } + + public override function load(id: String, path: String, callback: (asset: BitmapFont)->Void, ?props: Dynamic, + ?keep: Bool) { + keep ??= true; + assets.load({ + assetType: Image, + id: 'jume_bitmap_font_${id}', + path: '${path}.png', + keep: keep, + callback: (image) -> { + assets.load({ + assetType: String, + id: 'jume_bitmap_font_${id}', + path: '${path}.fnt', + keep: keep, + callback: (text) -> { + if (image == null && text == null) { + callback(null); + } else { + final font = new BitmapFont(image, text); + if (keep) { + loadedAssets[id] = font; + } + + callback(font); + } + } + }); + } + }); + } + + public override function unload(id: String): Bool { + if (loadedAssets.exists(id)) { + assets.unload(Image, 'jume_bitmap_font_${id}'); + assets.unload(String, 'jume_bitmap_font_${id}'); + } + + return super.unload(id); + } +} diff --git a/src/jume/assets/ImageLoader.hx b/src/jume/assets/ImageLoader.hx new file mode 100644 index 0000000..7386b9a --- /dev/null +++ b/src/jume/assets/ImageLoader.hx @@ -0,0 +1,53 @@ +package jume.assets; + +import js.html.CanvasElement; +import js.Browser; +import js.lib.Uint8ClampedArray; + +import jume.graphics.Image; + +class ImageLoader extends AssetLoader { + public function new() { + super(Image); + } + + public override function load(id: String, path: String, callback: (asset: Image)->Void, ?props: Dynamic, + ?keep: Bool) { + keep ??= true; + final element = Browser.document.createImageElement(); + element.onload = () -> { + element.onload = null; + final canvas: CanvasElement = cast Browser.document.createElement('canvas'); + canvas.width = element.width; + canvas.height = element.height; + + final canvasContext = canvas.getContext2d(); + canvasContext.drawImage(element, 0, 0); + + final data: Uint8ClampedArray = cast canvasContext.getImageData(0, 0, element.width, element.height).data; + final image = new Image(element.width, element.height, data); + if (keep) { + loadedAssets[id] = image; + } + + callback(image); + } + + element.onerror = () -> { + trace('Unable to load image ${id}.'); + callback(null); + } + + element.src = path; + } + + public override function unload(id: String): Bool { + final image = loadedAssets[id]; + if (image != null) { + image.destroy(); + return super.unload(id); + } + + return false; + } +} diff --git a/src/jume/assets/ShaderLoader.hx b/src/jume/assets/ShaderLoader.hx new file mode 100644 index 0000000..67c4c0c --- /dev/null +++ b/src/jume/assets/ShaderLoader.hx @@ -0,0 +1,59 @@ +package jume.assets; + +import jume.graphics.ShaderType; + +import haxe.io.Path; + +import jume.graphics.gl.Context; +import jume.graphics.Shader; + +class ShaderLoader extends AssetLoader { + @:inject + var assets: Assets; + + @:inject + var context: Context; + + public function new() { + super(Shader); + } + + public override function load(id: String, path: String, callback: (asset: Shader)->Void, ?props: Dynamic, + ?keep: Bool) { + keep ??= true; + final extension = Path.extension(path); + if (context.isGL1) { + final dirAndFile = Path.withoutExtension(path); + path = '${dirAndFile}.gl1${extension}'; + } + + final shaderType: ShaderType = extension == 'vert' ? VERTEX : FRAGMENT; + assets.load({ + assetType: String, + id: 'jume_shader_${id}', + path: path, + callback: (text) -> { + if (text == null) { + callback(null); + } else { + final shader = new Shader(text, shaderType); + if (keep) { + loadedAssets[id] = shader; + } + + callback(shader); + } + } + }); + } + + public override function unload(id: String): Bool { + final shader = loadedAssets[id]; + if (shader != null) { + shader.destroy(); + return super.unload(id); + } + + return false; + } +} diff --git a/src/jume/assets/SoundLoader.hx b/src/jume/assets/SoundLoader.hx new file mode 100644 index 0000000..545fa62 --- /dev/null +++ b/src/jume/assets/SoundLoader.hx @@ -0,0 +1,38 @@ +package jume.assets; + +import js.Browser; + +import jume.audio.Audio; +import jume.audio.Sound; + +class SoundLoader extends AssetLoader { + @:inject + var audio: Audio; + + public function new() { + super(Sound); + } + + public override function load(id: String, path: String, callback: (asset: Sound)->Void, ?props: Dynamic, + ?keep: Bool) { + keep ??= true; + Browser.window.fetch(path).then((response) -> { + if (response.status < 400) { + response.arrayBuffer().then((buffer) -> { + audio.decodeSound(id, buffer, (sound) -> { + if (sound != null) { + if (keep) { + loadedAssets[id] = sound; + } + + callback(sound); + } else { + trace(response.statusText); + callback(null); + } + }); + }); + } + }); + } +} diff --git a/src/jume/assets/TextLoader.hx b/src/jume/assets/TextLoader.hx new file mode 100644 index 0000000..1fd767a --- /dev/null +++ b/src/jume/assets/TextLoader.hx @@ -0,0 +1,27 @@ +package jume.assets; + +import js.Browser; + +class TextLoader extends AssetLoader { + public function new() { + super(String); + } + + public override function load(id: String, path: String, callback: (asset: String)->Void, ?props: Dynamic, + ?keep: Bool) { + keep ??= true; + Browser.window.fetch(path).then((response) -> { + if (response.status < 400) { + response.text().then((text) -> { + if (keep) { + loadedAssets[id] = text; + } + callback(text); + }); + } else { + trace(response.statusText); + callback(null); + } + }); + } +} diff --git a/src/jume/ecs/Component.hx b/src/jume/ecs/Component.hx new file mode 100644 index 0000000..990d79e --- /dev/null +++ b/src/jume/ecs/Component.hx @@ -0,0 +1,31 @@ +package jume.ecs; + +typedef ComponentParams = { + var ?_entityId: Int; + var ?_components: ComponentContainer; +} + +class Component { + public final entityId: Int; + + final components: ComponentContainer; + + public function new(params: ComponentParams) { + entityId = params._entityId; + components = params._components; + } + + inline function getComponent(componentType: Class): T { + return components.get(componentType); + } + + inline function hasComponent(componentType: Class): Bool { + return components.has(componentType); + } + + inline function hasComponents(componentTypes: Array>): Bool { + return components.hasAll(componentTypes); + } + + public function destroy() {} +} diff --git a/src/jume/ecs/ComponentContainer.hx b/src/jume/ecs/ComponentContainer.hx new file mode 100644 index 0000000..908c9de --- /dev/null +++ b/src/jume/ecs/ComponentContainer.hx @@ -0,0 +1,83 @@ +package jume.ecs; + +import jume.ecs.Component.ComponentParams; + +class ComponentContainer { + final components: Map; + + final updatables: Array; + + final renderables: Array; + + public function new() { + components = new Map(); + updatables = []; + renderables = []; + } + + public function add(componentType: Class, params: ComponentParams): T { + final name = Type.getClassName(componentType); + final component = Type.createInstance(componentType, [params]); + components[name] = component; + + if (Std.isOfType(component, Updatable)) { + updatables.push(cast component); + } + + if (Std.isOfType(component, Renderable)) { + renderables.push(cast component); + } + + return component; + } + + public function remove(componentType: Class): Bool { + var removed = false; + final name = Type.getClassName(componentType); + if (components.exists(name)) { + final component = components[name]; + component.destroy(); + removed = true; + + components.remove(name); + updatables.remove(cast component); + renderables.remove(cast component); + } + + return removed; + } + + public inline function get(componentType: Class): T { + final name = Type.getClassName(componentType); + return cast components[name]; + } + + public inline function has(componentType: Class): Bool { + final name = Type.getClassName(componentType); + return components.exists(name); + } + + public function hasAll(componentTypes: Array>): Bool { + for (componentType in componentTypes) { + if (!has(componentType)) { + return false; + } + } + + return true; + } + + public inline function getRenderables(): Array { + return renderables; + } + + public inline function getUpdatables(): Array { + return updatables; + } + + public function destroy() { + for (component in components) { + component.destroy(); + } + } +} diff --git a/src/jume/ecs/Entities.hx b/src/jume/ecs/Entities.hx new file mode 100644 index 0000000..f06840c --- /dev/null +++ b/src/jume/ecs/Entities.hx @@ -0,0 +1,89 @@ +package jume.ecs; + +import jume.di.Service; + +class Entities implements Service { + final entities: Array; + + final entitiesToRemove: Array; + + final systems: Systems; + + public function new(systems: Systems) { + this.systems = systems; + entities = []; + entitiesToRemove = []; + } + + public function update(dt: Float) { + while (entitiesToRemove.length > 0) { + final entity = entitiesToRemove.pop(); + systems.updateSystemEntities(entity, true); + + entity.destroy(); + entities.remove(entity); + } + + for (entity in entities) { + if (entity.componentsUpdated) { + systems.updateSystemEntities(entity); + entity.componentsUpdated = false; + } + } + } + + public function add(entityType: Class, params: Dynamic): T { + final entity = Type.createInstance(entityType, [params]); + entities.push(entity); + systems.updateSystemEntities(entity); + + return entity; + } + + public function remove(entity: Entity) { + entitiesToRemove.push(entity); + } + + public function getById(id: Int): Entity { + for (entity in entities) { + if (entity.id == id) { + return entity; + } + } + + return null; + } + + public function removeById(id: Int) { + final entity = getById(id); + if (entity != null) { + entities.remove(entity); + } + } + + public function getByTag(tag: String): Array { + final taggedEntities: Array = []; + + for (entity in entities) { + if (entity.tag == tag) { + taggedEntities.push(entity); + } + } + + return taggedEntities; + } + + public function removeByTag(tag: String) { + final taggedEntities = getByTag(tag); + + while (taggedEntities.length > 0) { + entitiesToRemove.push(taggedEntities.pop()); + } + } + + public function destroy() { + for (entity in entities) { + entity.destroy(); + } + } +} diff --git a/src/jume/ecs/Entity.hx b/src/jume/ecs/Entity.hx new file mode 100644 index 0000000..43634d4 --- /dev/null +++ b/src/jume/ecs/Entity.hx @@ -0,0 +1,89 @@ +package jume.ecs; + +import jume.ecs.Component.ComponentParams; + +import haxe.Exception; + +using jume.math.MathUtils; + +class Entity { + public final id: Int; + + public var active: Bool; + + public var tag: String; + + public var componentsUpdated: Bool; + + public var layer(default, set): Int; + + public var layerChanged: Bool; + + static var nextId = 0; + + static final freeIds: Array = []; + + final components: ComponentContainer; + + public function new() { + id = getId(); + active = true; + tag = ''; + componentsUpdated = false; + components = new ComponentContainer(); + } + + public function destroy() { + freeIds.push(id); + components.destroy(); + } + + public inline function addComponent(componentType: Class, params: ComponentParams): T { + params._components = components; + params._entityId = id; + componentsUpdated = true; + return components.add(componentType, params); + } + + public inline function removeComponent(componentType: Class): Bool { + componentsUpdated = true; + return components.remove(componentType); + } + + public inline function hasComponent(componentType: Class): Bool { + return components.has(componentType); + } + + public inline function hasComponents(componentTypes: Array>): Bool { + return components.hasAll(componentTypes); + } + + public inline function getRenderComponents(): Array { + return components.getRenderables(); + } + + public inline function getUpdateComponents(): Array { + return components.getUpdatables(); + } + + function getId(): Int { + var i: Int; + if (nextId < MathUtils.MAX_VALUE_INT) { + i = nextId; + nextId++; + } else if (freeIds.length > 0) { + i = freeIds.pop(); + } else { + throw new Exception('Cannot create entity maximum number of entities reached.'); + } + + return i; + } + + inline function set_layer(value: Int): Int { + layer = value; + layerChanged = true; + + return layer; + } +} diff --git a/src/jume/ecs/Renderable.hx b/src/jume/ecs/Renderable.hx new file mode 100644 index 0000000..ac753cf --- /dev/null +++ b/src/jume/ecs/Renderable.hx @@ -0,0 +1,8 @@ +package jume.ecs; + +import jume.graphics.Graphics; + +interface Renderable { + function cRender(graphics: Graphics): Void; + function cDebugRender(graphics: Graphics): Void; +} diff --git a/src/jume/ecs/Scene.hx b/src/jume/ecs/Scene.hx new file mode 100644 index 0000000..5bcf069 --- /dev/null +++ b/src/jume/ecs/Scene.hx @@ -0,0 +1,94 @@ +package jume.ecs; + +import jume.di.Services; +import jume.ecs.System.SystemParams; +import jume.graphics.Graphics; +import jume.tweens.Tweens; +import jume.view.Camera; + +class Scene { + final cameras: Array; + + final entities: Entities; + + final systems: Systems; + + final tweens: Tweens; + + public function new() { + cameras = [new Camera()]; + systems = new Systems(cameras); + entities = new Entities(systems); + tweens = new Tweens(); + + Services.add(systems); + Services.add(entities); + Services.add(tweens); + } + + public inline function addEntity(entityType: Class, params: Dynamic): T { + return this.entities.add(entityType, params); + } + + public inline function removeEntity(entity: Entity) { + entities.remove(entity); + } + + public inline function getEntityById(id: Int): Entity { + return entities.getById(id); + } + + public inline function removeEntityById(id: Int) { + entities.removeById(id); + } + + public inline function getEntityByTag(tag: String): Array { + return entities.getByTag(tag); + } + + public inline function removeEntityByTag(tag: String) { + entities.removeByTag(tag); + } + + public inline function addSystem(systemType: Class, order: Int, params: SystemParams): T { + return systems.add(systemType, order, params); + } + + public inline function removeSystem(systemType: Class): Bool { + return systems.remove(systemType); + } + + public inline function getSystem(systemType: Class): T { + return systems.get(systemType); + } + + public inline function has(systemType: Class): Bool { + return systems.has(systemType); + } + + public inline function update(dt: Float) { + tweens.update(dt); + entities.update(dt); + systems.update(dt); + } + + public inline function render(graphics: Graphics) { + systems.render(graphics); + } + + public function resize() { + for (camera in cameras) { + camera.resize(); + } + } + + public function hasFocus() {} + + public function lostFocus() {} + + public function destroy() { + tweens.clearTweens(); + systems.destroy(); + entities.destroy(); + } +} diff --git a/src/jume/ecs/System.hx b/src/jume/ecs/System.hx new file mode 100644 index 0000000..c7c1f95 --- /dev/null +++ b/src/jume/ecs/System.hx @@ -0,0 +1,92 @@ +package jume.ecs; + +import jume.view.Camera; +import jume.graphics.Graphics; + +typedef SystemParams = { + var ?_systems: Map; + var ?_order: Int; +} + +typedef EntityList = { + var entities: Array; + var ?components: Array>; + var ?updatables: Bool; + var ?renderables: Bool; + var ?addCallback: (entity: Entity)->Void; + var ?removeCallback: (entity: Entity)->Void; +} + +class System { + public final order: Int; + + public var active: Bool; + + public var debug: Bool; + + final lists: Array; + + final systems: Map; + + public function new(params: SystemParams) { + order = params._order; + systems = params._systems; + lists = []; + debug = true; + active = true; + } + + public function update(dt: Float) {} + + public function render(graphics: Graphics, cameras: Array) {} + + public function debugRender(graphics: Graphics, cameras: Array) {} + + public function updateEntityLists(entity: Entity, removed: Bool) { + for (list in lists) { + if (removed) { + if (list.entities.contains(entity)) { + if (list.entities.remove(entity)) { + if (list.removeCallback != null) { + list.removeCallback(entity); + } + } + } + } else { + if (!list.entities.contains(entity) && hasAny(entity, list)) { + list.entities.push(entity); + if (list.addCallback != null) { + list.addCallback(entity); + } + } else if (list.entities.contains(entity) && !hasAny(entity, list)) { + if (list.entities.remove(entity)) { + if (list.removeCallback != null) { + list.removeCallback(entity); + } + } + } + } + } + } + + public function destroy() {} + + inline function getSystem(systemType: Class): T { + final name = Type.getClassName(systemType); + return cast systems[name]; + } + + inline function hasSystem(systemType: Class): Bool { + final name = Type.getClassName(systemType); + return systems.exists(name); + } + + function hasAny(entity: Entity, list: EntityList): Bool { + if (list.components != null) { + return entity.hasComponents(list.components); + } else { + return (list.renderables != null && entity.getRenderComponents().length > 0) + || (list.updatables != null && entity.getUpdateComponents().length > 0); + } + } +} diff --git a/src/jume/ecs/Systems.hx b/src/jume/ecs/Systems.hx new file mode 100644 index 0000000..8fe684a --- /dev/null +++ b/src/jume/ecs/Systems.hx @@ -0,0 +1,123 @@ +package jume.ecs; + +import jume.di.Service; +import jume.graphics.Color; +import jume.math.Vec2; +import jume.graphics.Graphics; +import jume.ecs.System.SystemParams; +import jume.view.View; +import jume.view.Camera; + +class Systems implements Service { + final systems: Map; + + final systemList: Array; + + final cameras: Array; + + final tempPos: Vec2; + + @:inject + var view: View; + + public function new(cameras: Array) { + this.cameras = cameras; + systems = new Map(); + systemList = []; + tempPos = new Vec2(); + } + + public function add(systemType: Class, order: Int, params: SystemParams): T { + params._order = order; + params._systems = systems; + + final system = Type.createInstance(systemType, [params]); + final name = Type.getClassName(systemType); + + systems[name] = system; + systemList.push(system); + + systemList.sort((a, b) -> { + if (a.order > b.order) { + return -1; + } else if (a.order < b.order) { + return 1; + } + + return 0; + }); + + return system; + } + + public function remove(systemType: Class): Bool { + var removed = false; + final name = Type.getClassName(systemType); + if (systems.exists(name)) { + final system = systems[name]; + system.destroy(); + + systems.remove(name); + systemList.remove(system); + removed = true; + } + + return removed; + } + + public inline function get(systemType: Class): T { + final name = Type.getClassName(systemType); + return cast systems[name]; + } + + public inline function has(systemType: Class): Bool { + final name = Type.getClassName(systemType); + return systems.exists(name); + } + + public function update(dt: Float) { + for (system in systemList) { + if (system.active) { + system.update(dt); + } + } + } + + public function render(graphics: Graphics) { + for (system in systemList) { + if (system.active) { + system.render(graphics, cameras); + } + } + + if (view.debugRender) { + for (system in systemList) { + if (system.active) { + system.debugRender(graphics, cameras); + } + } + } + + graphics.transform.identity(); + graphics.color.copyFrom(Color.WHITE); + + graphics.start(); + for (camera in cameras) { + tempPos.set(camera.screenBounds.x, camera.screenBounds.y); + graphics.drawRenderTarget(tempPos, camera.target); + } + graphics.present(); + } + + public function updateSystemEntities(entity: Entity, removed = false) { + for (system in systemList) { + system.updateEntityLists(entity, removed); + } + } + + public function destroy() { + for (system in systemList) { + system.destroy(); + } + } +} diff --git a/src/jume/ecs/Updatable.hx b/src/jume/ecs/Updatable.hx new file mode 100644 index 0000000..c5cb089 --- /dev/null +++ b/src/jume/ecs/Updatable.hx @@ -0,0 +1,5 @@ +package jume.ecs; + +interface Updatable { + function cUpdate(dt: Float): Void; +} diff --git a/src/jume/events/EventListener.hx b/src/jume/events/EventListener.hx index 09d11ee..ff3d89f 100644 --- a/src/jume/events/EventListener.hx +++ b/src/jume/events/EventListener.hx @@ -28,6 +28,11 @@ typedef EventListenerParams = { * Extra filter before receiving an event. */ var ?filter: (Dynamic)->Bool; + + /** + * Is this a game wide event, not tied to a scene. + */ + var global: Bool; } /** @@ -64,6 +69,11 @@ class EventListener { */ public final filter: (Dynamic)->Bool; + /** + * Is this a game wide event, not tied to a scene. + */ + public final global: Bool; + /** * Create a new EventListener instance. * @param params The listener input params. @@ -75,5 +85,6 @@ class EventListener { canCancel = params.canCancel; priority = params.priority; filter = params.filter; + global = params.global; } } diff --git a/src/jume/events/Events.hx b/src/jume/events/Events.hx index 4172b5a..04aabf8 100644 --- a/src/jume/events/Events.hx +++ b/src/jume/events/Events.hx @@ -30,6 +30,11 @@ typedef AddListenerParams = { * Optional extra filter before the callback receives and event. */ var ?filter: (T)->Bool; + + /** + * Is this a game wide event, not tied to a scene. + */ + var ?global: Bool; } /** @@ -55,7 +60,8 @@ class Events implements Service { callback: params.callback, canCancel: params.canCancel ?? true, priority: params.priority ?? 0, - filter: params.filter + filter: params.filter, + global: params.global ?? false }); if (listeners[params.type] == null) { @@ -121,6 +127,23 @@ class Events implements Service { event.put(); } + /** + * Clear all events. + * @param clearGlobal Also clear global events. + */ + public function clearEvents(clearGlobal = false) { + for (key in listeners.keys()) { + final list = listeners[key]; + var index = list.length - 1; + while (index >= 0) { + final listener = list[index]; + if (!listener.global || clearGlobal) { + list.remove(listener); + } + } + } + } + /** * Process the event with all callbacks. * @param event The event to process. diff --git a/src/jume/events/SceneEvent.hx b/src/jume/events/SceneEvent.hx new file mode 100644 index 0000000..cb438b6 --- /dev/null +++ b/src/jume/events/SceneEvent.hx @@ -0,0 +1,9 @@ +package jume.events; + +import jume.ecs.Scene; + +class SceneEvent extends Event { + public static final CHANGE: EventType = 'jume_scene_event'; + + var sceneType: Class; +} diff --git a/src/jume/tweens/Tweens.hx b/src/jume/tweens/Tweens.hx index f731fb5..2e36bb3 100644 --- a/src/jume/tweens/Tweens.hx +++ b/src/jume/tweens/Tweens.hx @@ -1,9 +1,11 @@ package jume.tweens; +import jume.di.Service; + /** * The tween manager class. */ -class Tweens { +class Tweens implements Service { /** * All tweens updated by the manager. */ @@ -191,4 +193,21 @@ class Tweens { current.remove(tween); } } + + /** + * Clear all the tween that are active. + */ + public function clearTweens() { + while (current.length > 0) { + current.pop(); + } + + while (sequences.length > 0) { + sequences.pop(); + } + + while (completed.length > 0) { + completed.pop(); + } + } } diff --git a/src/jume/view/Camera.hx b/src/jume/view/Camera.hx new file mode 100644 index 0000000..8201296 --- /dev/null +++ b/src/jume/view/Camera.hx @@ -0,0 +1,260 @@ +package jume.view; + +import jume.math.Size; +import jume.graphics.RenderTarget; +import jume.graphics.Color; +import jume.di.Injectable; +import jume.math.Mat4; +import jume.math.Rectangle; +import jume.math.Vec2; + +using jume.math.MathUtils; + +/** + * The camera renders everything in view. + */ +class Camera implements Injectable { + /** + * Only active cameras render actors. + */ + public var active: Bool; + + /** + * The camera position in pixels. + */ + public var position: Vec2; + + /** + * The camera rotation in degrees. + */ + public var rotation: Float; + + /** + * The camera zoom. 1.0 is no zoom. + */ + public var zoom: Float; + + /** + * The camera transform matrix. + */ + public var transform: Mat4; + + /** + * The camera background color. + */ + public var bgColor: Color; + + /** + * Layers skip when rendering. + */ + public var ignoredLayers: Array; + + /** + * The camera bounds in pixels. + */ + public var bounds(default, null) = new Rectangle(); + + /** + * Screen bounds are the bounds in view space. + */ + public var screenBounds(default, null) = new Rectangle(); + + /** + * The camera render target. + */ + public var target(default, null): RenderTarget; + + /** + * The rectangle of the space this camera takes up in the game view. + */ + var viewRect: Rectangle; + + /** + * Matrix used for transform calculations. + */ + var tempMatrix: Mat4; + + /** + * Used in screen to world. + */ + var tempPos: Vec2; + + /** + * View service reference. + */ + @:inject + var view: View; + + /** + * Create a new camera. + * @param options Creation options. + * @param view Injected view dependency. + */ + public function new(?options: CameraOptions) { + active = true; + position = new Vec2(); + rotation = 0; + zoom = 1; + transform = new Mat4(); + ignoredLayers = []; + viewRect = new Rectangle(); + tempMatrix = new Mat4(); + tempPos = new Vec2(); + + if (options != null) { + position.set(options.x ?? view.viewCenterX, options.y ?? view.viewCenterY); + rotation = options.rotation ?? 0.0; + zoom = options.zoom ?? 1.0; + bgColor = options.bgColor ?? Color.BLACK; + ignoredLayers = options.ignoredLayers ?? []; + updateView(options.viewX ?? 0, options.viewY ?? 0, options.viewWidth ?? 1, options.viewHeight ?? 1); + } else { + position.set(view.viewCenterX, view.viewCenterY); + updateView(0, 0, 1, 1); + } + updateBounds(); + } + + /** + * Update the camera transform. + */ + public function updateTransform() { + this.updateBounds(); + Mat4.fromTranslation(screenBounds.width * 0.5, screenBounds.height * 0.5, 0, transform); + Mat4.fromZRotation(Math.toRad(rotation), tempMatrix); + Mat4.multiply(transform, tempMatrix, transform); + + Mat4.fromScale(zoom, zoom, 1, tempMatrix); + Mat4.multiply(transform, tempMatrix, transform); + + Mat4.fromTranslation(-position.x, -position.y, 0, tempMatrix); + Mat4.multiply(transform, tempMatrix, transform); + } + + /** + * Update the camera size and position inside the game view. + * @param x The top left x position (0 - 1). + * @param y The top left y position (0 - 1). + * @param width The view width (0 - 1). + * @param height The view height (0 - 1). + */ + public function updateView(x: Float, y: Float, width: Float, height: Float) { + x = Math.clamp(x, 0, 1); + y = Math.clamp(y, 0, 1); + width = Math.clamp(width, 0, 1); + height = Math.clamp(height, 0, 1); + viewRect.set(x, y, width, height); + + screenBounds.set(x * view.viewWidth, y * view.viewHeight, width * view.viewWidth, height * view.viewHeight); + target = new RenderTarget(new Size(Std.int(width * view.viewWidth), Std.int(height * view.viewHeight))); + } + + /** + * Update the camera bounds. + */ + public function updateBounds() { + bounds.x = position.x - (screenBounds.width * 0.5) / zoom; + bounds.y = position.y - (screenBounds.height * 0.5) / zoom; + bounds.width = screenBounds.width / zoom; + bounds.height = screenBounds.height / zoom; + } + + /** + * Resize gets called when the canvas resizes. + */ + public function resize() { + updateView(viewRect.x, viewRect.y, viewRect.width, viewRect.height); + updateBounds(); + } + + /** + * Convert a screen position to a game world position. + * @param x The x position on the screen in pixels. + * @param y The y position on the screen in pixels. + * @param out Optional variable to store the result in. + * @return The converted position. + */ + public function screenToWorld(x: Float, y: Float, ?out: Vec2): Vec2 { + final tempX = position.x + - (screenBounds.width * 0.5) / zoom + + (x / (view.canvasWidth / view.pixelRatio)) * (screenBounds.width / zoom); + final tempY = position.y + - (screenBounds.height * 0.5) / zoom + + (y / (view.canvasHeight / view.pixelRatio)) * (screenBounds.height / zoom); + + tempPos.set(tempX, tempY); + + return Math.rotateAround(tempPos, position, -rotation, out); + } + + /** + * Convert a screen position to a game view position. + * @param x The x position on the screen in pixels. + * @param y The y position on the screen in pixels. + * @param out Optional variable to store the result in. + * @return The converted position. + */ + public function screenToView(x: Float, y: Float, ?out: Vec2): Vec2 { + if (out == null) { + out = Vec2.get(); + } + + return out.set((x / (view.canvasWidth / view.pixelRatio)) * view.viewWidth, + (y / (view.canvasHeight / view.pixelRatio)) * view.viewHeight); + } +} + +/** + * Camera creation options. + */ +typedef CameraOptions = { + /** + * The x position. + */ + var ?x: Float; + + /** + * The y position. + */ + var ?y: Float; + + /** + * The z rotation in degrees. + */ + var ?rotation: Float; + + /** + * The camera zoom. + */ + var ?zoom: Float; + + /** + * The x position of the top left on the canvas (0 - 1). + */ + var ?viewX: Float; + + /** + * The x position of the top left on the canvas (0 - 1). + */ + var ?viewY: Float; + + /** + * The x position of the top left on the canvas (0 - 1). + */ + var ?viewWidth: Float; + + /** + * The x position of the top left on the canvas (0 - 1). + */ + var ?viewHeight: Float; + + /** + * The camera background color. + */ + var ?bgColor: Color; + + /** + * Layers to skip when rendering. + */ + var ?ignoredLayers: Array; +} diff --git a/src/jume/view/View.hx b/src/jume/view/View.hx index 707dc91..6694649 100644 --- a/src/jume/view/View.hx +++ b/src/jume/view/View.hx @@ -50,6 +50,11 @@ typedef ViewParams = { * The view class has view related information like design, view, and window size. */ class View implements Service { + /** + * If true debug render systems and components. + */ + public var debugRender: Bool; + /** * The ratio between physical pixels and logical pixels. */