DragonRuby: Following the Mouse
Recently I discovered it is very easy to have objects move towards (or away from) any points in DragonRuby.
This post might be a little easier if you’ve already read my post on moving in arbitrary directions, but actually the code here is even simpler.
If I skip any explanations here, the concepts should have been covered earlier in the series.
Setup
To get started, let’s just output a square (roughly) in the middle of the screen.
args.grid.center_x
and args.grid.center_y
are helpful for this instead of remembering/hardcoding the screen size.
In addition, the code uses args.state.tick_count == 0
to do some setup on the first tick.
def tick(args)
# On the first tick...
if args.state.tick_count == 0
# Create a 50x50 pixel square in the middle of the screen
args.state.player = { x: args.grid.center_x, y: args.grid.center_y, h: 50, w: 50}
# Output that square on every tick
args.outputs.static_solids << args.state.player
end
end
Pretty basic!
(Note I’m skipping straight to static_solids
because that’s what I’d prefer in a “real” game.)
Moving to a Point
Now we’ll move the “player” to a given point - in this case where the mouse is. In typical DragonRuby fashion, args.inputs.mouse
can be used to as a point, even though it has a bunch of other information attached to it.
To get the angle from the player to the mouse, there is a very convenient angle_to
method! (Also angle_from
depending on which way you’d like to go.)
Just like args.inputs.mouse
, the player
solid can be treated as if it is a point, too.
One the angle is calculated, vector_x
and vector_y
will provide the magnitude to move in the x
and y
directions.
def tick(args)
if args.state.tick_count == 0
args.state.player = {
x: args.grid.center_x,
y: args.grid.center_y,
h: 50,
w: 50
}
args.outputs.static_solids << args.state.player
end
# Find angle from the square to the current location of the mouse
angle = args.state.player.angle_to(args.inputs.mouse)
# Move towards the mouse using the unit vector
args.state.player.x += angle.vector_x
args.state.player.y += angle.vector_y
end
And… that’s it!! Less than 10 lines of code (without comments/spaces) and it just works.
So now let’s make it more complicated…
Centering
It is annoying that the player moves so the bottom, right-hand corner meets the mouse, there is a simple fix: use anchor_x
and anchor_y
. DragonRuby will automatically use the anchor point for calculations.
To learn more about anchor points, see this earlier post. But typically the values are set to 0.5
which means “middle of the object”.
def tick(args)
if args.state.tick_count == 0
args.state.player = {
x: args.grid.center_x,
y: args.grid.center_y,
h: 50,
w: 50,
anchor_x: 0.5,
anchor_y: 0.5,
}
args.outputs.static_solids << args.state.player
end
angle = args.state.player.angle_to(args.inputs.mouse)
args.state.player.x += angle.vector_x
args.state.player.y += angle.vector_y
end
Moving to Classes
I continue to prefer taking an object/class-based approach in my own code. This makes it much easier to manage as the code grows larger, even if it seems silly for these small examples.
In a little departure from previous posts, I am not walking through the code in detail. (Please check out my other posts in this series to learn more!)
The main difference from the above is adding a speed
to the movement calculation. Other than that, this is how I generally move from simple code like the above into a structure more suitable (in my opinion) as the code.
class Game
attr_gtk
def initialize(args)
# Separate setup method is easier if you need to
# reset during the game
setup(args)
end
def setup(args)
# Since static_sprites persists between ticks,
# need to clear it in case of reset
args.outputs.static_sprites.clear
@player = Player.new(x: args.grid.center_x, y: args.grid.center_y, h: 50, w: 50, speed: 2)
args.outputs.static_sprites << @player
end
def tick(args)
@player.tick(args)
end
end
class Player
attr_sprite
def initialize(x:, y:, h:, w:, speed:)
@x = x
@y = y
@h = h
@w = w
@anchor_x = 0.5
@anchor_y = 0.5
@speed = speed
end
def tick(args)
angle = self.angle_to(args.inputs.mouse)
@x += angle.vector_x * @speed
@y += angle.vector_y * @speed
end
end
def tick(args)
$Game ||= Game.new(args)
$Game.tick(args)
end
One minor note if you actually run this code - the square will be white (default for sprites) instead of black (default for solids).
DragonRuby: Smoothly Growing and Shrinking Sprites
This post is about DragonRuby, a Ruby implementation for writing games. Check it out!
Starting Off
Let’s start simply by rendering a sprite in the middle of the window. For convenience, the program also renders guidelines marking the center of the window (for brevity, not showing this in later code). Also for convenience, this code uses a sprite included with the DragonRuby distribution.
def tick args
# Double size of the original sprite, in pixels
width = 80 * 2
height = 80 * 2
# Output sprite in middle of the window
args.outputs.sprites << {
x: args.grid.center_x, # Horizontal center
y: args.grid.center_y, # Vertical center
w: width,
h: height,
path: 'sprites/hexagon/red.png',
}
# Add grid lines to mark center of window
args.outputs.lines << {
x: 0,
y: args.grid.center_y,
x2: args.grid.w,
y2: args.grid.center_y
} <<
{
x: args.grid.center_x,
y: 0,
x2: args.grid.center_x,
y2: args.grid.h
}
end
As you can see, we’ve perfectly rendered a sprite in the center of the -
Wait, that’s not quite right. What happened?
Anchoring
In DragonRuby, the (0, 0) coordinate is the bottom-left of the window. Similarly, when setting x
and y
location for a sprite, those correspond to the bottom-left of the sprite.
This could be solved with some math like x = x + (sprite.w / 2)
to adjust the sprite appropriately. But this will cause complications later when resizing the sprite.
Preview of the problem:
This might be what you want, but for this post we want the sprite to expand/shrink from the center.
A simpler approach that trying to move x
and y
around is to instead use the anchor_x
and anchor_y
attributes.
Important note: anchor_*
attributes were introduced in DragonRuby 4.8.
These are very similar to the angle_anchor_*
attributes discussed previously.
The values used for the anchors are a percentage of the width or height of the sprite. This diagram from the earlier post might help:
The default “anchors” are essentially at (0, 0) (technically, they are nil
, but never mind that).
The center of the sprite is at (0.5, 0.5).
In code:
def tick args
# Double size of the original sprite
width = 80 * 2
height = 80 * 2
# Actually render sprite in middle of the window
args.outputs.sprites << {
x: args.grid.center_x,
y: args.grid.center_y,
w: width,
h: height,
anchor_x: 0.5,
anchor_y: 0.5,
path: 'sprites/hexagon/red.png',
}
end
There we go! Digression over, back to squishing and stretching this sprite.
“Simple” Approach
For the first approach, let’s do this:
- Set a rate of growth (e.g. 1 pixel per tick)
- Set a target size
- Grow (or shrink) the size on each tick
- When the target size is met, reverse direction
To avoid some duplication, this code uses the same size for width and height. Adjust as desired.
def tick args
# How big to make the sprite
target_size = 80 * 2
# Current size of the sprite
args.state.size ||= 0
# How fast to grow
args.state.growth_rate ||= 1
# If the target size is reached, reverse
if args.state.size >= target_size
args.state.growth_rate = -1
elsif args.state.size <= 0
args.state.growth_rate = 1
end
# Grow (or shrink) the size
args.state.size += args.state.growth_rate
args.outputs.sprites << {
x: args.grid.center_x,
y: args.grid.center_y,
w: args.state.size,
h: args.state.size,
path: 'sprites/hexagon/red.png',
anchor_x: 0.5,
anchor_y: 0.5,
}
end
Result:
Nailed it. Post over..?
Easing In
Instead of calculating the growth rate “by hand,” wouldn’t it be nice if a function could do that for us? Maybe even have the ability to vary the growth rate over time?
args.easing.ease
is here for that very reason.
(This video, linked in the DragonRuby docs, is really good. Plus it also will explain the names of the easing functions used below.)
args.easing.ease
will return a “percentage” value (between 0 and 1) which can be multiplied against the target value to get the current value. For the example here, that means it will calculate the current percentage of the size of the the sprite.
def tick args
target_size = 80 * 2
duration = 60
args.state.start_time ||= 0
args.state.easing_function = :identity
# Calculate percentage (0 to 1) of progress based
# on the start time, current time, duration, and easing function
percentage = args.easing.ease args.state.start_time,
args.state.tick_count,
duration,
args.state.easing_function
# Output the scaled image
args.outputs.sprites << {
x: args.grid.center_x,
y: args.grid.center_y,
w: target_size * percentage,
h: target_size * percentage,
path: 'sprites/hexagon/red.png',
anchor_x: 0.5,
anchor_y: 0.5,
}
end
This code uses the :identity
function which is linear - essentially the same as the earlier code that adds a constant “growth rate” on each tick.
Here is the result:
Oops, forgot to shrink it back down!
Easing Out
The code is going to get slightly more complicated now. Instead of passing in a single easing function, the new code uses an array of function names. The array is “splatted” into args.ease.easing
.
When the set duration is up, the code adds :flip
to the list of functions. Instead of going from 0 to 1, the percentage will now go from 1 to 0.
When that’s over, :flip
is removed from the list and it starts all over.
def tick args
target_size = 80 * 2
duration = 60
args.state.start_time ||= 0
# List of easing functions
args.state.easing_functions ||= [:identity]
# Calculate percentage (0 to 1) of progress based
# on the start time, current time, duration, and easing function(s)
percentage = args.easing.ease args.state.start_time,
args.state.tick_count,
duration,
*args.state.easing_functions
# When we reach the end of the duration, switch direction
if args.state.tick_count == args.state.start_time + duration
# Reset the start time for the easing function
args.state.start_time = args.state.tick_count
if args.state.easing_functions == [:identity]
args.state.easing_functions = [:identity, :flip]
else
args.state.easing_functions = [:identity]
end
end
# Output the scaled image
args.outputs.sprites << {
x: args.grid.center_x,
y: args.grid.center_y,
w: target_size * percentage,
h: target_size * percentage,
path: 'sprites/hexagon/red.png',
anchor_x: 0.5,
anchor_y: 0.5,
}
end
One way to think of [:identity, :flip]
is like this:
:identity
is f(x) = x and :identity, :flip
is g(x) = 1 - f(x). :flip
can be used to “reverse” any function.
More Easing
This may not be very exciting, but keep in mind there are several pre-defined easing functions:
:identity
(f(x) = x):quad
(f(x) = x^2):cube
(f(x) = x^3):quint
(f(x) = x^4):smooth_start_quad
(same as:quad
):smooth_start_cube
(same as:cube
):smooth_start_quart
(same as:quart
):smooth_start_quint
(same as:quint
):smooth_stop_quad
(f(x) = 1 - (1 - x)^2):smooth_stop_cube
(f(x) = 1 - (1 - x)^3):smooth_stop_quart
(f(x) = 1 - (1 - x)^4):smooth_stop_quint
(f(x) = 1 - (1 - x)^5)
Mix and match as you’d like… for example, here’s growing with :cube
but shrinking with :quint
:
def tick args
target_size = 80 * 2
duration = 60
args.state.start_time ||= 0
# List of easing functions
args.state.easing_functions ||= [:cube]
# Calculate percentage (0 to 1) of progress based
# on the start time, current time, duration, and easing function(s)
percentage = args.easing.ease args.state.start_time,
args.state.tick_count,
duration,
*args.state.easing_functions
# When we reach the end of the duration, switch direction
if args.state.tick_count == args.state.start_time + duration
args.state.start_time = args.state.tick_count
if args.state.easing_functions == [:cube]
args.state.easing_functions = [:quint, :flip]
else
args.state.easing_functions = [:cube]
end
end
# Output the scaled image
args.outputs.sprites << {
x: args.grid.center_x,
y: args.grid.center_y,
w: target_size * percentage,
h: target_size * percentage,
path: 'sprites/hexagon/red.png',
anchor_x: 0.5,
anchor_y: 0.5,
}
end
Closing Out
This post demonstrates two concepts: changing the anchors for a sprite, and using “easing” to set the size of a sprite.
Easing is a general-purpose concept that can be used for any applications, such as smooth movement.
Keep in mind, you can also:
- Change up the anchors
- Grow/shrink width and height separately
- Change up the target width/height as desired (for example, grow all the way but only shrink back a little)
- Combine several easing functions together
- Write your own easing functions
- ???
Have fun!
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!