3
\$\begingroup\$

I'm working on a hobby game, basically about train dispatcher's job.

(stack: Typescript, phaser.js)

My map is based on a grid, each tile can hold 1 rail tile (well, except junctions, but we can simplify it for now, since junctions are just multiple tiles, from which only one is active).

Example (black is grid, white is rail):

example rails

As you can see, we have basically three different tiles: straight rail, 45 degree turn and diagonal rail (which can be also considered straight rail). I don't plan to add more tiles, as you can already build pretty complex setups from these tiles.

On these rails, trains would drive. Each carriage has two "wheels" - positions, where it snaps to a track. This may be seen on the following example from my prototype:

working prototype

However the existing system (see below) has some issues:

  • when a train travels diagonally, sometimes it's x and y coordinates fit in a field than it should (example: traveling from 1|1 to 2|2, but at one point the train's coordinates correspond to the field 1|2 or 2|1).
  • axles sometimes get closer or futher away from each other. This has almost no effect on resulting simulation (as the carriage is centered between those two axles), but may cause carriages to be closer or futher away from each other (this effect is visible on the prototype as weell)
  • moving the train in a certain direction is handled by a big chunk of code that essentially lists all possibilities of directions and handles it. I'm interested if there's a better way to do it, so I could simplify the code.

I decided that I would improve the core. I'm wondering, what would be the best way to do this?

