--- /dev/null
+package game
+
+import (
+ "image/color"
+
+ "github.com/hajimehoshi/ebiten/v2"
+)
+
+// Draw the game! But the game itself is not something
+// to draw. Draw the parts that make up a game, in the right
+// order. This order is important!
+func (g *Game) Draw(i *ebiten.Image) {
+ // Oh yeah and draw the background colour.
+ i.Fill(color.RGBA{255, 223, 212, 255})
+
+ // tracks on the bottom
+ for _, t := range g.tracks {
+ t.Draw(i)
+ }
+
+ // connections on the tracks
+ for c, _ := range g.connections {
+ c.Draw(i)
+ }
+
+ // trains over the connections
+ for _, t := range g.trains {
+ t.Draw(i)
+ }
+
+ // Do some extra debug drawing if that flag is set
+ if g.DebugDraw {
+ // First draw the tracks now on top of all the
+ // previous stuff. These are least interesting to
+ // debug.
+ for _, t := range g.tracks {
+ t.DebugDraw(i)
+ }
+
+ // Debug the trains.
+ for _, t := range g.trains {
+ t.DebugDraw(i)
+ }
+
+ // And then the connections.
+ for c, _ := range g.connections {
+ c.DebugDraw(i)
+ }
+ }
+}
+
+func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
+ return outsideWidth, outsideHeight
+}
--- /dev/null
+package game
+
+import (
+ "choochoo/track"
+ "choochoo/train"
+)
+
+// The representation of our entire game!
+// Our game is just some trains, some tracks,
+// and a distinct set of connections. Because when
+// adding a track it likely reuses some connections,
+// and we don't want to double tick/draw connections.
+type Game struct {
+ trains []*train.Train
+ tracks []track.Track
+ connections map[track.Connection]struct{}
+
+ DebugDraw bool
+}
+
+func NewGame() *Game {
+ return &Game{
+ make([]*train.Train, 0),
+ make([]track.Track, 0),
+ make(map[track.Connection]struct{}),
+ false,
+ }
+}
+
+// Register a new track to the game.
+// This will store it inside together with its
+// two connections.
+func (g *Game) AddTrack(t track.Track) {
+ g.tracks = append(g.tracks, t)
+ for _, c := range t.Connections() {
+ _, ok := g.connections[c]
+ if !ok {
+ g.connections[c] = struct{}{}
+ }
+ }
+}
+
+// Add a new train to the game.
+// It just stores the train.
+func (g *Game) AddTrain(t *train.Train) {
+ g.trains = append(g.trains, t)
+}
--- /dev/null
+package game
+
+// Update the entire game.
+// For now we just update the trains, but
+// if this becomes a real game also update
+// handle user input actions to set switch states or train speeds.
+// Also interactively building new tracks.
+// And train collisions, can't forget train collisions.
+func (g *Game) Update() error {
+ // but for now just update the trains.
+ for _, t := range g.trains {
+ if err := t.Update(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
--- /dev/null
+module choochoo
+
+go 1.24.0
+
+require (
+ github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect
+ github.com/ebitengine/hideconsole v1.0.0 // indirect
+ github.com/ebitengine/purego v0.8.0 // indirect
+ github.com/hajimehoshi/ebiten/v2 v2.8.6 // indirect
+ github.com/jezek/xgb v1.1.1 // indirect
+ golang.org/x/sync v0.8.0 // indirect
+ golang.org/x/sys v0.25.0 // indirect
+)
--- /dev/null
+github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM=
+github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY=
+github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
+github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
+github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
+github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/hajimehoshi/ebiten/v2 v2.8.6 h1:Dkd/sYI0TYyZRCE7GVxV59XC+WCi2BbGAbIBjXeVC1U=
+github.com/hajimehoshi/ebiten/v2 v2.8.6/go.mod h1:cCQ3np7rdmaJa1ZnvslraVlpxNb3wCjEnAP1LHNyXNA=
+github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
+github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
--- /dev/null
+package main
+
+import (
+ "choochoo/game"
+ "choochoo/math"
+ "choochoo/track"
+ "choochoo/train"
+ "log"
+
+ "github.com/hajimehoshi/ebiten/v2"
+)
+
+// Starting game setup. But really just a debug setup.
+// If I make this an actual game you'll be able to place tracks and play with trains.
+func startingGame() *game.Game {
+ // create a new empty game, as implemented in the game/ folder
+ g := game.NewGame()
+
+ // Consecutive points to create the starting track.
+ // It has a right angle bend which isn't really nice for trains, but at least it works
+ points := []math.Point{
+ {200, 70}, {330, 80}, {400, 170}, {314, 400}, {100, 350}, {100, 140},
+ }
+
+ // Make a track piece between the first two points, as implemented in the track/ directory
+ current := track.NewStraightTrack(points[0], points[1])
+ first := current
+ // Add this track to the game we created at the beginning
+ g.AddTrack(current)
+ // Go over the rest of the points that we haven't covered yet
+ for i := 2; i < len(points); i++ {
+ // make a track piece from the last connection of the previous track to the new point
+ next := track.ExtendStraightTrack(current.Connections()[1], points[i])
+ g.AddTrack(next)
+ // update this current for the next loop
+ current = next
+ }
+
+ // The last track piece is between the first and last (current) track pieces, and their first and second connections respectively
+ last := track.ConnectPointsStraight(current.Connections()[1], first.Connections()[0])
+ g.AddTrack(last)
+
+ // make a train and place it on the track
+ choo := train.NewTrainOnTrack(last)
+ g.AddTrain(choo)
+
+ // draws some extra line if this is true
+ g.DebugDraw = false
+
+ return g
+}
+
+// Entry point of the application
+func main() {
+ g := startingGame()
+
+ // Set up the window
+ ebiten.SetWindowSize(512, 512)
+ ebiten.SetWindowTitle("choo choo")
+
+ // Run the game until exit. If there was a problem just print it out
+ if err := ebiten.RunGame(g); err != nil {
+ log.Fatal(err)
+ }
+}
--- /dev/null
+package math
+
+import "math"
+
+// A point in 2d space. It has 2 values X and Y.
+// In ebiten (the engine) (0,0) is top left.
+// To the right is increasing X. To the bottom is increasing Y.
+// Points should be considered as values. Operations on a point create a new point.
+type Point struct {
+ X, Y float32
+}
+
+// Add two points together, valuewise.
+// It doesn't modify the point. Instead it returns a new one.
+func (p Point) Add(a Point) Point {
+ return Point{p.X + a.X, p.Y + a.Y}
+}
+
+// Multiply or scale a point with a scalar
+func (p Point) Mult(f float32) Point {
+ return Point{p.X * f, p.Y * f}
+}
+
+// The distance between two points.
+func (p Point) Dist(other Point) float32 {
+ squareDistance := math.Pow(float64(p.X-other.X), 2) + math.Pow(float64(p.Y-other.Y), 2)
+ return float32(math.Pow(squareDistance, 1.0/2))
+}
+
+// Given a distance to take, calculate the fraction this distance is
+// compared to the distance between two points.
+//
+// For example, if the result is 0.5 the distance between the two points is twice the stepSize
+func DistanceToIncrement(stepSize float32, from, to Point) float32 {
+ squareDistance := math.Pow(float64(from.X-to.X), 2) + math.Pow(float64(from.Y-to.Y), 2)
+ squareStep := float64(stepSize * stepSize)
+ return float32(math.Pow(squareStep/squareDistance, 1.0/2))
+}
+
+// Sum two points together but scaled with a respective scalar.
+// Typically the two scalar should sum up to 1, but you can do whatever you want.
+//
+// This can be used to calculate the center between two points by setting both
+// scalars to 0.5. This basically takes the average of the two points
+func WeightedAverage(a Point, aw float32, b Point, bw float32) Point {
+ return a.Mult(aw).Add(b.Mult(bw))
+}
--- /dev/null
+package math
+
+// This is a file containing some tests for the math functions.
+// Boring stuff
+
+import "testing"
+
+func TestWeightedAverage(t *testing.T) {
+ testData := []struct {
+ from, to Point
+ ratio float32
+ expected Point
+ }{
+ {Point{0, 0}, Point{1, 1}, 0, Point{0, 0}},
+ }
+
+ for _, td := range testData {
+ t.Run("", func(t *testing.T) {
+ res := WeightedAverage(td.from, 1-td.ratio, td.to, td.ratio)
+ dx := res.X - td.expected.X
+ dy := res.Y - td.expected.Y
+ if dx < 0 {
+ dx *= -1
+ }
+ if dy < 0 {
+ dy *= -1
+ }
+ if dx > 0.001 || dy > 0.001 {
+ t.Errorf("TestWeightedAverage(%v, %f, %v, %f) = %v but expected %v.", td.from, td.ratio, td.to, 1-td.ratio, res, td.expected)
+ }
+
+ })
+ }
+
+}
+
+func TestDistanceToIncrement(t *testing.T) {
+ testData := []struct {
+ stepSize float32
+ from, to Point
+ exp float32
+ }{
+ {1, Point{0, 0}, Point{1, 0}, 1},
+ {0.5, Point{0, 0}, Point{1, 0}, 0.5},
+ {1, Point{0, 0}, Point{2, 0}, 0.5},
+ }
+
+ for _, td := range testData {
+ t.Run("", func(t *testing.T) {
+ res := DistanceToIncrement(td.stepSize, td.from, td.to)
+ d := res - td.exp
+ if d < 0 {
+ d *= -1
+ }
+ if d > 0.001 {
+ t.Errorf("DistanceToIncrement(%f, %v, %v) = %f but expected %f", td.stepSize, td.from, td.to, res, td.exp)
+ }
+ })
+ }
+}
--- /dev/null
+package track
+
+import (
+ "choochoo/math"
+ "errors"
+
+ "github.com/hajimehoshi/ebiten/v2"
+)
+
+// Useful for error handling, if I ever decide to do that
+var ConnectionFull = errors.New("Cannot add any more connections to this track")
+
+// A connection in general. It can have many implementation,
+// but they must fulfil to this thing.
+type Connection interface {
+ // Where is this connection in space?
+ At() math.Point
+ // Add a new track piece to a connection. It can
+ // fail when this connection is filled by connections.
+ AddTrack(t Track) error
+
+ // When coming from one track, what's the other track
+ // you should continue on?
+ NextTrack(previous Track) Track
+
+ // Draw this connection in the image.
+ Draw(i *ebiten.Image)
+ DebugDraw(i *ebiten.Image)
+}
+
+// The simplest type of connection. It is between
+// two pieces of track.
+type StraightConnection struct {
+ // The two track pieces that share this connection
+ a, b Track
+ // Where this connection is at
+ at math.Point
+}
+
+// A more complex type of connection that I haven't implemented yet.
+// It connects multiple tracks and can decide which two to connect.
+type Switch struct {
+}
+
+func (sc *StraightConnection) At() math.Point { return sc.at }
+
+// Adding a track to a straight connection fills
+// one of two slots in this connection. If both
+// are full this returns an error.
+func (sc *StraightConnection) AddTrack(t Track) error {
+ // Is slot a empty?
+ if sc.a == nil {
+ sc.a = t
+ // Is slot b empty?
+ } else if sc.b == nil {
+ sc.b = t
+ // nope, I'm full.
+ } else {
+ return ConnectionFull
+ }
+ return nil
+}
+
+// The next track on a straight connection is
+// just the other track.
+func (sc *StraightConnection) NextTrack(t Track) Track {
+ // Are we coming from slot a?
+ if t == sc.a {
+ return sc.b
+ // Are we coming from slot b?
+ } else if t == sc.b {
+ return sc.a
+ }
+
+ // I don't even know that track. I can't give you a next one!
+ return nil
+}
--- /dev/null
+package track
+
+import (
+ "image/color"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/vector"
+)
+
+// Drawing a track. It's a really boring black
+// line between its endpoints.
+func (t *StraightTrack) Draw(i *ebiten.Image) {
+ vector.StrokeLine(i, t.From.At().X, t.From.At().Y, t.To.At().X, t.To.At().Y, 8, color.Black, true)
+}
+
+// Draw some more, but still uninteresting stuff.
+func (t *StraightTrack) DebugDraw(i *ebiten.Image) {
+ debugColor := color.RGBA{255, 102, 110, 255}
+ middle := t.From.At().Add(t.To.At()).Mult(0.5)
+ vector.StrokeLine(i, middle.X, middle.Y, t.From.At().X, t.From.At().Y, 1, debugColor, false)
+ vector.StrokeLine(i, middle.X, middle.Y, t.To.At().X, t.To.At().Y, 1, debugColor, false)
+}
+
+// Drawing a connection point. It's also really boring.
+// It's a less boring colour, but a more boring dot.
+func (st *StraightConnection) Draw(i *ebiten.Image) {
+ vector.DrawFilledCircle(i, st.at.X, st.at.Y, 4, color.RGBA{77, 127, 133, 255}, true)
+}
+
+// Nothing to debug draw for a connection.
+func (st *StraightConnection) DebugDraw(i *ebiten.Image) {
+}
--- /dev/null
+package track
+
+import (
+ "choochoo/math"
+)
+
+// A simple piece of straight track.
+// It's super straight, which really doesn't
+// work well with trains.
+// But programming curves is hard OK?
+type StraightTrack struct {
+ From, To Connection
+}
+
+// Make a new straight track between two points
+func NewStraightTrack(from, to math.Point) Track {
+ st := new(StraightTrack)
+ st.From = &StraightConnection{nil, st, from}
+ st.To = &StraightConnection{st, nil, to}
+ return st
+}
+
+// Extend an existing track with a connection to a new point
+func ExtendStraightTrack(from Connection, to math.Point) Track {
+ st := new(StraightTrack)
+ st.From = from
+ from.AddTrack(st)
+ st.To = &StraightConnection{st, nil, to}
+ return st
+}
+
+// Connect two track pieces together with a straight track
+func ConnectPointsStraight(from Connection, to Connection) Track {
+ st := new(StraightTrack)
+ st.From = from
+ from.AddTrack(st)
+ st.To = to
+ to.AddTrack(st)
+ return st
+}
+
+func (t *StraightTrack) Connections() [2]Connection {
+ return [2]Connection{t.From, t.To}
+}
+
+func (t *StraightTrack) FractionTowards(fraction float32, target Connection) math.Point {
+ // Get our direction straight by finding the source.
+ // This source is the other connection that's not the target.
+ // The source will the result when the fraction is 0, and target is when fraction is 1
+ var src Connection
+ // You've seen this before.
+ if target == t.From {
+ src = t.To
+ } else if target == t.To {
+ src = t.From
+ } else {
+ return math.Point{0, 0}
+ }
+ // Since we're on a straight line, the point between source and target
+ // is just a weighted average of those two points.
+ // It would be a whole different story if we're doing curves.
+ return math.WeightedAverage(src.At(), 1-fraction, target.At(), fraction)
+}
+
+// The length of a straight track, which is the distance between the points.
+func (t *StraightTrack) Length() float32 {
+ // Again, WHOLE different story if if we're doing curves.
+ return t.From.At().Dist(t.To.At())
+}
--- /dev/null
+package track
+
+import (
+ "choochoo/math"
+
+ "github.com/hajimehoshi/ebiten/v2"
+)
+
+// A track in general. It can have many implementations,
+// but they must fulfil to this thing.
+type Track interface {
+ // A track is always between two connections.
+ Connections() [2]Connection
+ // Given a completion ratio or percentage, towards one of your two connections,
+ // where is that in space?
+ FractionTowards(fraction float32, towards Connection) math.Point
+ // The full length of this track.
+ Length() float32
+
+ // It can be drawn
+ Draw(i *ebiten.Image)
+ DebugDraw(i *ebiten.Image)
+}
--- /dev/null
+package train
+
+import (
+ "choochoo/math"
+ "image/color"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/vector"
+)
+
+// Draw function of a train which is called once every frame.
+// It doesn't block game ticks in this engine, which is nice.
+//
+// A train is a series of bogies, and pairs of them are connected
+// by a carriage. Bogies are circles, carriages are thick lines
+func (t *Train) Draw(i *ebiten.Image) {
+ bogieColor := color.RGBA{101, 105, 92, 255}
+ cartColor := color.RGBA{105, 255, 215, 255}
+
+ // Draw all the bogies
+ for _, b := range t.Bogies {
+ // Ask the bogie where it is
+ at := b.at()
+ // Draw a circle there
+ vector.DrawFilledCircle(i, at.X, at.Y, 6, bogieColor, true)
+ }
+
+ // Draw all the carriages
+ // This is a funky loop header. Basically
+ // it goes over all the bogies but skips one
+ // This way the current and next bogie form a
+ // pair for our carriage.
+ // If there are an uneven amount of bogies... uhhh don't
+ for j := 0; j < len(t.Bogies)-1; j += 2 {
+ // These are our two bogies. The one at the index and one after it
+ one := t.Bogies[j].at()
+ two := t.Bogies[j+1].at()
+ // This is the center point between the two bogies, on which
+ // we'll draw the carriage. I think this hides some jitteriness
+ // of the bogies themselves since it averages the two.
+ center := math.WeightedAverage(one, 0.5, two, 0.5)
+ // Front and back points for our carriage ends
+ front := calculateCartEnd(center, one, t.cartLength/2)
+ back := calculateCartEnd(center, two, t.cartLength/2)
+ // Draw a rectangle there
+ vector.StrokeLine(i, front.X, front.Y, back.X, back.Y, 8, cartColor, true)
+ }
+}
+
+// Debug drawing on top of the previous draw.
+// It adds some lines to know where things are looking.
+func (t *Train) DebugDraw(i *ebiten.Image) {
+ bogieColor := color.RGBA{112, 201, 56, 255}
+ debugColor := color.RGBA{255, 255, 0, 255}
+ for _, b := range t.Bogies {
+ at := b.at()
+ vector.DrawFilledCircle(i, at.X, at.Y, 6, bogieColor, false)
+ }
+
+ b := t.Bogies[0]
+ at := b.at()
+ targetAt := b.target.At()
+ vector.StrokeLine(i, at.X, at.Y, targetAt.X, targetAt.Y, 1, debugColor, false)
+}
+
+// Very fun function! It calculates half a carriage.
+// The distance between a pair of bogies isn't fixed, even if we want it to be.
+// And neither is the distance between a bogie and the center of two.
+// So to not grow and shrink our carriages, the line between the center
+// and a carriage is scaled so that it matches the target distance.
+func calculateCartEnd(center, bogie math.Point, targetDistance float32) math.Point {
+ // The distance between the center and dist.
+ // This is only useful as a direction.
+ centerToBogie := center.Dist(bogie)
+ // Now!
+ // First move the bogie so that the center would be at the origin. You do this
+ // by just subtracting the center point. And subtraction is adding the negative value.
+ // Then multiply this point around center with a distance correcting factor.
+ // It was centerToBogie long but it needs to be tragetDistance long. So divide by
+ // centerToBogie and multipy by targetDistance. So x * (y/x).
+ // Lastly put it back in place by adding the center point again.
+ // You can also look at the diagram below. Left to right, top to bottom
+ return bogie.Add(center.Mult(-1)).Mult(targetDistance / centerToBogie).Add(center)
+}
+
+// ^ ^
+// | C |
+// | `. |
+// | B |
+// | |
+// +-------> C------->
+// `.
+// B
+//
+// ^ ^
+// | | C
+// | | \
+// | | `.
+// | | T
+// C-------> ------->
+// \
+// `.
+// T
--- /dev/null
+package train
+
+import (
+ "choochoo/math"
+ "choochoo/track"
+)
+
+// A train, choo choo.
+// A train is defined by a couple of bogies,
+// it's speed and the length of a cart/carriage.
+// The length of a cart is only for visual purposes.
+type Train struct {
+ Bogies []*bogie
+ speed float32
+ cartLength float32
+}
+
+// A bogie, roll roll?
+// A bogie is always on a track, its currentTrack.
+// On this current track it has a completion ratio
+// between 0 and 1.
+// 0 being at one end, and 1 being at the other.
+// At what end this is is defined by the target connection.
+// This connection is one of the two connections of the current track,
+// and the direction of this connection is forward, with increasing
+// completion. So 1 is at that connection, and 0 is at the other.
+// It also has a target distance to the next bogie that it tries
+// to maintain at all times.
+type bogie struct {
+ trackCompletion float32 // 0.0 to 1.0
+ currentTrack track.Track
+ target track.Connection
+ distanceToNext float32
+}
+
+// Make a new train, and put it on a track immediately
+func NewTrainOnTrack(t track.Track) *Train {
+ tr := new(Train)
+ // Prepare the number of bogies of the train.
+ // These will be empty but we'll fill them in.
+ // The number of bogies must be even! Because two bogies form a cart.
+ // No high speed trains with bogies in between carriages sorry.
+ tr.Bogies = make([]*bogie, 8)
+ // This is a fun variable. It is the distance between the current
+ // bogie and the one in front of it. But this distance alternates!
+ // Two bogies under the same cart have a smaller distance
+ // than two bogies across a cart coupling.
+ var d float32 = 20
+ // Go over all the bogies positions
+ for i := range tr.Bogies {
+ // make a bogie
+ b := new(bogie)
+ // Put it on the track
+ b.currentTrack = t
+ // Orient the bogie towards the next connection
+ b.target = t.Connections()[1]
+ // Set it's distance to the next bogie
+ b.distanceToNext = d
+ // Give this bogie to the train
+ tr.Bogies[i] = b
+ // Change the bogie distance for the next one. By always
+ // subtracting it from 50 the value d will be 20, 30, 20, 30
+ // repeating.
+ d = 50 - d
+ }
+ // set the train speed
+ tr.speed = 3
+ // and the train cart's length
+ tr.cartLength = 48
+ return tr
+}
+
+// Give back the connections that a bogie is facing, and the other one
+// The front connection will always be the target connection
+func (b *bogie) frontAndBack() (front, back track.Connection) {
+ // Get both connections of the current track
+ conns := b.currentTrack.Connections()
+ // Is the first one our target?
+ if conns[0] == b.target {
+ // Then the first one is in front,
+ front = conns[0]
+ // and the other in the back
+ back = conns[1]
+ // nah the second one is our target
+ } else {
+ // so the second one is in front,
+ front = conns[1]
+ // and the other in the back
+ back = conns[0]
+ }
+ return
+}
+
+// Where is this bogie in space?
+// We know it is somewhere along the track but
+// turn this into an actual point in space.
+func (b *bogie) at() math.Point {
+ // Just ask the track where I am at.
+ return b.currentTrack.FractionTowards(b.trackCompletion, b.target)
+}
+
+// How far is a bogie to another one?
+func (b *bogie) dist(other *bogie) float32 {
+ // Just get both bogie's positions
+ myPos := b.at()
+ otherPos := other.at()
+ // And calculate the distance between these points
+ return myPos.Dist(otherPos)
+}
--- /dev/null
+package train
+
+// Update the train position. Called once per tick so don't take too long.
+// The train, which is just a sequence of bogies, will step the first
+// bogie by the train's speed, and step all other bogies forward
+// so that they maintain the same fixed distance to the one in front.
+// This is not an exact solution and will fail in impossible track designs.
+//
+// Well... it's one of the two approaches I can think off, and this is
+// much more robust/elegant than the other I guess.
+// The second approach would be to calculate the intersection point of a
+// circle around the bogie in front and the entire track.
+// This produces 2 or more points and one of them is correct.
+// Lot's of math, and not better, or even worse, in impossible tracks.
+func (t *Train) Update() error {
+ // The first bogie can just step forward with the train's speed.
+ // This is the leading bogie and is not constrained by anything.
+ b := t.Bogies[0]
+ // I say forward but if the speed is negative it will go backwards instead.
+ b.step(t.speed)
+
+ nextBogie := b
+ // Go through all the other bogies one after the other
+ for i := 1; i < len(t.Bogies); i++ {
+ b := t.Bogies[i]
+ // This is the current distance to the bogie in front,
+ // subtracted with our target distance. We want the
+ // result to be 0, but this isn't always possible.
+ d := b.dist(nextBogie) - b.distanceToNext
+ // This is a factor we'll multiply the step with,
+ // and then decrease. We want to take big steps
+ // at the initial iterations, but fine tune it
+ // later. I'll also use this to limit the amount
+ // of max iterations
+ var falloff float32 = 1.0
+ // If the subtracted distance is too small, or too big, move
+ // the bogie until it is small enough.
+ // You could also compare abs(d) < 0.1 but technicalities blah blah.
+ // ALSO check that the falloff is becoming too small so that
+ // our update doesn't take too long.
+ for (d < -0.1 || 0.1 < d) && falloff > 0.1 {
+ // Step the bogie according to the error in distance, but a
+ // little smaller than that. Taking the exact error as
+ // step only works on perfectly straight track, and we don't
+ // want to get stuck
+ b.step(d * falloff)
+ // Decrease the falloff by multiplying it with 0.9
+ // This basically calculates 0.9^n with n the amount
+ // of iterations. With the limit at 0.1 the max
+ // amount of iterations is 22.
+ falloff *= 0.9
+ // After making a step, calculate the error again.
+ // This is the distance to the next bogie, subtracted
+ // with our target distance.
+ d = b.dist(nextBogie) - b.distanceToNext
+ }
+ // The bogie we just put in the right place is the target
+ // for the next bogie
+ nextBogie = b
+ }
+
+ return nil
+}
+
+// Move a bogie along the track that it is on.
+// It is moved by a distance in the game, but
+// a bogie's position is kept as a progress on its track
+func (b *bogie) step(stepSize float32) {
+ // First of all turn that distance into a progress increment.
+ // This is like a percentage increase.
+ incr := stepSize / b.currentTrack.Length()
+ b.trackCompletion += incr
+
+ // If we overshoot our track (completion is bigger than 1.0 or 100%)
+ // we'll go to the beginning of next track.
+ // It's not a complete implementation actually.
+ // If the completion would be 1.2, that 0.2 overshoot
+ // should be turned back into a step size, and this
+ // step should be made on the next track.
+ // In reality this overshoot is going to be small
+ // so I don't bother with it.
+ if b.trackCompletion > 1.0 {
+ // Start on the new track on the beginning
+ b.trackCompletion = 0
+ // Ask our target connection, that we're moving towards,
+ // what the next track is. Very important to ask because
+ // a connection could be a switch, and it needs to decide
+ // for us where we're going.
+ b.currentTrack = b.target.NextTrack(b.currentTrack)
+ // Ask our new track what its connections are.
+ // One of the connections will be our current target,
+ // because that's the one we just crossed, and the
+ // other is our next target.
+ nextConnections := b.currentTrack.Connections()
+ // Is our target the first connection?
+ if nextConnections[0] == b.target {
+ // so our new target becomes the other of the track's connections
+ b.target = nextConnections[1]
+ // Nah its the second one
+ } else {
+ // so our new target becomes the other of the track's connections
+ b.target = nextConnections[0]
+ }
+ }
+
+ // This does the same as the previous block, except for under
+ // shoot. This happens when our completion goes below 0 when
+ // we're moving backwards.
+ // It might look simpler than going forward, but the same
+ // code is hidden behind the frontAndBack() function of a bogie.
+ if b.trackCompletion < 0.0 {
+ // Our bogie is on a track. A track has two connections
+ // and one of those is the bogie's target that the bogie
+ // looks at. That's the one in front of the bogie, and the
+ // other is in back. When undershooting a track
+ // this back bogie becomes our new target.
+ _, back := b.frontAndBack()
+ // Start the new track at the end, we're going backwards
+ // after all
+ b.trackCompletion = 1
+ // The bogie, going backwards, is now looking at the
+ // connection it just crossed, which used to be behind it
+ b.target = back
+ // Ask this connection what the next track is, given
+ // that we came from the previous track.
+ b.currentTrack = b.target.NextTrack(b.currentTrack)
+ }
+}