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() {
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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"
)
// 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.

View File

@ -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() {

View File

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

View File

@ -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(),

View File

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

View File

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

View File

@ -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
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.
s.screen.Place(s.invenFrame, ui.Place{
Left: 40,
Bottom: 40,
Top: 40,
Right: 40,
})
// Hide inventory if empty.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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