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.
physics
Noah 2020-04-04 21:00:32 -07:00
parent c3d7348843
commit 08e65c32b5
37 changed files with 481 additions and 146 deletions

View File

@ -1,5 +1,5 @@
function main() { function main() {
log.Info("Azulian '%s' initialized!", Self.Doodad.Title); log.Info("Azulian '%s' initialized!", Self.Doodad().Title);
var playerSpeed = 4; var playerSpeed = 4;
var gravity = 4; var gravity = 4;
@ -28,8 +28,10 @@ function main() {
} }
sampleTick++; sampleTick++;
var Vx = playerSpeed * (direction === "left" ? -1 : 1); // TODO: Vector() requires floats, pain in the butt for JS,
Self.SetVelocity(Point(Vx, 0)); // 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()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("walk-"+direction, null); Self.PlayAnimation("walk-"+direction, null);

View File

@ -1,5 +1,5 @@
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); console.log("%s initialized!", Self.Doodad().Title);
var timer = 0; var timer = 0;
@ -28,7 +28,7 @@ function main() {
}); });
// Events.OnLeave(function(e) { // 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)); // Self.Canvas.SetBackground(RGBA(0, 0, 1, 0));
// }) // })
} }

View File

@ -1,5 +1,5 @@
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); console.log("%s initialized!", Self.Doodad().Title);
var pressed = false; var pressed = false;

View File

