From 08e65c32b5611b787c80337ba4a0746556835490 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 4 Apr 2020 21:00:32 -0700 Subject: [PATCH] Overhaul the Platformer Physics System * Player character now experiences acceleration and friction when walking around the map! * Actor position and movement had to be converted from int's (render.Point) to float64's to support fine-grained acceleration steps. * Added "physics" package and physics.Vector to be a float64 counterpart for render.Point. Vector is used for uix.Actor.Position() for the sake of movement math. Vector is flattened back to a render.Point for collision purposes, since the levels and hitboxes are pixel-bound. * Refactor the uix.Actor to no longer extend the doodads.Drawing (so it can have a Position that's a Vector instead of a Point). This broke some code that expected `.Doodad` to directly reference the Drawing.Doodad: now you had to refer to it as `a.Drawing.Doodad` which was ugly. Added convenience method .Doodad() for a shortcut. * Moved functions like GetBoundingRect() from doodads package to collision, where it uses its own slimmer Actor interface for just the relevant methods it needs. --- dev-assets/doodads/azulian/azulian-red.js | 8 +- dev-assets/doodads/buttons/button.js | 4 +- dev-assets/doodads/buttons/sticky.js | 2 +- dev-assets/doodads/doors/colored-door.js | 2 +- dev-assets/doodads/doors/electric-door.js | 4 +- dev-assets/doodads/doors/keys.js | 4 +- dev-assets/doodads/doors/locked-door.js | 2 +- dev-assets/doodads/mischievous.js | 2 +- dev-assets/doodads/objects/exit-flag.js | 2 +- dev-assets/doodads/on-off/state-button.js | 2 +- dev-assets/doodads/switches/switch.js | 2 +- dev-assets/doodads/trapdoors/down.js | 2 +- dev-assets/doodads/trapdoors/trapdoor.js | 2 +- dev-assets/guidebook/pages/DoodadScripts.md | 12 +-- docs/public/Doodad Scripts.md | 14 +-- pkg/balance/numbers.go | 6 +- pkg/collision/bounding_rect.go | 42 ++++++++ pkg/collision/collide_actors.go | 9 ++ pkg/collision/collide_level.go | 5 +- pkg/doodads/actor.go | 52 +--------- pkg/doodads/drawing.go | 4 +- pkg/doodads/dummy/dummy.go | 12 ++- pkg/editor_ui.go | 2 +- pkg/fps.go | 2 +- pkg/physics/math.go | 15 +++ pkg/physics/math_test.go | 38 +++++++ pkg/physics/movement.go | 33 ++++++ pkg/physics/vector.go | 54 ++++++++++ pkg/physics/vector_test.go | 53 ++++++++++ pkg/play_inventory.go | 4 +- pkg/play_scene.go | 106 +++++++++++++++----- pkg/scripting/vm.go | 2 + pkg/uix/actor.go | 75 ++++++++++++-- pkg/uix/actor_animation.go | 4 +- pkg/uix/actor_collision.go | 41 +++++--- pkg/uix/canvas_actors.go | 2 +- pkg/uix/canvas_link_tool.go | 2 +- 37 files changed, 481 insertions(+), 146 deletions(-) create mode 100644 pkg/collision/bounding_rect.go create mode 100644 pkg/physics/math.go create mode 100644 pkg/physics/math_test.go create mode 100644 pkg/physics/movement.go create mode 100644 pkg/physics/vector.go create mode 100644 pkg/physics/vector_test.go diff --git a/dev-assets/doodads/azulian/azulian-red.js b/dev-assets/doodads/azulian/azulian-red.js index f631e40..ca87ff3 100644 --- a/dev-assets/doodads/azulian/azulian-red.js +++ b/dev-assets/doodads/azulian/azulian-red.js @@ -1,5 +1,5 @@ function main() { - log.Info("Azulian '%s' initialized!", Self.Doodad.Title); + log.Info("Azulian '%s' initialized!", Self.Doodad().Title); var playerSpeed = 4; var gravity = 4; @@ -28,8 +28,10 @@ function main() { } sampleTick++; - var Vx = playerSpeed * (direction === "left" ? -1 : 1); - Self.SetVelocity(Point(Vx, 0)); + // TODO: Vector() requires floats, pain in the butt for JS, + // the JS API should be friendlier and custom... + var Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)); + Self.SetVelocity(Vector(Vx, 0.0)); if (!Self.IsAnimating()) { Self.PlayAnimation("walk-"+direction, null); diff --git a/dev-assets/doodads/buttons/button.js b/dev-assets/doodads/buttons/button.js index 7e48193..976c2d6 100644 --- a/dev-assets/doodads/buttons/button.js +++ b/dev-assets/doodads/buttons/button.js @@ -1,5 +1,5 @@ function main() { - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); var timer = 0; @@ -28,7 +28,7 @@ function main() { }); // Events.OnLeave(function(e) { - // console.log("%s has stopped touching %s", e, Self.Doodad.Title) + // console.log("%s has stopped touching %s", e, Self.Doodad().Title) // Self.Canvas.SetBackground(RGBA(0, 0, 1, 0)); // }) } diff --git a/dev-assets/doodads/buttons/sticky.js b/dev-assets/doodads/buttons/sticky.js index ed4113d..8750fad 100644 --- a/dev-assets/doodads/buttons/sticky.js +++ b/dev-assets/doodads/buttons/sticky.js @@ -1,5 +1,5 @@ function main() { - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); var pressed = false; diff --git a/dev-assets/doodads/doors/colored-door.js b/dev-assets/doodads/doors/colored-door.js index 48015d8..a221b58 100644 --- a/dev-assets/doodads/doors/colored-door.js +++ b/dev-assets/doodads/doors/colored-door.js @@ -1,6 +1,6 @@ function main() { - var color = Self.Doodad.Tag("color"); + var color = Self.Doodad().Tag("color"); var keyname = "key-" + color + ".doodad"; // Layers in the doodad image. diff --git a/dev-assets/doodads/doors/electric-door.js b/dev-assets/doodads/doors/electric-door.js index 9aa75af..ab6dc54 100644 --- a/dev-assets/doodads/doors/electric-door.js +++ b/dev-assets/doodads/doors/electric-door.js @@ -1,5 +1,5 @@ function main() { - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); Self.AddAnimation("open", 100, [0, 1, 2, 3]); Self.AddAnimation("close", 100, [3, 2, 1, 0]); @@ -9,7 +9,7 @@ function main() { Self.SetHitbox(16, 0, 32, 64); Message.Subscribe("power", function(powered) { - console.log("%s got power=%+v", Self.Doodad.Title, powered); + console.log("%s got power=%+v", Self.Doodad().Title, powered); if (powered) { if (animating || opened) { diff --git a/dev-assets/doodads/doors/keys.js b/dev-assets/doodads/doors/keys.js index 667022e..ee7586a 100644 --- a/dev-assets/doodads/doors/keys.js +++ b/dev-assets/doodads/doors/keys.js @@ -1,9 +1,9 @@ function main() { - var color = Self.Doodad.Tag("color"); + var color = Self.Doodad().Tag("color"); Events.OnCollide(function(e) { if (e.Settled) { - e.Actor.AddItem(Self.Doodad.Filename, 0); + e.Actor.AddItem(Self.Doodad().Filename, 0); Self.Destroy(); } }) diff --git a/dev-assets/doodads/doors/locked-door.js b/dev-assets/doodads/doors/locked-door.js index 3ad7a8a..e1f9f3f 100644 --- a/dev-assets/doodads/doors/locked-door.js +++ b/dev-assets/doodads/doors/locked-door.js @@ -2,7 +2,7 @@ function main() { Self.AddAnimation("open", 0, [1]); var unlocked = false; - var color = Self.Doodad.Tag("color"); + var color = Self.Doodad().Tag("color"); Self.SetHitbox(16, 0, 32, 64); diff --git a/dev-assets/doodads/mischievous.js b/dev-assets/doodads/mischievous.js index b694f08..c7b4e0b 100644 --- a/dev-assets/doodads/mischievous.js +++ b/dev-assets/doodads/mischievous.js @@ -1,5 +1,5 @@ function main() { - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); console.log(Object.keys(console)); console.log(Object.keys(log)); diff --git a/dev-assets/doodads/objects/exit-flag.js b/dev-assets/doodads/objects/exit-flag.js index df4aec0..215ad05 100644 --- a/dev-assets/doodads/objects/exit-flag.js +++ b/dev-assets/doodads/objects/exit-flag.js @@ -1,6 +1,6 @@ // Exit Flag. function main() { - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); Self.SetHitbox(22+16, 16, 75-16, 86); Events.OnCollide(function(e) { diff --git a/dev-assets/doodads/on-off/state-button.js b/dev-assets/doodads/on-off/state-button.js index 8770b08..88c06ed 100644 --- a/dev-assets/doodads/on-off/state-button.js +++ b/dev-assets/doodads/on-off/state-button.js @@ -4,7 +4,7 @@ var state = false; function main() { - console.log("%s ID '%s' initialized!", Self.Doodad.Title, Self.ID()); + console.log("%s ID '%s' initialized!", Self.Doodad().Title, Self.ID()); Self.SetHitbox(0, 0, 33, 33); // When the button is activated, don't keep toggling state until we're not diff --git a/dev-assets/doodads/switches/switch.js b/dev-assets/doodads/switches/switch.js index 489aedf..c5aff2e 100644 --- a/dev-assets/doodads/switches/switch.js +++ b/dev-assets/doodads/switches/switch.js @@ -1,5 +1,5 @@ function main() { - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); // Switch has two frames: // 0: Off diff --git a/dev-assets/doodads/trapdoors/down.js b/dev-assets/doodads/trapdoors/down.js index 1f383cb..2ed3907 100644 --- a/dev-assets/doodads/trapdoors/down.js +++ b/dev-assets/doodads/trapdoors/down.js @@ -1,5 +1,5 @@ function main() { - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); var timer = 0; diff --git a/dev-assets/doodads/trapdoors/trapdoor.js b/dev-assets/doodads/trapdoors/trapdoor.js index a5b4f71..9e2a27a 100644 --- a/dev-assets/doodads/trapdoors/trapdoor.js +++ b/dev-assets/doodads/trapdoors/trapdoor.js @@ -1,6 +1,6 @@ function main() { // What direction is the trapdoor facing? - var direction = Self.Doodad.Tag("direction"); + var direction = Self.Doodad().Tag("direction"); console.log("Trapdoor(%s) initialized", direction); var timer = 0; diff --git a/dev-assets/guidebook/pages/DoodadScripts.md b/dev-assets/guidebook/pages/DoodadScripts.md index c4dece8..6f189d2 100644 --- a/dev-assets/guidebook/pages/DoodadScripts.md +++ b/dev-assets/guidebook/pages/DoodadScripts.md @@ -16,7 +16,7 @@ function main() { // other doodads. // Logs go to the game's log file (standard output on Linux/Mac). - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); // If our doodad has 'solid' parts that should prohibit movement, // define the hitbox here. Coordinates are relative so 0,0 is the @@ -59,13 +59,13 @@ Self holds information about the current doodad. The full surface area of the Self object is subject to change, but some useful things you can access from it include: -* Self.Doodad: a pointer to the doodad's file data. - * Self.Doodad.Title: get the title of the doodad file. - * Self.Doodad.Author: the name of the author who wrote the doodad. - * Self.Doodad.Script: the doodad's JavaScript source code. Note that +* Self.Doodad(): a pointer to the doodad's file data. + * Self.Doodad().Title: get the title of the doodad file. + * Self.Doodad().Author: the name of the author who wrote the doodad. + * Self.Doodad().Script: the doodad's JavaScript source code. Note that modifying this won't have any effect in-game, as the script had already been loaded into the interpreter. - * Self.Doodad.GameVersion: the version of {{ app_name }} that was used + * Self.Doodad().GameVersion: the version of {{ app_name }} that was used when the doodad was created. ### Events diff --git a/docs/public/Doodad Scripts.md b/docs/public/Doodad Scripts.md index 72dc87a..b0948ce 100644 --- a/docs/public/Doodad Scripts.md +++ b/docs/public/Doodad Scripts.md @@ -14,7 +14,7 @@ Provide a script file with a `main` function: ```javascript function main() { - console.log("%s initialized!", Self.Doodad.Title); + console.log("%s initialized!", Self.Doodad().Title); var timer = 0; Events.OnCollide( function() { @@ -84,18 +84,18 @@ The global variable `Self` holds an API for the current doodad. The full surface area of this API is subject to change, but some useful examples you can do with this are as follows. -### Self.Doodad +### Self.Doodad() -Self.Doodad is a pointer into the doodad's metadata file. Not +Self.Doodad() is a pointer into the doodad's metadata file. Not all properties in there can be written to or read from the JavaScript engine, but some useful attributes are: -* `str Self.Doodad.Title`: the title of the doodad. -* `str Self.Doodad.Author`: the author name of the doodad. -* `str Self.Doodad.Script`: your own source code. Note that +* `str Self.Doodad().Title`: the title of the doodad. +* `str Self.Doodad().Author`: the author name of the doodad. +* `str Self.Doodad().Script`: your own source code. Note that editing this won't have any effect in-game, as your doodad's source has already been loaded into the interpreter. -* `str Self.Doodad.GameVersion`: the game version that created +* `str Self.Doodad().GameVersion`: the game version that created the doodad. ### Self.ShowLayer(int index) diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index bafe54f..1697cbf 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -14,9 +14,9 @@ var ( ScrollboxVert = 128 // Player speeds - PlayerMaxVelocity = 6 - PlayerAcceleration = 2 - Gravity = 6 + PlayerMaxVelocity float64 = 6 + PlayerAcceleration float64 = 0.2 + Gravity float64 = 6 // Default chunk size for canvases. ChunkSize = 128 diff --git a/pkg/collision/bounding_rect.go b/pkg/collision/bounding_rect.go new file mode 100644 index 0000000..cecc93d --- /dev/null +++ b/pkg/collision/bounding_rect.go @@ -0,0 +1,42 @@ +package collision + +import "git.kirsle.net/go/render" + +// GetBoundingRect computes the full pairs of points for the bounding box of +// the actor. +// +// The X,Y coordinates are the position in the level of the actor, +// The W,H are the size of the actor's drawn box. +func GetBoundingRect(a Actor) render.Rect { + var ( + P = a.Position() + S = a.Size() + ) + return render.Rect{ + X: P.X, + Y: P.Y, + W: S.W, + H: S.H, + } +} + +// GetBoundingRectHitbox returns the bounding rect of the Actor taking into +// account their self-declared collision hitbox. +// +// The rect returned has the X,Y coordinate set to the actor's position, plus +// the X,Y of their hitbox, if any. +// +// The W,H of the rect is the W,H of their declared hitbox. +// +// If the actor has NOT declared its hitbox, this function returns exactly the +// same way as GetBoundingRect() does. +func GetBoundingRectHitbox(a Actor, hitbox render.Rect) render.Rect { + rect := GetBoundingRect(a) + if !hitbox.IsZero() { + rect.X += hitbox.X + rect.Y += hitbox.Y + rect.W = hitbox.W + rect.H = hitbox.H + } + return rect +} diff --git a/pkg/collision/collide_actors.go b/pkg/collision/collide_actors.go index 90bc369..b38d412 100644 --- a/pkg/collision/collide_actors.go +++ b/pkg/collision/collide_actors.go @@ -7,6 +7,15 @@ import ( "git.kirsle.net/go/render" ) +// Actor is a subset of the uix.Actor interface with just the methods needed +// for collision checking purposes. +type Actor interface { + Position() render.Point + Size() render.Rect + Grounded() bool + SetGrounded(bool) +} + // BoxCollision holds the result of a collision BetweenBoxes. type BoxCollision struct { // A and B are the indexes of the boxes sent to BetweenBoxes. diff --git a/pkg/collision/collide_level.go b/pkg/collision/collide_level.go index b8376d2..19e9c4d 100644 --- a/pkg/collision/collide_level.go +++ b/pkg/collision/collide_level.go @@ -3,7 +3,6 @@ package collision import ( "sync" - "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/go/render" ) @@ -53,7 +52,7 @@ CollidesWithGrid checks if a Doodad collides with level geometry. The `target` is the point the actor wants to move to on this tick. */ -func CollidesWithGrid(d doodads.Actor, grid *level.Chunker, target render.Point) (*Collide, bool) { +func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Collide, bool) { var ( P = d.Position() S = d.Size() @@ -72,7 +71,7 @@ func CollidesWithGrid(d doodads.Actor, grid *level.Chunker, target render.Point) ) // Test all of the bounding boxes for a collision with level geometry. - if ok := result.ScanBoundingBox(doodads.GetBoundingRect(d), grid); ok { + if ok := result.ScanBoundingBox(GetBoundingRect(d), grid); ok { // We've already collided! Try to wiggle free. if result.Bottom { if !d.Grounded() { diff --git a/pkg/doodads/actor.go b/pkg/doodads/actor.go index 8e8891e..1d4e066 100644 --- a/pkg/doodads/actor.go +++ b/pkg/doodads/actor.go @@ -10,8 +10,8 @@ type Actor interface { ID() string // Position and velocity, not saved to disk. - Position() render.Point - Velocity() render.Point + Position() render.Point // DEPRECATED + Velocity() render.Point // DEPRECATED for uix.Actor Size() render.Rect Grounded() bool SetGrounded(bool) @@ -24,51 +24,3 @@ type Actor interface { MoveBy(render.Point) // Add {X,Y} to current Position. MoveTo(render.Point) // Set current Position to {X,Y}. } - -// GetBoundingRect computes the full pairs of points for the bounding box of -// the actor. -// -// The X,Y coordinates are the position in the level of the actor, -// The W,H are the size of the actor's drawn box. -func GetBoundingRect(d Actor) render.Rect { - var ( - P = d.Position() - S = d.Size() - ) - return render.Rect{ - X: P.X, - Y: P.Y, - W: S.W, - H: S.H, - } -} - -// GetBoundingRectHitbox returns the bounding rect of the Actor taking into -// account their self-declared collision hitbox. -// -// The rect returned has the X,Y coordinate set to the actor's position, plus -// the X,Y of their hitbox, if any. -// -// The W,H of the rect is the W,H of their declared hitbox. -// -// If the actor has NOT declared its hitbox, this function returns exactly the -// same way as GetBoundingRect() does. -func GetBoundingRectHitbox(d Actor, hitbox render.Rect) render.Rect { - rect := GetBoundingRect(d) - if !hitbox.IsZero() { - rect.X += hitbox.X - rect.Y += hitbox.Y - rect.W = hitbox.W - rect.H = hitbox.H - } - return rect -} - -// GetBoundingRectWithHitbox is like GetBoundingRect but adjusts it for the -// relative hitbox of the actor. -// func GetBoundingRectWithHitbox(d Actor, hitbox render.Rect) render.Rect { -// rect := GetBoundingRect(d) -// rect.W = hitbox.W -// rect.H = hitbox.H -// return rect -// } diff --git a/pkg/doodads/drawing.go b/pkg/doodads/drawing.go index e576926..2ccc1b5 100644 --- a/pkg/doodads/drawing.go +++ b/pkg/doodads/drawing.go @@ -20,11 +20,11 @@ type Drawing struct { // NewDrawing creates a Drawing actor based on a Doodad drawing. If you pass // an empty ID string, it will make a random UUIDv4 ID. -func NewDrawing(id string, doodad *Doodad) Drawing { +func NewDrawing(id string, doodad *Doodad) *Drawing { if id == "" { id = uuid.Must(uuid.NewRandom()).String() } - return Drawing{ + return &Drawing{ id: id, Doodad: doodad, size: doodad.Rect(), diff --git a/pkg/doodads/dummy/dummy.go b/pkg/doodads/dummy/dummy.go index ffcf784..00ba051 100644 --- a/pkg/doodads/dummy/dummy.go +++ b/pkg/doodads/dummy/dummy.go @@ -1,11 +1,14 @@ // Package dummy implements a dummy doodads.Drawing. package dummy -import "git.kirsle.net/apps/doodle/pkg/doodads" +import ( + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/go/render" +) // Drawing is a dummy doodads.Drawing that has no data. type Drawing struct { - doodads.Drawing + Drawing *doodads.Drawing } // NewDrawing creates a new dummy drawing. @@ -14,3 +17,8 @@ func NewDrawing(id string, doodad *doodads.Doodad) *Drawing { Drawing: doodads.NewDrawing(id, doodad), } } + +// Size returns the size of the underlying doodads.Drawing. +func (d *Drawing) Size() render.Rect { + return d.Drawing.Size() +} diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 22ffe67..0542b1c 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -348,7 +348,7 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { b.Actor.AddLink(idA) // Reset the Link tool. - d.Flash("Linked '%s' and '%s' together", a.Doodad.Title, b.Doodad.Title) + d.Flash("Linked '%s' and '%s' together", a.Doodad().Title, b.Doodad().Title) } // Set up the drop handler for draggable doodads. diff --git a/pkg/fps.go b/pkg/fps.go index caf4f7a..b1c820b 100644 --- a/pkg/fps.go +++ b/pkg/fps.go @@ -142,7 +142,7 @@ func (d *Doodle) DrawCollisionBox(actor doodads.Actor) { } var ( - rect = doodads.GetBoundingRect(actor) + rect = collision.GetBoundingRect(actor) box = collision.GetCollisionBox(rect) ) diff --git a/pkg/physics/math.go b/pkg/physics/math.go new file mode 100644 index 0000000..0dbd321 --- /dev/null +++ b/pkg/physics/math.go @@ -0,0 +1,15 @@ +package physics + +// Lerp performs linear interpolation between two numbers. +// +// a and b are the two bounds of the number, and t is a fraction between 0 and +// 1 that will return a number between a and b. If t=0, returns a; if t=1, +// returns b. +func Lerp(a, b, t float64) float64 { + return (1.0-t)*a + t*b +} + +// LerpInt runs lerp using integers. +func LerpInt(a, b int, t float64) float64 { + return Lerp(float64(a), float64(b), t) +} diff --git a/pkg/physics/math_test.go b/pkg/physics/math_test.go new file mode 100644 index 0000000..7bd385d --- /dev/null +++ b/pkg/physics/math_test.go @@ -0,0 +1,38 @@ +package physics_test + +import ( + "testing" + + "git.kirsle.net/apps/doodle/pkg/physics" +) + +func TestLerp(t *testing.T) { + var tests = []struct { + Inputs []float64 + Expect float64 + }{ + { + Inputs: []float64{0, 1, 0.75}, + Expect: 0.75, + }, + { + Inputs: []float64{0, 100, 0.5}, + Expect: 50.0, + }, + { + Inputs: []float64{10, 75, 0.3}, + Expect: 29.5, + }, + { + Inputs: []float64{30, 2, 0.75}, + Expect: 9, + }, + } + + for _, test := range tests { + result := physics.Lerp(test.Inputs[0], test.Inputs[1], test.Inputs[2]) + if result != test.Expect { + t.Errorf("Lerp(%+v) expected %f but got %f", test.Inputs, test.Expect, result) + } + } +} diff --git a/pkg/physics/movement.go b/pkg/physics/movement.go new file mode 100644 index 0000000..7f8d1f8 --- /dev/null +++ b/pkg/physics/movement.go @@ -0,0 +1,33 @@ +package physics + +// Mover is a moving object. +type Mover struct { + Acceleration float64 + Friction float64 + // Gravity Vector + + // // Position and previous frame's position. + // Position render.Point + // OldPosition render.Point + // + // // Speed and previous frame's speed. + // Speed render.Point + // OldSpeed render.Point + MaxSpeed Vector + // + // // Object is on the ground and its grounded state last frame. + // Grounded bool + // WasGrounded bool +} + +// NewMover initializes state for a moving object. +func NewMover() *Mover { + return &Mover{} +} + +// // UpdatePhysics runs calculations on the mover's physics each frame. +// func (m *Mover) UpdatePhysics() { +// m.OldPosition = m.Position +// m.OldSpeed = m.Speed +// m.WasGrounded = m.Grounded +// } diff --git a/pkg/physics/vector.go b/pkg/physics/vector.go new file mode 100644 index 0000000..351d0b5 --- /dev/null +++ b/pkg/physics/vector.go @@ -0,0 +1,54 @@ +package physics + +import ( + "fmt" + "math" + + "git.kirsle.net/go/render" +) + +// Vector holds floating point values on an X and Y coordinate. +type Vector struct { + X float64 + Y float64 +} + +// NewVector creates a Vector from X and Y values. +func NewVector(x, y float64) Vector { + return Vector{ + X: x, + Y: y, + } +} + +// VectorFromPoint converts a render.Point into a vector. +func VectorFromPoint(p render.Point) Vector { + return Vector{ + X: float64(p.X), + Y: float64(p.Y), + } +} + +// IsZero returns if the vector is zero. +func (v Vector) IsZero() bool { + return v.X == 0 && v.Y == 0 +} + +// Add to the vector. +func (v *Vector) Add(other Vector) { + v.X += other.X + v.Y += other.Y +} + +// ToPoint converts the vector into a render.Point with integer coordinates. +func (v Vector) ToPoint() render.Point { + return render.Point{ + X: int(math.Round(v.X)), + Y: int(math.Round(v.Y)), + } +} + +// String encoding of the vector. +func (v Vector) String() string { + return fmt.Sprintf("%f,%f", v.X, v.Y) +} diff --git a/pkg/physics/vector_test.go b/pkg/physics/vector_test.go new file mode 100644 index 0000000..27cccf1 --- /dev/null +++ b/pkg/physics/vector_test.go @@ -0,0 +1,53 @@ +package physics_test + +import ( + "testing" + + "git.kirsle.net/apps/doodle/pkg/physics" + "git.kirsle.net/go/render" +) + +// Test converting points to vectors and back again. +func TestVectorPoint(t *testing.T) { + var tests = []struct { + In render.Point + Mid physics.Vector + Add physics.Vector + Out render.Point + }{ + { + In: render.NewPoint(102, 102), + Mid: physics.NewVector(102.0, 102.0), + Out: render.NewPoint(102, 102), + }, + { + In: render.NewPoint(64, 128), + Mid: physics.NewVector(64.0, 128.0), + Add: physics.NewVector(0.4, 0.6), + Out: render.NewPoint(64, 129), + }, + } + + for _, test := range tests { + // Convert point to vector. + v := physics.VectorFromPoint(test.In) + if v != test.Mid { + t.Errorf("Unexpected Vector from Point(%s): wanted %s, got %s", + test.In, test.Mid, v, + ) + continue + } + + // Add other vector. + v.Add(test.Add) + + // Verify output point rounded down correctly. + out := v.ToPoint() + if out != test.Out { + t.Errorf("Unexpected output vector from Point(%s) -> V(%s) + V(%s): wanted %s, got %s", + test.In, test.Mid, test.Add, test.Out, out, + ) + continue + } + } +} diff --git a/pkg/play_inventory.go b/pkg/play_inventory.go index d92c091..8fa18ff 100644 --- a/pkg/play_inventory.go +++ b/pkg/play_inventory.go @@ -39,8 +39,8 @@ func (s *PlayScene) setupInventoryHud() { // Add the inventory frame to the screen frame. s.screen.Place(s.invenFrame, ui.Place{ - Left: 40, - Bottom: 40, + Top: 40, + Right: 40, }) // Hide inventory if empty. diff --git a/pkg/play_scene.go b/pkg/play_scene.go index cd39d93..f6da15d 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -8,6 +8,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/physics" "git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/go/render" @@ -51,6 +52,7 @@ type PlayScene struct { // Player character Player *uix.Actor + playerPhysics *physics.Mover antigravity bool // Cheat: disable player gravity noclip bool // Cheat: disable player clipping playerJumpCounter int // limit jump length @@ -235,6 +237,14 @@ func (s *PlayScene) setupPlayer() { s.drawing.AddActor(s.Player) s.drawing.FollowActor = s.Player.ID() + // Set up the movement physics for the player. + s.playerPhysics = &physics.Mover{ + MaxSpeed: physics.NewVector(balance.PlayerMaxVelocity, balance.PlayerMaxVelocity), + // Gravity: physics.NewVector(balance.Gravity, balance.Gravity), + Acceleration: 0.025, + Friction: 0.1, + } + // Set up the player character's script in the VM. if err := s.scripting.AddLevelScript(s.Player.ID()); err != nil { log.Error("PlayScene.Setup: scripting.InstallActor(player) failed: %s", err) @@ -412,7 +422,7 @@ func (s *PlayScene) Draw(d *Doodle) error { s.drawing.Present(d.Engine, s.drawing.Point()) // Draw out bounding boxes. - d.DrawCollisionBox(s.Player) + d.DrawCollisionBox(s.Player.Drawing) // Draw the UI screen and any widgets that attached to it. s.screen.Compute(d.Engine) @@ -445,35 +455,79 @@ func (s *PlayScene) Draw(d *Doodle) error { // movePlayer updates the player's X,Y coordinate based on key pressed. func (s *PlayScene) movePlayer(ev *event.State) { - var playerSpeed = balance.PlayerMaxVelocity + var ( + playerSpeed = float64(balance.PlayerMaxVelocity) + velocity = s.Player.Velocity() + direction float64 + jumping bool + ) - // If antigravity enabled and the Shift key is pressed down, move the - // player by only one pixel per tick. - if s.antigravity && ev.Shift { - playerSpeed = 1 - } + // Antigravity: player can move anywhere with arrow keys. + if s.antigravity { + velocity.X = 0 + velocity.Y = 0 - var velocity render.Point - - if ev.Left { - velocity.X = -playerSpeed - } - if ev.Right { - velocity.X = playerSpeed - } - if ev.Up && (s.Player.Grounded() || s.playerJumpCounter >= 0 || s.antigravity) { - velocity.Y = -playerSpeed - - if s.Player.Grounded() { - s.playerJumpCounter = 12 + // Shift to slow your roll to 1 pixel per tick. + if ev.Shift { + playerSpeed = 1 } - } - if ev.Down && s.antigravity { - velocity.Y = playerSpeed - } - if !s.Player.Grounded() { - s.playerJumpCounter-- + if ev.Left { + velocity.X = -playerSpeed + } else if ev.Right { + velocity.X = playerSpeed + } + if ev.Up { + velocity.Y = -playerSpeed + } else if ev.Down { + velocity.Y = playerSpeed + } + } else { + // Moving left or right. + if ev.Left { + direction = -1 + } else if ev.Right { + direction = 1 + } + + // Up button to signal they want to jump. + if ev.Up && (s.Player.Grounded() || s.playerJumpCounter >= 0) { + jumping = true + + if s.Player.Grounded() { + // Allow them to sustain the jump this many ticks. + s.playerJumpCounter = 32 + } + } + + // Moving left or right? Interpolate their velocity by acceleration. + if direction != 0 { + // TODO: fast turn-around if they change directions so they don't + // slip and slide while their velocity updates. + velocity.X = physics.Lerp( + velocity.X, + direction*s.playerPhysics.MaxSpeed.X, + s.playerPhysics.Acceleration, + ) + } else { + // Slow them back to zero using friction. + velocity.X = physics.Lerp( + velocity.X, + 0, + s.playerPhysics.Friction, + ) + } + + // Moving upwards (jumping): give them full acceleration upwards. + if jumping { + velocity.Y = -playerSpeed + } + + // While in the air, count down their jump counter; when zero they + // cannot jump again until they touch ground. + if !s.Player.Grounded() { + s.playerJumpCounter-- + } } s.Player.SetVelocity(velocity) diff --git a/pkg/scripting/vm.go b/pkg/scripting/vm.go index d3cfdef..ee0f322 100644 --- a/pkg/scripting/vm.go +++ b/pkg/scripting/vm.go @@ -7,6 +7,7 @@ import ( "time" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/physics" "git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/go/render" "github.com/robertkrimen/otto" @@ -73,6 +74,7 @@ func (vm *VM) RegisterLevelHooks() error { "Flash": shmem.Flash, "RGBA": render.RGBA, "Point": render.NewPoint, + "Vector": physics.NewVector, "Self": vm.Self, // i.e., the uix.Actor object "Events": vm.Events, "GetTick": func() uint64 { diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 62096af..1c8c8db 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -6,9 +6,11 @@ import ( "sort" "sync" + "git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/physics" "git.kirsle.net/go/render" "github.com/google/uuid" "github.com/robertkrimen/otto" @@ -23,9 +25,10 @@ import ( // as defined in the map: its spawn coordinate and configuration. // - A uix.Canvas that can present the actor's graphics to the screen. type Actor struct { - doodads.Drawing - Actor *level.Actor - Canvas *Canvas + id string + Drawing *doodads.Drawing + Actor *level.Actor + Canvas *Canvas activeLayer int // active drawing frame for display flagDestroy bool // flag the actor for destruction @@ -38,6 +41,11 @@ type Actor struct { inventory map[string]int // item inventory. doodad name -> quantity, 0 for key item. data map[string]string // arbitrary key/value store. DEPRECATED ?? + // Movement data. + position render.Point + velocity physics.Vector + grounded bool + // Animation variables. animations map[string]*Animation activeAnimation *Animation @@ -81,6 +89,17 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor return actor } +// ID returns the actor's ID. This is the underlying doodle.Drawing.ID(). +func (a *Actor) ID() string { + return a.Drawing.ID() +} + +// Doodad offers access to the underlying Doodad object. +// Shortcut to the `.Drawing.Doodad` property path. +func (a *Actor) Doodad() *doodads.Doodad { + return a.Drawing.Doodad +} + // SetGravity configures whether the actor is affected by gravity. func (a *Actor) SetGravity(v bool) { a.hasGravity = v @@ -98,6 +117,46 @@ func (a *Actor) IsMobile() bool { return a.isMobile } +// Size returns the size of the actor, from the underlying doodads.Drawing. +func (a *Actor) Size() render.Rect { + return a.Drawing.Size() +} + +// Velocity returns the actor's current velocity vector. +func (a *Actor) Velocity() physics.Vector { + return a.velocity +} + +// SetVelocity updates the actor's velocity vector. +func (a *Actor) SetVelocity(v physics.Vector) { + a.velocity = v +} + +// Position returns the actor's position. +func (a *Actor) Position() render.Point { + return a.position +} + +// MoveTo sets the actor's position. +func (a *Actor) MoveTo(p render.Point) { + a.position = p +} + +// MoveBy adjusts the actor's position. +func (a *Actor) MoveBy(p render.Point) { + a.position.Add(p) +} + +// Grounded returns if the actor is touching a floor. +func (a *Actor) Grounded() bool { + return a.grounded +} + +// SetGrounded sets the actor's grounded value. +func (a *Actor) SetGrounded(v bool) { + a.grounded = v +} + // SetNoclip sets the noclip setting for an actor. If true, the actor can // clip through level geometry. func (a *Actor) SetNoclip(v bool) { @@ -187,7 +246,7 @@ func (a *Actor) Inventory() map[string]int { // GetBoundingRect gets the bounding box of the actor's doodad. func (a *Actor) GetBoundingRect() render.Rect { - return doodads.GetBoundingRect(a) + return collision.GetBoundingRect(a) } // SetHitbox sets the actor's elected hitbox. @@ -233,26 +292,26 @@ func (a *Actor) GetData(key string) string { // LayerCount returns the number of layers in this actor's drawing. func (a *Actor) LayerCount() int { - return len(a.Doodad.Layers) + return len(a.Doodad().Layers) } // ShowLayer sets the actor's ActiveLayer to the index given. func (a *Actor) ShowLayer(index int) error { if index < 0 { return errors.New("layer index must be 0 or greater") - } else if index > len(a.Doodad.Layers) { + } else if index > len(a.Doodad().Layers) { return fmt.Errorf("layer %d out of range for doodad's layers", index) } a.activeLayer = index - a.Canvas.Load(a.Doodad.Palette, a.Doodad.Layers[index].Chunker) + a.Canvas.Load(a.Doodad().Palette, a.Doodad().Layers[index].Chunker) return nil } // ShowLayerNamed sets the actor's ActiveLayer to the one named. func (a *Actor) ShowLayerNamed(name string) error { // Find the layer. - for i, layer := range a.Doodad.Layers { + for i, layer := range a.Doodad().Layers { if layer.Name == name { return a.ShowLayer(i) } diff --git a/pkg/uix/actor_animation.go b/pkg/uix/actor_animation.go index ea25dd1..8d70557 100644 --- a/pkg/uix/actor_animation.go +++ b/pkg/uix/actor_animation.go @@ -61,7 +61,7 @@ func (a *Actor) AddAnimation(name string, interval int64, layers []interface{}) switch v := name.(type) { case string: var found bool - for i, layer := range a.Doodad.Layers { + for i, layer := range a.Doodad().Layers { if layer.Name == v { indexes = append(indexes, i) found = true @@ -80,7 +80,7 @@ func (a *Actor) AddAnimation(name string, interval int64, layers []interface{}) } iv := int(v) - if iv < len(a.Doodad.Layers) { + if iv < len(a.Doodad().Layers) { indexes = append(indexes, iv) } else { return fmt.Errorf("layer numbered '%d' is out of bounds", iv) diff --git a/pkg/uix/actor_collision.go b/pkg/uix/actor_collision.go index 0f557aa..b56cb76 100644 --- a/pkg/uix/actor_collision.go +++ b/pkg/uix/actor_collision.go @@ -7,8 +7,8 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/collision" - "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/physics" "git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/go/render" "github.com/robertkrimen/otto" @@ -63,23 +63,36 @@ func (w *Canvas) loopActorCollision() error { // Get the actor's velocity to see if it's moving this tick. v := a.Velocity() - if a.hasGravity && v.Y >= 0 { - v.Y += balance.Gravity + + // Apply gravity to the actor's velocity. + if a.hasGravity && !a.Grounded() { //v.Y >= 0 { + if !a.Grounded() { + v.Y = physics.Lerp( + v.Y, // current speed + balance.Gravity, // target max gravity falling downwards + balance.PlayerAcceleration, + ) + } else { + v.Y = 0 + } + a.SetVelocity(v) + // v.Y += balance.Gravity } // If not moving, grab the bounding box right now. - if v == render.Origin { - boxes[i] = doodads.GetBoundingRect(a) + if v.IsZero() { + boxes[i] = collision.GetBoundingRect(a) return } // Create a delta point from their current location to where they // want to move to this tick. - delta := a.Position() + delta := physics.VectorFromPoint(a.Position()) delta.Add(v) // Check collision with level geometry. - info, ok := collision.CollidesWithGrid(a, w.chunks, delta) + chkPoint := delta.ToPoint() + info, ok := collision.CollidesWithGrid(a, w.chunks, chkPoint) if ok { // Collision happened with world. if w.OnLevelCollision != nil { @@ -89,17 +102,17 @@ func (w *Canvas) loopActorCollision() error { // Move us back where the collision check put us if !a.noclip { - delta = info.MoveTo + delta = physics.VectorFromPoint(info.MoveTo) } // Move the actor's World Position to the new location. - a.MoveTo(delta) + a.MoveTo(delta.ToPoint()) // Keep the actor from leaving the world borders of bounded maps. w.loopContainActorsInsideLevel(a) // Store this actor's bounding box after they've moved. - boxes[i] = doodads.GetBoundingRect(a) + boxes[i] = collision.GetBoundingRect(a) }(i, a) wg.Wait() } @@ -115,10 +128,12 @@ func (w *Canvas) loopActorCollision() error { collidingActors[a.ID()] = b.ID() + // log.Error("between boxes: %+v <%s> <%s>", tuple, a.ID(), b.ID()) + // Call the OnCollide handler for A informing them of B's intersection. if w.scripting != nil { var ( - rect = doodads.GetBoundingRect(b) + rect = collision.GetBoundingRect(b) lastGoodBox = render.Rect{ X: originalPositions[b.ID()].X, Y: originalPositions[b.ID()].Y, @@ -190,7 +205,7 @@ func (w *Canvas) loopActorCollision() error { // Did A protest? if err == scripting.ErrReturnFalse { // Are they on top? - aHitbox := doodads.GetBoundingRectHitbox(a, a.Hitbox()) + aHitbox := collision.GetBoundingRectHitbox(a.Drawing, a.Hitbox()) if render.AbsInt(test.Y+test.H-aHitbox.Y) == 0 { onTop = true onTopY = test.Y @@ -241,7 +256,7 @@ func (w *Canvas) loopActorCollision() error { log.Error( "ERROR: Actors %s and %s overlap and the script returned false,"+ "but I didn't store %s original position earlier??", - a.Doodad.Title, b.Doodad.Title, b.Doodad.Title, + a.Doodad().Title, b.Doodad().Title, b.Doodad().Title, ) } diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index 6ff0ec7..b73eeb8 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -58,7 +58,7 @@ func (w *Canvas) InstallScripts() error { vm.Self = actor vm.Set("Self", vm.Self) - if _, err := vm.Run(actor.Doodad.Script); err != nil { + if _, err := vm.Run(actor.Doodad().Script); err != nil { log.Error("Run script for actor %s failed: %s", actor.ID(), err) } diff --git a/pkg/uix/canvas_link_tool.go b/pkg/uix/canvas_link_tool.go index b8995f4..0495d92 100644 --- a/pkg/uix/canvas_link_tool.go +++ b/pkg/uix/canvas_link_tool.go @@ -19,7 +19,7 @@ func (w *Canvas) LinkAdd(a *Actor) error { // First click, hold onto this actor. w.linkFirst = a shmem.Flash("Doodad '%s' selected, click the next Doodad to link it to", - a.Doodad.Title, + a.Doodad().Title, ) } else { // Second click, call the OnLinkActors handler with the two actors. -- 2.30.2