mirror of
https://github.com/charmbracelet/harmonica.git
synced 2026-07-19 10:09:33 -06:00
Introduce 2D + 3D projectile motion
Example Usage:
```
projectile := NewProjectile(FPS(60), Point{6.0, 100.0, 0.0}, Vector{2.0, 0.0, 0.0}, Vector{2.0, -9.81, 0.0})
someUpdateLoop(func() {
pos := projectile.Update()
})
```
NOTE, this is still missing:
1. Projectile mass
2. Forces (projectile.AddForce(Force{...})...)
3. Collisions (maybe?)
This commit is contained in:
parent
987abb3735
commit
c13404a663
77
physics.go
Normal file
77
physics.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Simple physics projectile motion.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Run once to initialize.
|
||||
// projectile := NewProjectile(FPS(60), Point{6.0, 100.0, 0.0}, Vector{2.0, 0.0, 0.0}, Vector{2.0, -9.81, 0.0})
|
||||
//
|
||||
// // Update on every frame.
|
||||
// someUpdateLoop(func() {
|
||||
// pos := projectile.Update()
|
||||
// })
|
||||
//
|
||||
// For background on projectile motion see:
|
||||
// https://en.wikipedia.org/wiki/Projectile_motion
|
||||
package harmonica
|
||||
|
||||
// Projectile is the representation of a projectile that has a position on a
|
||||
// plane and an acceleration and velocity
|
||||
type Projectile struct {
|
||||
pos Point
|
||||
vel Vector
|
||||
acc Vector
|
||||
deltaTime float64
|
||||
}
|
||||
|
||||
// Point is a representation of a point which contains the X, Y, Z coordinates
|
||||
// of the point on a plane.
|
||||
type Point struct {
|
||||
X, Y, Z float64
|
||||
}
|
||||
|
||||
// Vector is a representation of a vector which carries a magnitude and a
|
||||
// direction. We represent the vector as a point from the origin (0, 0) where
|
||||
// the magnitude is the euclidean distance from the origin and the direction is
|
||||
// the direction to the point from the origin.
|
||||
type Vector struct {
|
||||
X, Y, Z float64
|
||||
}
|
||||
|
||||
// Gravity is a utility vector that represents gravity in 2D and 3D contexts,
|
||||
// assuming that your coordinate plane looks like in 2D or 3D:
|
||||
//
|
||||
// -y -y ±z
|
||||
// │ │ /
|
||||
// │ │/
|
||||
// └───── ±x └───── ±x
|
||||
//
|
||||
// Note: Gravity usually is -9.81m/s however we use a positive value because we
|
||||
// assume the origin is placed at the top-left corner and that downward is the
|
||||
// positive y direction (only if using this utility variable).
|
||||
// Otherwise, you can place the origin wherever you'd like.
|
||||
var Gravity = Vector{0, 9.81, 0}
|
||||
|
||||
// NewProjectile accepts a frame rate, and initial values for position, velocity, and acceleration and
|
||||
// returns a new projectile.
|
||||
func NewProjectile(deltaTime float64, initialPosition Point, initialVelocity, initalAcceleration Vector) Projectile {
|
||||
return Projectile{
|
||||
pos: initialPosition,
|
||||
vel: initialVelocity,
|
||||
acc: initalAcceleration,
|
||||
deltaTime: deltaTime,
|
||||
}
|
||||
}
|
||||
|
||||
// Update updates the position and velocity values for the given projectile.
|
||||
// Call this after calling NewProjectile to update values.
|
||||
func (p *Projectile) Update() Point {
|
||||
p.pos.X += (p.vel.X * p.deltaTime)
|
||||
p.pos.Y += (p.vel.Y * p.deltaTime)
|
||||
p.pos.Z += (p.vel.Z * p.deltaTime)
|
||||
|
||||
p.vel.X += (p.acc.X * p.deltaTime)
|
||||
p.vel.Y += (p.acc.Y * p.deltaTime)
|
||||
p.vel.Z += (p.acc.Z * p.deltaTime)
|
||||
|
||||
return p.pos
|
||||
}
|
||||
91
physics_test.go
Normal file
91
physics_test.go
Normal file
@ -0,0 +1,91 @@
|
||||
package harmonica_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
. "github.com/charmbracelet/harmonica"
|
||||
)
|
||||
|
||||
const fps = 60
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
x := 8
|
||||
y := 20
|
||||
z := 0
|
||||
|
||||
projectile := NewProjectile(FPS(60), Point{float64(x), float64(y), float64(z)}, Vector{1, 1, 0}, Vector{0, 9.81, 0})
|
||||
pos := projectile.Update()
|
||||
if x != int(pos.X) {
|
||||
t.Logf("Want: %d, Got: %d", int(x), int(pos.X))
|
||||
t.Fatal("x coordinate unexpected")
|
||||
}
|
||||
|
||||
if y != int(pos.Y) {
|
||||
t.Logf("Want: %d, Got: %d", int(y), int(pos.Y))
|
||||
t.Fatal("y coordinate unexpected")
|
||||
}
|
||||
}
|
||||
|
||||
const equalityThreshold = 1e-2
|
||||
|
||||
// floating point comparison function that tests for an equality under the:
|
||||
// equalityThreshold
|
||||
func equal(a, b float64) bool {
|
||||
return math.Abs(a-b) <= equalityThreshold
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
projectile := NewProjectile(FPS(fps), Point{0, 0, 0}, Vector{5, 5, 0}, Vector{0, 0, 0})
|
||||
coordinates := []Point{
|
||||
{5.0, 5.0, 0},
|
||||
{10.0, 10.0, 0},
|
||||
{15.0, 15.0, 0},
|
||||
{20.0, 20.0, 0},
|
||||
{25.0, 25.0, 0},
|
||||
{30.0, 30.0, 0},
|
||||
{35.0, 35.0, 0},
|
||||
}
|
||||
|
||||
for _, c := range coordinates {
|
||||
var pos Point
|
||||
for i := 0; i < fps; i++ {
|
||||
pos = projectile.Update()
|
||||
}
|
||||
|
||||
if !equal(pos.X, c.X) || !equal(pos.Y, c.Y) {
|
||||
t.Logf("Want: (%.2f, %.2f)", c.X, c.Y)
|
||||
t.Logf("Got: (%.2f, %.2f)", pos.X, pos.Y)
|
||||
t.Fatal("coordinate unexpected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateGravity(t *testing.T) {
|
||||
fps := 60
|
||||
projectile := NewProjectile(FPS(fps), Point{0, 0, 0}, Vector{5, 5, 0}, Vector{0, 9.81, 0})
|
||||
|
||||
coordinates := []Point{
|
||||
{5.0, 9.82, 0},
|
||||
{10.0, 29.46, 0},
|
||||
{15.0, 58.90, 0},
|
||||
{20.0, 98.15, 0},
|
||||
{25.0, 147.22, 0},
|
||||
{30.0, 206.09, 0},
|
||||
{35.0, 274.77, 0},
|
||||
}
|
||||
|
||||
for _, c := range coordinates {
|
||||
var pos Point
|
||||
for f := 0; f < fps; f++ {
|
||||
pos = projectile.Update()
|
||||
}
|
||||
|
||||
if !equal(pos.X, c.X) || !equal(pos.Y, c.Y) {
|
||||
t.Log("coordinate unexpected")
|
||||
t.Logf("Want: (%.2f, %.2f)", c.X, c.Y)
|
||||
t.Logf("Got: (%.2f, %.2f)", pos.X, pos.Y)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user