Justin

Posted on

# DragonRuby: Moving in Arbitrary Directions

In a previous post we looked at rotating rectangles in DragonRuby.

Now let's take that one step further to try turning and moving!

In this post, we'll look at very simple movement of a spaceship.

## Setup

First, let's get the state all ready.

The code below puts the sprite of a ship in the middle of the screen.

``````def tick args
# Setting up initial state
args.state.ship.w ||= 50
args.state.ship.h ||= 50
args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
args.state.ship.angle ||= 0
args.state.ship.speed ||= 0

# Show the ship
args.outputs.sprites << {
x: args.state.ship.x,
y: args.state.ship.y,
w: args.state.ship.w,
h: args.state.ship.h,
path: 'sprites/ship.png',
angle: args.state.ship.angle,
}
end
``````

Not too exciting thus far, but we'll make it better.

## Rotation

For rotation, we'll turn the ship `2.5` degrees when the left or right arrow keys are pressed or held down.

``````def tick args
# Setting up initial state
args.state.ship.w ||= 50
args.state.ship.h ||= 50
args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
args.state.ship.angle ||= 0
args.state.ship.speed ||= 0

# Turn left and right
if args.inputs.keyboard.right
args.state.ship.angle += 2.5
elsif args.inputs.keyboard.left
args.state.ship.angle -= 2.5
end

# Keep angle between 0 and 360
args.state.ship.angle %= 360

# Show the ship
args.outputs.sprites << {
x: args.state.ship.x,
y: args.state.ship.y,
w: args.state.ship.w,
h: args.state.ship.h,
path: 'sprites/ship.png',
angle: args.state.ship.angle,
}
end
``````

Important to note DragonRuby puts `0` straight to the right or "due east", with increasing angles rotating counter-clockwise.

## Acceleration

Still keeping things simple, let's accelerate the ship when the up arrow is held down and decelerate otherwise.

For now we won't worry about what direction the ship is heading.

``````def tick args
# Setting up initial state
args.state.ship.w ||= 50
args.state.ship.h ||= 50
args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
args.state.ship.angle ||= 0
args.state.ship.speed ||= 0

# Accelerate with up arrow, otherwise decelerate
if args.inputs.keyboard.up
args.state.ship.speed += 0.2
else
args.state.ship.speed -= 0.1
end

# Keep speed between 0 and 10
args.state.ship.speed = args.state.ship.speed.clamp(0, 10)

# Turn left and right
if args.inputs.keyboard.right
args.state.ship.angle -= 2.5
elsif args.inputs.keyboard.left
args.state.ship.angle += 2.5
end

# Keep angle between 0 and 360
args.state.ship.angle %= 360

# Go?
args.state.ship.x += args.state.ship.speed

# Show the ship
args.outputs.sprites << {
x: args.state.ship.x,
y: args.state.ship.y,
w: args.state.ship.w,
h: args.state.ship.h,
path: 'sprites/ship.png',
angle: args.state.ship.angle,
}
end
``````

## Moving in Arbitrary Directions

We are almost there!

In the code above, we only update the ship's `x` position. This is just to make sure we have the acceleration/deceleration working how we'd like.

But what we really want is to go in the direction the ship is pointing!

To do so, we need to update both the `x` and `y` position, proportional to the angle and speed... ugh that sounds like we might need some math! Trigonometry even??

Actually, DragonRuby comes to the rescue here! `Integer#vector` will return a unit vector (`[x, y]` where `x` and `y` are between `-1` and `1`) corresponding to the angle.

For example:

``````0.vector # => [1.0, 0.0]
``````

So at an angle of 0 degrees, only the `x` direction is affected.

But all we really need to know is that `angle.vector_x` and `angle.vector_y` will give us the "magnitude" we need to convert speed and angle to x and y distance:

``````  args.state.ship.x += args.state.ship.speed * args.state.ship.angle.vector_x
args.state.ship.y += args.state.ship.speed * args.state.ship.angle.vector_y
``````

Putting that into context:

``````def tick args
# Setting up initial state
args.state.ship.w ||= 50
args.state.ship.h ||= 50
args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
args.state.ship.angle ||= 0
args.state.ship.speed ||= 0

# Accelerate with up arrow, otherwise decelerate
if args.inputs.keyboard.up
args.state.ship.speed += 0.2
else
args.state.ship.speed -= 0.1
end

# Keep speed between 0 and 10
args.state.ship.speed = args.state.ship.speed.clamp(0, 10)

# Turn left and right
if args.inputs.keyboard.right
args.state.ship.angle -= 2.5
elsif args.inputs.keyboard.left
args.state.ship.angle += 2.5
end

# Keep angle between 0 and 360
args.state.ship.angle %= 360

# Go in the correct direction!
args.state.ship.x += args.state.ship.speed * args.state.ship.angle.vector_x
args.state.ship.y += args.state.ship.speed * args.state.ship.angle.vector_y

# Show the ship
args.outputs.sprites << {
x: args.state.ship.x,
y: args.state.ship.y,
w: args.state.ship.w,
h: args.state.ship.h,
path: 'sprites/ship.png',
angle: args.state.ship.angle,
}
end
``````

## Staying Within Bounds

Just to round this out, let's do Astroids-style wrap-around to keep the ship on the screen.

`args.grid.right` is the width of the screen and `args.grid.top` can be used for the height.

``````def tick args
# Setting up initial state
args.state.ship.w ||= 50
args.state.ship.h ||= 50
args.state.ship.x ||= args.grid.center_x - (args.state.ship.w / 2)
args.state.ship.y ||= args.grid.center_y - (args.state.ship.h / 2)
args.state.ship.angle ||= 0
args.state.ship.speed ||= 0

# Accelerate with up arrow, otherwise decelerate
if args.inputs.keyboard.up
args.state.ship.speed += 0.2
else
args.state.ship.speed -= 0.1
end

# Keep speed between 0 and 10
args.state.ship.speed = args.state.ship.speed.clamp(0, 10)

# Turn left and right
if args.inputs.keyboard.right
args.state.ship.angle -= 2.5
elsif args.inputs.keyboard.left
args.state.ship.angle += 2.5
end

# Keep angle between 0 and 360
args.state.ship.angle %= 360

# Go in the right direction!
args.state.ship.x += args.state.ship.speed * args.state.ship.angle.vector_x
args.state.ship.y += args.state.ship.speed * args.state.ship.angle.vector_y

# Wrap around to keep the ship on the screen
args.state.ship.x %= args.grid.right
args.state.ship.y %= args.grid.top

# Show the ship
args.outputs.sprites << {
x: args.state.ship.x,
y: args.state.ship.y,
w: args.state.ship.w,
h: args.state.ship.h,
path: 'sprites/ship.png',
angle: args.state.ship.angle,
}
end
``````

## Conclusion

And there we go. In less than 50 lines of code and with no complicated math, we can move an object around in arbitrary directions!