What I am looking for:

  • how to save the rail pieces on the map?
  • how to move train (I know it's x, y, speed and acceleration)?
  • how to have the train continue to the next piece on the grid?

Current solution (broken in some edge cases, also not ideal):

  • each rail piece has it's class (CurvedRail, StraightRail)
  • each rail piece class has method movePoint, that accepts x, y, direction and distance and returns new point (that is distance away from the original point, but also considering track turns). It may also return delta, if the movement reached edge of the tile and it needs to be moved futher (on another piece). This is handled by a separate class, that finds the following rail piece and calls movePoint with the delta instead
  • it has issues, because sometimes trains passing diagonal connection don't exactly follow the diagonal, so they instead of ending up on the next diagonal piece, they end up on neighboring horizontal or vertical piece and derail.

One of the issues I may be facing is that tiles are 32x32 pixels, so it's hard to travel exactly in the "middle" of the piece, because it's "between two pixels", and although I'm using decimals, it may be a problem. Is it better to have a grid from odd-sized tiles?

\$\endgroup\$
  • 1
    \$\begingroup\$ I am struggling to distill the issues you are encountering with your current implementation. Perhaps including a photo/gif of the incorrect behavior would help explain. What alternative implementations have you considered? How is the grid represented in your game? If you are primarily interested in how to architect grid based movement you might try searching for some source code for Snake, since its movement is very similar to your desired effect (grid based, linked series of bodies that move following the head). \$\endgroup\$ – disc_code22 Nov 12 '19 at 20:41
  • 1
    \$\begingroup\$ "One of the issues I may be facing is that tiles are 32x32 pixels, so it's hard to travel exactly in the "middle" of the piece..." it sounds like you may want to decouple the logical location of your train's axle, eg. ("30% of the way along tile (5, 3)") from its rendered position ("pixel (243, 182)").Then whether the center of a tile is at pixel 15 or 16 is a problem only your rendering code needs to deal with, and it never needs to impact your movement logic. This frees your renderer to do things like always round in a set direction regardless of rotation, or blend/dither to average it out. \$\endgroup\$ – DMGregory Nov 13 '19 at 3:03
  • 1
    \$\begingroup\$ @disc_code22 the issue is happening only in one direction (that isn't in the gif, SW direction). The train is located for example at coordinates that translate to tile 1|3 and should move to 2|2 next, but somehow it ends up on coordinates that translate to tile 1|2 or 2|3. That's why one of the things I'm looking for is how to handle the train movement in a way that this can't happen. Snake isn't the perfect example, since in my implementation, train can be on any x, y, not just integer numbers. (that's needed for smooth movement) \$\endgroup\$ – Tomáš Blatný Nov 13 '19 at 3:32
  • 1
    \$\begingroup\$ @DMGregory the engine I'm using accepts decimals for x,y position of a sprite, so I'm using that to store the train's absolute x, y (as well as it is used for rendering). Although your idea of storing some percentage (or length) traveled on a certain tile sounds good. Then I would remove the need of converting coordinates to tiles and would only convert tiles + distance to coordinates (which is probably one case of some of my issues). \$\endgroup\$ – Tomáš Blatný Nov 13 '19 at 3:35
  • 1
    \$\begingroup\$ I'd love to see that description of your error case (especially full repro steps) edited into your question itself rather than in a comment. \$\endgroup\$ – DMGregory Nov 13 '19 at 3:38
4
+100
\$\begingroup\$

On tracking

Your current simulation behaves pretty well, I would say. Because a rail wagon that is halfway a sharp bend, will indeed exhibit the behaviour that the wagon's center is no longer above the track, but next to the track.

Center of the tile

Traveling over the exact middle of a 32x32 tile: This is easily accomplished if you were to draw the rails with 2-pixel wide lines. (And the trains as 4 pixel or 6 pixels wide.)

Encoding

Encoding the tiles... You need to make a choice on whether your network is directional or not. If all tiles can be traversed in both directions, then there is no distinction between entrances and exits to the tile.

An entry/exit point can fall on any of the four tile corners, or any of the four tile edges.

You can then define a tile with an 8-bit value where a one-bit means a gate is present, and a zero-bit means there is no exit/entry for that location.

If you consider only single-track per file, not all 8-bit values would be valid, only the ones that have 2 bits set to one, and of those, only if the turn is not too sharp.

Take, e.g. the tile below. It would have encoding 00010010b which is decimal value 34.

In the example network you provided, you would filter out the cases where the turn is too acute. So if there is a gate at bit 1, you only allow the other gate at 4, 5 or 6.

This gives you ( 8 * 3 ) / 2 possible encodings, so just 12 of them. Or 24 if you define your tracks to have a direction (explicit entrance and exit.)

tile encoding

possible encodings:

 // 3 cases with gate at 0:
 00001001
 00010001
 00100001

 // 3 cases with gate at 1:
 00010010
 00100010
 01000010

 // 3 cases with gate at 2:
 00100100
 01000100
 10000100

 // 2 remaining cases with gate at 3:
 01001000
 10001000
(00001001) duplicate

 // 1 remaining case with gate at 4:
 10010000
(00010001) duplicate
(00010010) duplicate

You could chose to store the tile pattern as this 8-bit value [0..256) or as a case number [0..12) if you prefer.

Train Travel

To move a train (or wagon) between tiles, you would need to keep track of a 'from' tile and a 'to' tile. Let's call them tile_fr and tile_to.

And then keep a parameter 0 <= t <= 1 that records where on the track the train is.

At t==0, the tail of the wagon is at the start of tile_fr. At t==q, the nose of the wagon is at the end of tile_fr, at t==1, the tail of the wagon is at the start of tile_to.

If q <= t <= 1 then the wagon is occupying both tile_fr and tile_to partly.

When t has reached 1, you need to swap the tiles. Tile_fr is set to the old tile_to value, and tile_to will be assigned the next tile.

Progression

Axle Travel

As you stated, when consider positions of wheels (axles, really) it gets easier. You track a progression parameter for the rear-axle, and one for the front-axle.

To deal with differences in track-lengths per tile, you can modulate the speed at which you move the axle parameters along the track.

If you define the straight-track piece as having unit length, then you can determine lengths L₁ for the bend, and L₂ for the diagonal track across a cell.

When an axle position parameter is moved along the track, you divide the speed by the track length, so use (v / L₁) to move the parameter along the cell with a bend.

The rationale: On longer pieces of track, you need to take more time to progress from value 0 to value 1, so that you get the same distance per unit of time.

Alternative Grid

Let me conclude with describing the approach I used in my train simulation. First off, my tiles are hexagons, not squares. Next, my tracks are splines, not line segments. Last, my tracks are directional, meaning there is an entrance edge (0..5) and an other edge as exit, which MUST NOT be a neighbour of the entrance edge. This gives me 6*3 or 18 different cases. Each track goes either L, R or S. A nice bonus is that the splines in all three cases (L,R,S) has pretty much the same length, which is not the case in your square-crossings.

hexagon encoding

Using splines means that there will be no sharp corners, which may or may not be something you want. If you do, look into Hermite Splines, for example.

hexagon world

\$\endgroup\$
  • \$\begingroup\$ Thanks for your ideas. The bit encoding definitely is something that I'll consider. For your second point - yes, tracks are 2 tiles wide and trains are 6 or 8 tiles wide (can't remember now). I forgot to mention that in the question. One of the things I'm looking for is the trian movement over the tile and transitioning onto the next tile. But I guess the biggest issue there is my crappy code :) I've thought a bit about splines, but decided not to use them in the first phase. I'll definitely look to the link you provided and see if I can use some of that. \$\endgroup\$ – Tomáš Blatný Nov 13 '19 at 3:28
  • \$\begingroup\$ @TomášBlatný I've added a section on how to transition a train between two tiles. \$\endgroup\$ – Bram Nov 13 '19 at 4:03
  • \$\begingroup\$ thanks, that's really useful as well. I'll need to modify it a little though. My trains have two axles and they are at a certain position of a train (e.g. 2 meters = 2 pixels from start). That is just a graphical thing tho. I count each axle separately, so I probably don't need the "transition" state, when train occupies both tiles, since I'm only wondering about the one axle. \$\endgroup\$ – Tomáš Blatný Nov 13 '19 at 4:20
  • \$\begingroup\$ The % of tile traveled is a good idea (also was mentioned in comments under original question), so I'll almost definitely use that. I just need to handle different lengths of tile (turn is slightly longer than a straight track), but that can be handled for each "part" of the tile individually (basically splitting each tile in two parts, so I only calculate stuff for straight lines without curves). \$\endgroup\$ – Tomáš Blatný Nov 13 '19 at 4:20
  • \$\begingroup\$ unless another better answer comes in, I'll give you the bounty. It says I can do it in 16 hours. \$\endgroup\$ – Tomáš Blatný Nov 13 '19 at 4:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.