UNPKG

mgk.js

Version:

epik framework for creating simple 2d games with web technologies, this package is new so 100% there might be bugs(entire package not working could also be the case) PLUSS this is open-source (can access it via github: https://github.com/helloworld190/MGK

436 lines (369 loc) 11.6 kB
// MiniGameKit.js import { UIContainer, UIButton, UILabel, UIElement } from './UI.js'; export class Component { constructor() { } start(go) { } update(go, dt) { } render(ctx, go) { } } export class Transform extends Component { constructor({ x = 0, y = 0, rotation = 0, angle = 0, scale = 1 } = {}) { super(); this.position = { x, y }; this.rotation = rotation; this.angle = angle; this.scale = scale; } } import { EventEmitter } from "./EventEmitter.js"; export class GameObject { constructor(scene) { this.scene = scene; this.components = []; this.plugins = []; this.eventEmitter = new EventEmitter(); } on(event, callback) { this.eventEmitter.on(event, callback); return this; } off(event, callback) { this.eventEmitter.off(event, callback); return this; } emit(event, ...args) { this.eventEmitter.emit(event, ...args); return this; } addComponent(comp) { this.components.push(comp); if (comp.start) comp.start(this); return this; } getComponent(type) { return this.components.find((c) => c instanceof type); } update(dt) { for (const c of this.components) if (c.update) c.update(this, dt); for (const p of this.plugins) if (p.update) p.update(this, dt); } render(ctx) { for (const c of this.components) if (c.render) c.render(ctx, this); for (const p of this.plugins) if (p.render) p.render(ctx, this); } addPlugin(plugin) { this.plugins.push(plugin); if (plugin.start) plugin.start(this); return this; } } import { Input } from './Input.js'; export class Game { constructor({ width = window.innerWidth, height = window.innerHeight, parent = document.body } = {}) { this.canvas = document.createElement("canvas"); this.canvas.width = width; this.canvas.height = height; parent.appendChild(this.canvas); this.ctx = this.canvas.getContext("2d"); this.objects = []; this.plugins = []; this.lastTime = 0; this.running = false; this.boundaryEnabled = false; this.input = new Input(this.canvas); this.uiRoot = new UIContainer({ x: 0, y: 0, width: this.canvas.width, height: this.canvas.height }); this.canvas.addEventListener('mousedown', (e) => { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.uiRoot.onPointerDown && this.uiRoot.onPointerDown(x, y, e); }); this.canvas.addEventListener('mouseup', (e) => { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.uiRoot.onPointerUp && this.uiRoot.onPointerUp(x, y, e); }); this.canvas.addEventListener('mousemove', (e) => { const rect = this.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; this.uiRoot.onPointerMove && this.uiRoot.onPointerMove(x, y, e); }); } enableBoundary(enable = true) { this.boundaryEnabled = enable; } update(dt) { // Update plugins first (e.g., CollisionSystem) for (const p of this.plugins) { if (p.update) p.update(this, dt); } // Update all game objects this.objects.forEach(obj => { obj.update(dt); if (this.boundaryEnabled) { this.handleBoundary(obj); } }); // Update UI root container if needed (optional) // If your UI components have update methods, you could: // this.uiRoot.update(dt); } render() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.objects.forEach(obj => obj.render(this.ctx)); for (const p of this.plugins) { if (p.render) p.render(this.ctx, this); } this.uiRoot.render(this.ctx); } handleBoundary(obj) { const t = obj.getComponent(Transform); const rb = obj.getComponent(RigidBody); const col = obj.getComponent(Collider); if (!t || !rb || !col) return; const radius = col.radius; if (t.position.x - radius < 0) { t.position.x = radius; rb.velocity.x = Math.abs(rb.velocity.x); } else if (t.position.x + radius > this.canvas.width) { t.position.x = this.canvas.width - radius; rb.velocity.x = -Math.abs(rb.velocity.x); } if (t.position.y - radius < 0) { t.position.y = radius; rb.velocity.y = Math.abs(rb.velocity.y); } else if (t.position.y + radius > this.canvas.height) { t.position.y = this.canvas.height - radius; rb.velocity.y = -Math.abs(rb.velocity.y); } } } export class RigidBody extends Component { constructor({ velocity = { x: 0, y: 0 }, angularVelocity = 0, gravity = 980, friction = 0.05, restitution = 0.7, mass = 1, buoyancy = 0, } = {}) { super(); this.velocity = { ...velocity }; this.angularVelocity = angularVelocity; this.gravity = gravity; this.friction = friction; this.restitution = restitution; this.mass = mass <= 0 ? 1 : mass; this.buoyancy = buoyancy; } update(go, dt) { const t = go.getComponent(Transform); if (!t) return; this.velocity.y += (this.gravity - this.buoyancy) * dt; this.velocity.x *= 1 - this.friction; this.velocity.y *= 1 - this.friction; t.position.x += this.velocity.x * dt; t.position.y += this.velocity.y * dt; t.angle += this.angularVelocity * dt; } } export class GameObject { constructor(scene) { this.scene = scene; this.components = []; this.plugins = []; this.startedComponents = new Set(); } addComponent(comp) { this.components.push(comp); if (!this.startedComponents.has(comp) && comp.start) { comp.start(this); this.startedComponents.add(comp); } return this; } getComponent(type) { return this.components.find((c) => c instanceof type); } update(dt) { for (const c of this.components) { if (c.update) c.update(this, dt); } for (const p of this.plugins) if (p.update) p.update(this, dt); } render(ctx) { for (const c of this.components) if (c.render) c.render(ctx, this); for (const p of this.plugins) if (p.render) p.render(ctx, this); } addPlugin(plugin) { this.plugins.push(plugin); if (plugin.start) plugin.start(this); return this; } destroy() { if (this.scene) { const index = this.scene.objects.indexOf(this); if (index !== -1) this.scene.objects.splice(index, 1); } this.components.length = 0; this.plugins.length = 0; } } export class Collider extends Component { constructor({ radius = 10 } = {}) { super(); this.radius = radius; } } export class CollisionSystem extends Plugin { start(scene) { this.scene = scene; } update(scene, dt) { const objs = scene.objects; for (let i = 0; i < objs.length; i++) { const a = objs[i]; const aCollider = a.getComponent(Collider); if (!aCollider) continue; const aTransform = a.getComponent(Transform); const aRigid = a.getComponent(RigidBody); for (let j = i + 1; j < objs.length; j++) { const b = objs[j]; const bCollider = b.getComponent(Collider); if (!bCollider) continue; const bTransform = b.getComponent(Transform); const bRigid = b.getComponent(RigidBody); const dx = bTransform.position.x - aTransform.position.x; const dy = bTransform.position.y - aTransform.position.y; const dist = Math.hypot(dx, dy); const minDist = aCollider.radius + bCollider.radius; if (dist < minDist) { const overlap = minDist - dist; const nx = dx / dist || 0; const ny = dy / dist || 0; if (aRigid && bRigid) { const totalMass = aRigid.mass + bRigid.mass; const correctionA = (overlap * (bRigid.mass / totalMass)); const correctionB = (overlap * (aRigid.mass / totalMass)); aTransform.position.x -= nx * correctionA; aTransform.position.y -= ny * correctionA; bTransform.position.x += nx * correctionB; bTransform.position.y += ny * correctionB; const relVelX = bRigid.velocity.x - aRigid.velocity.x; const relVelY = bRigid.velocity.y - aRigid.velocity.y; const velAlongNormal = relVelX * nx + relVelY * ny; if (velAlongNormal > 0) continue; const e = Math.min(aRigid.restitution, bRigid.restitution); const j = -(1 + e) * velAlongNormal / (1 / aRigid.mass + 1 / bRigid.mass); const impulseX = j * nx; const impulseY = j * ny; aRigid.velocity.x -= impulseX / aRigid.mass; aRigid.velocity.y -= impulseY / aRigid.mass; bRigid.velocity.x += impulseX / bRigid.mass; bRigid.velocity.y += impulseY / bRigid.mass; } } } } } } export class Circle extends Component { constructor({ radius = 20, fill = "cyan" } = {}) { super(); this.radius = radius; this.fill = fill; } render(ctx, go) { const t = go.getComponent(Transform); if (!t) return; ctx.save(); ctx.translate(t.position.x, t.position.y); ctx.rotate(t.angle); ctx.beginPath(); ctx.arc(0, 0, this.radius, 0, Math.PI * 2); ctx.fillStyle = this.fill; ctx.shadowColor = "rgba(0,255,255,0.6)"; ctx.shadowBlur = 12; ctx.fill(); ctx.restore(); } } export class SpriteRenderer extends Component { constructor({ url, frameWidth = null, frameHeight = null, frameCount = 1, frameSpeed = 10, loop = true, scale = 1, } = {}) { super(); this.url = url; this.image = new Image(); this.image.src = url; this.frameWidth = frameWidth; this.frameHeight = frameHeight; this.frameCount = frameCount; this.frameSpeed = frameSpeed; this.loop = loop; this.scale = scale; this.currentFrame = 0; this.accumulator = 0; this.loaded = false; this.image.onload = () => { this.loaded = true; if (!this.frameWidth) this.frameWidth = this.image.width; if (!this.frameHeight) this.frameHeight = this.image.height; }; } update(go, dt) { if (this.frameCount > 1 && this.loaded) { this.accumulator += dt; const frameDuration = 1 / this.frameSpeed; while (this.accumulator >= frameDuration) { this.accumulator -= frameDuration; this.currentFrame++; if (this.currentFrame >= this.frameCount) { if (this.loop) this.currentFrame = 0; else this.currentFrame = this.frameCount - 1; } } } } render(ctx, go) { if (!this.loaded) return; const t = go.getComponent(Transform); if (!t) return; ctx.save(); ctx.translate(t.position.x, t.position.y); ctx.rotate(t.angle); const sx = this.currentFrame * this.frameWidth; const sy = 0; const sw = this.frameWidth; const sh = this.frameHeight; const dw = sw * this.scale; const dh = sh * this.scale; ctx.drawImage(this.image, sx, sy, sw, sh, -dw / 2, -dh / 2, dw, dh); ctx.restore(); } } export class Plugin { constructor() { } start(target) { } update(target, dt) { } render(ctx, target) { } } export function createPlugin({ start, update, render }) { return class extends Plugin { constructor() { super(); if (start) this.start = start; if (update) this.update = update; if (render) this.render = render; } }; }