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.
This commit is contained in:
parent
c3d7348843
commit
08e65c32b5
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
// })
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function main() {
|
||||
console.log("%s initialized!", Self.Doodad.Title);
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
|
||||
var pressed = false;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function main() {
|
||||
console.log("%s initialized!", Self.Doodad.Title);
|
||||
console.log("%s initialized!", Self.Doodad().Title);
|
||||
|
||||
var timer = 0;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
42
pkg/collision/bounding_rect.go
Normal file
42
pkg/collision/bounding_rect.go
Normal 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
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -142,7 +142,7 @@ func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
|
|||
}
|
||||
|
||||
var (
|
||||
rect = doodads.GetBoundingRect(actor)
|
||||
rect = collision.GetBoundingRect(actor)
|
||||
box = collision.GetCollisionBox(rect)
|
||||
)
|
||||
|
||||
|
|
15
pkg/physics/math.go
Normal file
15
pkg/physics/math.go
Normal 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
38
pkg/physics/math_test.go
Normal 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
33
pkg/physics/movement.go
Normal 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
54
pkg/physics/vector.go
Normal 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)
|
||||
}
|
53
pkg/physics/vector_test.go
Normal file
53
pkg/physics/vector_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user