From: Fl_GUI Date: Fri, 7 Mar 2025 19:51:50 +0000 (+0100) Subject: initial commit X-Git-Url: https://git.openfl.eu/?a=commitdiff_plain;h=7cc7daab161e5b20fea27978bbf636b9773514b7;p=choochoo.git initial commit --- 7cc7daab161e5b20fea27978bbf636b9773514b7 diff --git a/choochoo.exe b/choochoo.exe new file mode 100755 index 0000000..782ea28 Binary files /dev/null and b/choochoo.exe differ diff --git a/choochoo.kra b/choochoo.kra new file mode 100644 index 0000000..14c879e Binary files /dev/null and b/choochoo.kra differ diff --git a/game/draw.go b/game/draw.go new file mode 100644 index 0000000..ac19896 --- /dev/null +++ b/game/draw.go @@ -0,0 +1,54 @@ +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 +} diff --git a/game/gametype.go b/game/gametype.go new file mode 100644 index 0000000..0d24de4 --- /dev/null +++ b/game/gametype.go @@ -0,0 +1,47 @@ +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) +} diff --git a/game/update.go b/game/update.go new file mode 100644 index 0000000..de19ae5 --- /dev/null +++ b/game/update.go @@ -0,0 +1,17 @@ +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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..78fff74 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a91e108 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..57b74a0 --- /dev/null +++ b/main.go @@ -0,0 +1,65 @@ +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) + } +} diff --git a/math/pointtype.go b/math/pointtype.go new file mode 100644 index 0000000..ad36e3b --- /dev/null +++ b/math/pointtype.go @@ -0,0 +1,47 @@ +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)) +} diff --git a/math/pointtype_test.go b/math/pointtype_test.go new file mode 100644 index 0000000..444caaf --- /dev/null +++ b/math/pointtype_test.go @@ -0,0 +1,60 @@ +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) + } + }) + } +} diff --git a/track/connectiontype.go b/track/connectiontype.go new file mode 100644 index 0000000..1e55498 --- /dev/null +++ b/track/connectiontype.go @@ -0,0 +1,77 @@ +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 +} diff --git a/track/draw.go b/track/draw.go new file mode 100644 index 0000000..af7aa90 --- /dev/null +++ b/track/draw.go @@ -0,0 +1,32 @@ +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) { +} diff --git a/track/straighttrack.go b/track/straighttrack.go new file mode 100644 index 0000000..a8dd5c3 --- /dev/null +++ b/track/straighttrack.go @@ -0,0 +1,69 @@ +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()) +} diff --git a/track/tracktype.go b/track/tracktype.go new file mode 100644 index 0000000..b5bd31b --- /dev/null +++ b/track/tracktype.go @@ -0,0 +1,23 @@ +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) +} diff --git a/train/draw.go b/train/draw.go new file mode 100644 index 0000000..4eca9af --- /dev/null +++ b/train/draw.go @@ -0,0 +1,103 @@ +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 diff --git a/train/traintype.go b/train/traintype.go new file mode 100644 index 0000000..d6e1989 --- /dev/null +++ b/train/traintype.go @@ -0,0 +1,109 @@ +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) +} diff --git a/train/update.go b/train/update.go new file mode 100644 index 0000000..f49d3ff --- /dev/null +++ b/train/update.go @@ -0,0 +1,128 @@ +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) + } +}