@ -1,6 +1,6 @@
function main() { function main() {
var color = Self.Doodad.Tag("color"); var color = Self.Doodad().Tag("color");
var keyname = "key-" + color + ".doodad"; var keyname = "key-" + color + ".doodad";
// Layers in the doodad image. // Layers in the doodad image.

View File

@ -1,5 +1,5 @@
function main() { 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("open", 100, [0, 1, 2, 3]);
Self.AddAnimation("close", 100, [3, 2, 1, 0]); Self.AddAnimation("close", 100, [3, 2, 1, 0]);
@ -9,7 +9,7 @@ function main() {
Self.SetHitbox(16, 0, 32, 64); Self.SetHitbox(16, 0, 32, 64);
Message.Subscribe("power", function(powered) { 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 (powered) {
if (animating || opened) { if (animating || opened) {

View File

@ -1,9 +1,9 @@
function main() { function main() {
var color = Self.Doodad.Tag("color"); var color = Self.Doodad().Tag("color");
Events.OnCollide(function(e) { Events.OnCollide(function(e) {
if (e.Settled) { if (e.Settled) {
e.Actor.AddItem(Self.Doodad.Filename, 0); e.Actor.AddItem(Self.Doodad().Filename, 0);
Self.Destroy(); Self.Destroy();
} }
}) })

View File

@ -2,7 +2,7 @@
function main() { function main() {
Self.AddAnimation("open", 0, [1]); Self.AddAnimation("open", 0, [1]);
var unlocked = false; var unlocked = false;
var color = Self.Doodad.Tag("color"); var color = Self.Doodad().Tag("color");
Self.SetHitbox(16, 0, 32, 64); Self.SetHitbox(16, 0, 32, 64);

View File

@ -1,5 +1,5 @@
function main() { 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(console));
console.log(Object.keys(log)); console.log(Object.keys(log));

View File

@ -1,6 +1,6 @@
// Exit Flag. // Exit Flag.
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); console.log("%s initialized!", Self.Doodad().Title);
Self.SetHitbox(22+16, 16, 75-16, 86); Self.SetHitbox(22+16, 16, 75-16, 86);
Events.OnCollide(function(e) { Events.OnCollide(function(e) {

View File

@ -4,7 +4,7 @@
var state = false; var state = false;
function main() { 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); Self.SetHitbox(0, 0, 33, 33);
// When the button is activated, don't keep toggling state until we're not // When the button is activated, don't keep toggling state until we're not

View File

@ -1,5 +1,5 @@
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); console.log("%s initialized!", Self.Doodad().Title);
// Switch has two frames: // Switch has two frames:
// 0: Off // 0: Off

View File

@ -1,5 +1,5 @@
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); console.log("%s initialized!", Self.Doodad().Title);
var timer = 0; var timer = 0;

View File

@ -1,6 +1,6 @@
function main() { function main() {
// What direction is the trapdoor facing? // What direction is the trapdoor facing?
var direction = Self.Doodad.Tag("direction"); var direction = Self.Doodad().Tag("direction");
console.log("Trapdoor(%s) initialized", direction); console.log("Trapdoor(%s) initialized", direction);
var timer = 0; var timer = 0;

View File

@ -16,7 +16,7 @@ function main() {
// other doodads. // other doodads.
// Logs go to the game's log file (standard output on Linux/Mac). // 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, // If our doodad has 'solid' parts that should prohibit movement,
// define the hitbox here. Coordinates are relative so 0,0 is the // 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 the Self object is subject to change, but some useful things you can access
from it include: from it include:
* Self.Doodad: a pointer to the doodad's file data. * Self.Doodad(): a pointer to the doodad's file data.
* Self.Doodad.Title: get the title of the doodad file. * Self.Doodad().Title: get the title of the doodad file.
* Self.Doodad.Author: the name of the author who wrote the doodad. * 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().Script: the doodad's JavaScript source code. Note that
modifying this won't have any effect in-game, as the script had already modifying this won't have any effect in-game, as the script had already
been loaded into the interpreter. 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. when the doodad was created.
### Events ### Events

View File

@ -14,7 +14,7 @@ Provide a script file with a `main` function:
```javascript ```javascript
function main() { function main() {
console.log("%s initialized!", Self.Doodad.Title); console.log("%s initialized!", Self.Doodad().Title);
var timer = 0; var timer = 0;
Events.OnCollide( function() { 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 surface area of this API is subject to change, but some useful examples you
can do with this are as follows. 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 all properties in there can be written to or read from the
JavaScript engine, but some useful attributes are: JavaScript engine, but some useful attributes are:
* `str Self.Doodad.Title`: the title of the doodad. * `str Self.Doodad().Title`: the title of the doodad.
* `str Self.Doodad.Author`: the author name 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().Script`: your own source code. Note that
editing this won't have any effect in-game, as your doodad's editing this won't have any effect in-game, as your doodad's
source has already been loaded into the interpreter. 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. the doodad.
### Self.ShowLayer(int index) ### Self.ShowLayer(int index)

View File

@ -14,9 +14,9 @@ var (
ScrollboxVert = 128 ScrollboxVert = 128
// Player speeds // Player speeds
PlayerMaxVelocity = 6 PlayerMaxVelocity float64 = 6
PlayerAcceleration = 2 PlayerAcceleration float64 = 0.2
Gravity = 6 Gravity float64 = 6
// Default chunk size for canvases. // Default chunk size for canvases.
ChunkSize = 128 ChunkSize = 128

View File

@ -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
}

View File

@ -7,6 +7,15 @@ import (
"git.kirsle.net/go/render" "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. // BoxCollision holds the result of a collision BetweenBoxes.
type BoxCollision struct { type BoxCollision struct {
// A and B are the indexes of the boxes sent to BetweenBoxes. // A and B are the indexes of the boxes sent to BetweenBoxes.

View File

@ -3,7 +3,6 @@ package collision
import ( import (
"sync" "sync"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/go/render" "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. 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 ( var (
P = d.Position() P = d.Position()
S = d.Size() 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. // 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. // We've already collided! Try to wiggle free.
if result.Bottom { if result.Bottom {
if !d.Grounded() { if !d.Grounded() {

View File

@ -10,8 +10,8 @@ type Actor interface {
ID() string ID() string
// Position and velocity, not saved to disk. // Position and velocity, not saved to disk.
Position() render.Point Position() render.Point // DEPRECATED
Velocity() render.Point Velocity() render.Point // DEPRECATED for uix.Actor
Size() render.Rect Size() render.Rect
Grounded() bool Grounded() bool
SetGrounded(bool) SetGrounded(bool)
@ -24,51 +24,3 @@ type Actor interface {
MoveBy(render.Point) // Add {X,Y} to current Position. MoveBy(render.Point) // Add {X,Y} to current Position.
MoveTo(render.Point) // Set current Position to {X,Y}. 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
// }

View File

@ -20,11 +20,11 @@ type Drawing struct {
// NewDrawing creates a Drawing actor based on a Doodad drawing. If you pass // NewDrawing creates a Drawing actor based on a Doodad drawing. If you pass
// an empty ID string, it will make a random UUIDv4 ID. // 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 == "" { if id == "" {
id = uuid.Must(uuid.NewRandom()).String() id = uuid.Must(uuid.NewRandom()).String()
} }
return Drawing{ return &Drawing{
id: id, id: id,
Doodad: doodad, Doodad: doodad,
size: doodad.Rect(), size: doodad.Rect(),

View File

@ -1,11 +1,14 @@
// Package dummy implements a dummy doodads.Drawing. // Package dummy implements a dummy doodads.Drawing.
package dummy 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. // Drawing is a dummy doodads.Drawing that has no data.
type Drawing struct { type Drawing struct {
doodads.Drawing Drawing *doodads.Drawing
} }
// NewDrawing creates a new dummy drawing. // NewDrawing creates a new dummy drawing.
@ -14,3 +17,8 @@ func NewDrawing(id string, doodad *doodads.Doodad) *Drawing {
Drawing: doodads.NewDrawing(id, doodad), Drawing: doodads.NewDrawing(id, doodad),
} }
} }
// Size returns the size of the underlying doodads.Drawing.
func (d *Drawing) Size() render.Rect {
return d.Drawing.Size()
}

View File

@ -348,7 +348,7 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
b.Actor.AddLink(idA) b.Actor.AddLink(idA)
// Reset the Link tool. // 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. // Set up the drop handler for draggable doodads.

View File

@ -142,7 +142,7 @@ func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
} }
var ( var (
rect = doodads.GetBoundingRect(actor) rect = collision.GetBoundingRect(actor)
box = collision.GetCollisionBox(rect) box = collision.GetCollisionBox(rect)
) )

15
pkg/physics/math.go Normal file
View File

@ -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)
}

38
pkg/physics/math_test.go Normal file
View File

@ -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)
}
}
}

33
pkg/physics/movement.go Normal file
View File

@ -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
// }

54
pkg/physics/vector.go Normal file
View File

@ -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)
}

View File

@ -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
}
}
}

View File

@ -39,8 +39,8 @@ func (s *PlayScene) setupInventoryHud() {
// Add the inventory frame to the screen frame. // Add the inventory frame to the screen frame.
s.screen.Place(s.invenFrame, ui.Place{ s.screen.Place(s.invenFrame, ui.Place{
Left: 40, Top: 40,
Bottom: 40, Right: 40,
}) })
// Hide inventory if empty. // Hide inventory if empty.

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "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/scripting"
"git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
@ -51,6 +52,7 @@ type PlayScene struct {
// Player character // Player character
Player *uix.Actor Player *uix.Actor
playerPhysics *physics.Mover
antigravity bool // Cheat: disable player gravity antigravity bool // Cheat: disable player gravity
noclip bool // Cheat: disable player clipping noclip bool // Cheat: disable player clipping
playerJumpCounter int // limit jump length playerJumpCounter int // limit jump length
@ -235,6 +237,14 @@ func (s *PlayScene) setupPlayer() {
s.drawing.AddActor(s.Player) s.drawing.AddActor(s.Player)
s.drawing.FollowActor = s.Player.ID() 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. // Set up the player character's script in the VM.
if err := s.scripting.AddLevelScript(s.Player.ID()); err != nil { if err := s.scripting.AddLevelScript(s.Player.ID()); err != nil {
log.Error("PlayScene.Setup: scripting.InstallActor(player) failed: %s", err) 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()) s.drawing.Present(d.Engine, s.drawing.Point())
// Draw out bounding boxes. // Draw out bounding boxes.
d.DrawCollisionBox(s.Player) d.DrawCollisionBox(s.Player.Drawing)
// Draw the UI screen and any widgets that attached to it. // Draw the UI screen and any widgets that attached to it.
s.screen.Compute(d.Engine) 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. // movePlayer updates the player's X,Y coordinate based on key pressed.
func (s *PlayScene) movePlayer(ev *event.State) { 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 // Antigravity: player can move anywhere with arrow keys.
// player by only one pixel per tick. if s.antigravity {
if s.antigravity && ev.Shift { velocity.X = 0
playerSpeed = 1 velocity.Y = 0
}
var velocity render.Point // Shift to slow your roll to 1 pixel per tick.
if ev.Shift {
if ev.Left { playerSpeed = 1
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
} }
}
if ev.Down && s.antigravity {
velocity.Y = playerSpeed
}
if !s.Player.Grounded() { if ev.Left {
s.playerJumpCounter-- 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) s.Player.SetVelocity(velocity)

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/physics"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
@ -73,6 +74,7 @@ func (vm *VM) RegisterLevelHooks() error {
"Flash": shmem.Flash, "Flash": shmem.Flash,
"RGBA": render.RGBA, "RGBA": render.RGBA,
"Point": render.NewPoint, "Point": render.NewPoint,
"Vector": physics.NewVector,
"Self": vm.Self, // i.e., the uix.Actor object "Self": vm.Self, // i.e., the uix.Actor object
"Events": vm.Events, "Events": vm.Events,
"GetTick": func() uint64 { "GetTick": func() uint64 {

View File

@ -6,9 +6,11 @@ import (
"sort" "sort"
"sync" "sync"
"git.kirsle.net/apps/doodle/pkg/collision"
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/physics"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
@ -23,9 +25,10 @@ import (
// as defined in the map: its spawn coordinate and configuration. // as defined in the map: its spawn coordinate and configuration.
// - A uix.Canvas that can present the actor's graphics to the screen. // - A uix.Canvas that can present the actor's graphics to the screen.
type Actor struct { type Actor struct {
doodads.Drawing id string
Actor *level.Actor Drawing *doodads.Drawing
Canvas *Canvas Actor *level.Actor
Canvas *Canvas
activeLayer int // active drawing frame for display activeLayer int // active drawing frame for display
flagDestroy bool // flag the actor for destruction 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. inventory map[string]int // item inventory. doodad name -> quantity, 0 for key item.
data map[string]string // arbitrary key/value store. DEPRECATED ?? data map[string]string // arbitrary key/value store. DEPRECATED ??
// Movement data.
position render.Point
velocity physics.Vector
grounded bool
// Animation variables. // Animation variables.
animations map[string]*Animation animations map[string]*Animation
activeAnimation *Animation activeAnimation *Animation
@ -81,6 +89,17 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor
return 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. // SetGravity configures whether the actor is affected by gravity.
func (a *Actor) SetGravity(v bool) { func (a *Actor) SetGravity(v bool) {
a.hasGravity = v a.hasGravity = v
@ -98,6 +117,46 @@ func (a *Actor) IsMobile() bool {
return a.isMobile 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 // SetNoclip sets the noclip setting for an actor. If true, the actor can
// clip through level geometry. // clip through level geometry.
func (a *Actor) SetNoclip(v bool) { 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. // GetBoundingRect gets the bounding box of the actor's doodad.
func (a *Actor) GetBoundingRect() render.Rect { func (a *Actor) GetBoundingRect() render.Rect {
return doodads.GetBoundingRect(a) return collision.GetBoundingRect(a)
} }
// SetHitbox sets the actor's elected hitbox. // 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. // LayerCount returns the number of layers in this actor's drawing.
func (a *Actor) LayerCount() int { 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. // ShowLayer sets the actor's ActiveLayer to the index given.
func (a *Actor) ShowLayer(index int) error { func (a *Actor) ShowLayer(index int) error {
if index < 0 { if index < 0 {
return errors.New("layer index must be 0 or greater") 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) return fmt.Errorf("layer %d out of range for doodad's layers", index)
} }
a.activeLayer = 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 return nil
} }
// ShowLayerNamed sets the actor's ActiveLayer to the one named. // ShowLayerNamed sets the actor's ActiveLayer to the one named.
func (a *Actor) ShowLayerNamed(name string) error { func (a *Actor) ShowLayerNamed(name string) error {
// Find the layer. // Find the layer.
for i, layer := range a.Doodad.Layers { for i, layer := range a.Doodad().Layers {
if layer.Name == name { if layer.Name == name {
return a.ShowLayer(i) return a.ShowLayer(i)
} }

View File

@ -61,7 +61,7 @@ func (a *Actor) AddAnimation(name string, interval int64, layers []interface{})
switch v := name.(type) { switch v := name.(type) {
case string: case string:
var found bool var found bool
for i, layer := range a.Doodad.Layers { for i, layer := range a.Doodad().Layers {
if layer.Name == v { if layer.Name == v {
indexes = append(indexes, i) indexes = append(indexes, i)
found = true found = true
@ -80,7 +80,7 @@ func (a *Actor) AddAnimation(name string, interval int64, layers []interface{})
} }
iv := int(v) iv := int(v)
if iv < len(a.Doodad.Layers) { if iv < len(a.Doodad().Layers) {
indexes = append(indexes, iv) indexes = append(indexes, iv)
} else { } else {
return fmt.Errorf("layer numbered '%d' is out of bounds", iv) return fmt.Errorf("layer numbered '%d' is out of bounds", iv)

View File

@ -7,8 +7,8 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/collision" "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/log"
"git.kirsle.net/apps/doodle/pkg/physics"
"git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"github.com/robertkrimen/otto" "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. // Get the actor's velocity to see if it's moving this tick.
v := a.Velocity() 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 not moving, grab the bounding box right now.
if v == render.Origin { if v.IsZero() {
boxes[i] = doodads.GetBoundingRect(a) boxes[i] = collision.GetBoundingRect(a)
return return
} }
// Create a delta point from their current location to where they // Create a delta point from their current location to where they
// want to move to this tick. // want to move to this tick.
delta := a.Position() delta := physics.VectorFromPoint(a.Position())
delta.Add(v) delta.Add(v)
// Check collision with level geometry. // 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 { if ok {
// Collision happened with world. // Collision happened with world.
if w.OnLevelCollision != nil { if w.OnLevelCollision != nil {
@ -89,17 +102,17 @@ func (w *Canvas) loopActorCollision() error {
// Move us back where the collision check put us // Move us back where the collision check put us
if !a.noclip { if !a.noclip {
delta = info.MoveTo delta = physics.VectorFromPoint(info.MoveTo)
} }
// Move the actor's World Position to the new location. // 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. // Keep the actor from leaving the world borders of bounded maps.
w.loopContainActorsInsideLevel(a) w.loopContainActorsInsideLevel(a)
// Store this actor's bounding box after they've moved. // Store this actor's bounding box after they've moved.
boxes[i] = doodads.GetBoundingRect(a) boxes[i] = collision.GetBoundingRect(a)
}(i, a) }(i, a)
wg.Wait() wg.Wait()
} }
@ -115,10 +128,12 @@ func (w *Canvas) loopActorCollision() error {
collidingActors[a.ID()] = b.ID() 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. // Call the OnCollide handler for A informing them of B's intersection.
if w.scripting != nil { if w.scripting != nil {
var ( var (
rect = doodads.GetBoundingRect(b) rect = collision.GetBoundingRect(b)
lastGoodBox = render.Rect{ lastGoodBox = render.Rect{
X: originalPositions[b.ID()].X, X: originalPositions[b.ID()].X,
Y: originalPositions[b.ID()].Y, Y: originalPositions[b.ID()].Y,
@ -190,7 +205,7 @@ func (w *Canvas) loopActorCollision() error {
// Did A protest? // Did A protest?
if err == scripting.ErrReturnFalse { if err == scripting.ErrReturnFalse {
// Are they on top? // 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 { if render.AbsInt(test.Y+test.H-aHitbox.Y) == 0 {
onTop = true onTop = true
onTopY = test.Y onTopY = test.Y
@ -241,7 +256,7 @@ func (w *Canvas) loopActorCollision() error {
log.Error( log.Error(
"ERROR: Actors %s and %s overlap and the script returned false,"+ "ERROR: Actors %s and %s overlap and the script returned false,"+
"but I didn't store %s original position earlier??", "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,
) )
} }

View File

@ -58,7 +58,7 @@ func (w *Canvas) InstallScripts() error {
vm.Self = actor vm.Self = actor
vm.Set("Self", vm.Self) 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) log.Error("Run script for actor %s failed: %s", actor.ID(), err)
} }

View File

@ -19,7 +19,7 @@ func (w *Canvas) LinkAdd(a *Actor) error {
// First click, hold onto this actor. // First click, hold onto this actor.
w.linkFirst = a w.linkFirst = a
shmem.Flash("Doodad '%s' selected, click the next Doodad to link it to", shmem.Flash("Doodad '%s' selected, click the next Doodad to link it to",
a.Doodad.Title, a.Doodad().Title,
) )
} else { } else {
// Second click, call the OnLinkActors handler with the two actors. // Second click, call the OnLinkActors handler with the two actors.