Home > Enterprise >  Calculating the 2D turn movement required given an incoming and outgoing direction
Calculating the 2D turn movement required given an incoming and outgoing direction

Time:11-06

Consider a 2D square tiled grid (chess board like) which contains conveyor belt like structures that can curve and move game pieces around.

I need to calculate the turn movement (TURN_LEFT, TURN_RIGHT or STAY), depending on

  1. the direction from which a piece moves onto the field
  2. the direction from which the underlying belt exits the field

Example:

    1   2 
1 |>X>|>v |
2 |   | v |

The belt makes a RIGHT turn. As such, the result of calcTurn(LEFT, DOWN) should be TURN_RIGHT. Meaning the X game piece will be rotated 90° right when it moves over the curve at (1,2).

I already implemented a function but it only works on some of my test cases.

enum class Direction {
    NONE,
    UP,
    RIGHT,
    DOWN,
    LEFT;

    fun isOpposite(other: Direction) = this == UP && other == DOWN
            || this == DOWN && other == UP
            || this == LEFT && other == RIGHT
            || this == RIGHT && other == LEFT
}

data class Vec2(val x: Float, val y: Float)

fun Direction.toVec2() = when (this) {
    Direction.NONE -> Vec2(0f, 0f)
    Direction.UP -> Vec2(0f, 1f)
    Direction.RIGHT -> Vec2(1f, 0f)
    Direction.DOWN -> Vec2(0f, -1f)
    Direction.LEFT -> Vec2(-1f, 0f)
}

fun getTurnMovement(incomingDirection: Direction, outgoingDirection: Direction): Movement {
    if (incomingDirection.isOpposite(outgoingDirection) || incomingDirection == outgoingDirection) {
        return Movement.STAY
    }

    val incVec = incomingDirection.toVec2()
    val outVec = outgoingDirection.toVec2()

    val angle = atan2(
        incVec.x * outVec.x - incVec.y * outVec.y,
        incVec.x * outVec.x   incVec.y * outVec.y
    )

    return when {
        angle < 0 -> Movement.TURN_RIGHT
        angle > 0 -> Movement.TURN_LEFT
        else -> Movement.STAY
    }
}

I can't quite figure out what's going wrong here, especially not because some test cases work (like DOWN LEFT=TURN_LEFT) but others don't (like DOWN RIGHT=STAY instead of TURN_LEFT)

CodePudding user response:

You're trying to calculate the angle between two two-dimensional vectors, but are doing so incorrectly.

Mathematically, given two vectors (x1,y1) and (x2,y2), the angle between them is the angle of the second to the x-axis minus the angle of the first to the x-axis. In equation form: arctan(y2/x2) - arctan(y1/x1).

Translating that to Kotlin, you should instead use:

val angle = atan2(outVec.y, outVec.x) - atan2(incVec.y, incVec.x)

I'd note that you could achieve also your overall goal by just delineating the cases in a when statement as you only have a small number of possible directions, but perhaps you want a more general solution.

CodePudding user response:

It's not answering your question of why your code isn't working, but here's another general approach you could use for wrapping ordered data like this:

enum class Direction {
    
    UP, RIGHT, DOWN, LEFT;
    
    companion object {
        // storing thing means you only need to generate the array once
        private val directions = values()
        private fun getPositionWrapped(pos: Int) = directions[(pos).mod(directions.size)]
    }
    
    // using getters here as a general example
    val toLeft get() = getPositionWrapped(ordinal - 1)
    val toRight get() = getPositionWrapped(ordinal   1)
    val opposite get() = getPositionWrapped(ordinal   2)
}

It's taking advantage of the fact enums are ordered, with an ordinal property to pull out the position of a particular constant. It also uses the (x).mod(y) trick where if x is negative, putting it in parentheses makes it wrap around

    x|  6  5  4  3  2  1  0 -1 -2 -3 -4 -5
mod 4|  2  1  0  3  2  1  0  3  2  1  0  3

which makes it easy to grab the next or previous (or however far you want to jump) index, acting like a circular array.

Since you have a NONE value in your example (which obviously doesn't fit into this pattern) I'd probably represent that with a null Direction? instead, since it's more of a lack of a value than an actual type of direction. Depends what you're doing of course!

  • Related