From e17e9b41ce113a386dd1c4820cfa5d94bcc5316d Mon Sep 17 00:00:00 2001 From: dragoncoder047 <101021094+dragoncoder047@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:26:12 -0400 Subject: [PATCH] complete platformEffector --- examples/platformBox.js | 75 ++++++++++++++++++++++++++ examples/platformEffector.js | 84 +++++++++++++++++++++++++++++ src/components/physics/effectors.ts | 51 +++++++++++++----- src/types.ts | 2 +- 4 files changed, 197 insertions(+), 15 deletions(-) create mode 100644 examples/platformBox.js create mode 100644 examples/platformEffector.js diff --git a/examples/platformBox.js b/examples/platformBox.js new file mode 100644 index 00000000..43f87201 --- /dev/null +++ b/examples/platformBox.js @@ -0,0 +1,75 @@ +// @ts-check + +kaplay(); + +loadSprite("bean", "/sprites/bean.png"); +loadSprite("coin", "/sprites/coin.png"); +loadSprite("grass", "/sprites/grass.png"); + +const SPEED = 480; + +setGravity(2400); + +const level = addLevel([ + "", + "= @ $$ =", + "==========", +], { + tileWidth: 64, + tileHeight: 64, + // Define what each symbol means (in components) + tiles: { + "@": () => [ + sprite("bean"), + area(), + body(), + anchor("bot"), + z(2), + "player", + ], + "=": () => [ + sprite("grass"), + area(), + body({ isStatic: true }), + anchor("bot"), + ], + "$": () => [ + sprite("coin"), + area(), + body(), + anchor("bot"), + platformEffector({ + shouldCollide(obj, normal) { + if (obj !== player) return true; + // Let the player push them if they hold shift + if (isKeyDown("shift")) return true; + if (normal.sub(LEFT).len() < Number.EPSILON) return false; + if (normal.sub(RIGHT).len() < Number.EPSILON) return false; + return true; + } + }), + ], + }, +}); + +// Get the player object from tag +const player = level.get("player")[0]; + +// Movements +onKeyPress("space", () => { + if (player.isGrounded()) { + player.jump(900); + } +}); + +onKeyDown("left", () => { + player.move(-SPEED, 0); +}); + +onKeyDown("right", () => { + player.move(SPEED, 0); +}); + +onUpdate(() => { + camPos(player.worldPos()); +}); diff --git a/examples/platformEffector.js b/examples/platformEffector.js new file mode 100644 index 00000000..613faf55 --- /dev/null +++ b/examples/platformEffector.js @@ -0,0 +1,84 @@ +// @ts-check + +kaplay(); + +loadSprite("bean", "/sprites/bean.png"); +loadSprite("steel", "/sprites/steel.png"); +loadSprite("grass", "/sprites/grass.png"); + +const SPEED = 480; + +setGravity(2400); + +const level = addLevel([ + " ##", + "", + " ##", + "", + " ##", + "", + " ##", + "", + "====##====", + "", + "= @ ## =", + "==========", +], { + tileWidth: 64, + tileHeight: 64, + // Define what each symbol means (in components) + tiles: { + "@": () => [ + sprite("bean"), + area(), + body(), + anchor("bot"), + z(2), + "player", + ], + "=": () => [ + sprite("grass"), + area(), + body({ isStatic: true }), + anchor("bot"), + ], + "#": () => [ + sprite("steel"), + area(), + body({ isStatic: true }), + anchor("bot"), + platformEffector(), + ], + }, +}); + +// Get the player object from tag +const player = level.get("player")[0]; + +// Always look at player +onUpdate(() => { + camPos(player.worldPos()); +}); + +// Movements +onKeyPress("space", () => { + if (player.isGrounded()) { + player.jump(900); + } +}); + +onKeyDown("left", () => { + player.move(-SPEED, 0); +}); + +onKeyDown("right", () => { + player.move(SPEED, 0); +}); + +// Fall through when down is pressed +onKeyDown("down", () => { + const p = player.curPlatform(); + if (p != null && p.is("platformEffector")) { + p.platformIgnore.add(player); + } +}); diff --git a/src/components/physics/effectors.ts b/src/components/physics/effectors.ts index 9ef14fa0..eb3198c8 100644 --- a/src/components/physics/effectors.ts +++ b/src/components/physics/effectors.ts @@ -1,4 +1,3 @@ -import { getGravityDirection } from "../../game"; import { Polygon, Vec2, vec2 } from "../../math"; import type { Comp, GameObj } from "../../types"; import type { PosComp } from "../transform"; @@ -117,8 +116,8 @@ export function pointEffector(opts: PointEffectorCompOpt): PointEffectorComp { const forceScale = this.forceMode === "constant" ? 1 : this.forceMode === "inverseLinear" - ? 1 / distance - : 1 / distance ** 2; + ? 1 / distance + : 1 / distance ** 2; const force = dir.scale( this.forceMagnitude * forceScale / length, ); @@ -153,31 +152,55 @@ export function constantForce(opts: ConstantForceCompOpt): ConstantForceComp { } export type PlatformEffectorCompOpt = { - surfaceArc?: number; - useOneWay?: boolean; + /** + * If the object is moving in a direction in here, it won't collide. + * + * Should be a list of unit vectors `LEFT`, `RIGHT`, `UP`, or `DOWN`. + */ + ignoreSides?: Vec2[] + /** + * A function that determines whether the object should collide. + * + * If present, it overrides the `ignoreSides`; if absent, it is + * automatically created from `ignoreSides`. + */ + shouldCollide?: (obj: GameObj, normal: Vec2) => boolean }; export interface PlatformEffectorComp extends Comp { - surfaceArc: number; - useOneWay: boolean; + /** + * A set of the objects that should not collide with this, because `shouldCollide` returned true. + * + * Objects in here are automatically removed when the stop colliding, so the casual user shouldn't + * need to touch this much. However, if an object is added to this set before the object collides + * with the platform effector, it won't collide even if `shouldCollide` returns true. + */ + platformIgnore: Set; } export function platformEffector( - opt: PlatformEffectorCompOpt, + opt: PlatformEffectorCompOpt = {}, ): PlatformEffectorComp { + opt.ignoreSides ??= [Vec2.UP]; + opt.shouldCollide ??= (_, normal) => { + return opt.ignoreSides?.findIndex(s => s.unit().sub(normal).slen() < Number.EPSILON) == -1; + } return { id: "platformEffector", require: ["area", "body"], - surfaceArc: opt.surfaceArc ?? 180, - useOneWay: opt.useOneWay ?? false, + platformIgnore: new Set(), add(this: GameObj) { this.onBeforePhysicsResolve(collision => { - const v = collision.target.vel; - const up = getGravityDirection().scale(-1); - const angle = up.angleBetween(v); - if (Math.abs(angle) > this.surfaceArc / 2) { + if (this.platformIgnore.has(collision.target)) { collision.preventResolution(); } + else if (!opt.shouldCollide!(collision.target, collision.normal)) { + collision.preventResolution(); + this.platformIgnore.add(collision.target); + } + }); + this.onCollideEnd(obj => { + this.platformIgnore.delete(obj); }); }, }; diff --git a/src/types.ts b/src/types.ts index 7ea978af..e6f17592 100644 --- a/src/types.ts +++ b/src/types.ts @@ -703,7 +703,7 @@ export interface KAPLAYCtx< * @since v3001.0 * @group Components */ - platformEffector(options: PlatformEffectorCompOpt): PlatformEffectorComp; + platformEffector(options?: PlatformEffectorCompOpt): PlatformEffectorComp; /** * Applies an upwards force (force against gravity) to colliding objects depending on the fluid density and submerged area. * Good to apply constant thrust.