DragonRuby: Following the Mouse

presidentbeef

Justin

Posted on December 22, 2023

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
Enter fullscreen mode Exit fullscreen mode

A square in the middle of a window

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
Enter fullscreen mode Exit fullscreen mode

Square following the mouse around

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
Enter fullscreen mode Exit fullscreen mode

Square following the mouse around, but centered

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
Enter fullscreen mode Exit fullscreen mode

One minor note if you actually run this code - the square will be white (default for sprites) instead of black (default for solids).

💖 💪 🙅 🚩
presidentbeef
Justin

Posted on December 22, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related