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!