the_empire_blog

Let’s try and move that circle around a little bit. We want to:

I’ll be following the tutorial on transforming entities and managing events.

If we want to move our circle around, let’s cache it in the initializer, and render from the instance variable:

# src/the_empire.cr

class TheEmpire
  def initialize
    ...

    @shape = SF::CircleShape.new(300)
    @shape.fill_color = SF::Color::Black
  end

  ...

  def render
    @window.clear(SF::Color::White)
    @window.draw(@shape)
    @window.display
  end
end

Moving around with AWSD

Since we already have the system of handling events, we just need to handle them:

# src/the_empire.cr, class TheEmpire

def handle_event(event : SF::Event::KeyPressed)
  case event.code
  when SF::Keyboard::Key::W then @shape.move(0, MOVING_SPEED)
  when SF::Keyboard::Key::S then @shape.move(0, -MOVING_SPEED)
  when SF::Keyboard::Key::A then @shape.move(MOVING_SPEED, 0)
  when SF::Keyboard::Key::D then @shape.move(-MOVING_SPEED, 0)
  end
end

If a SF::Event::KeyPressed event is received, for the key codes of A, W, S and D, @shape.move around. How do I know that this is the right combination of arguments as a response to each of AWSD keys? I tried a different way, it didn’t work like expected, and I just fiddled with it until it was right, of course. Let’s see:

Well, that works, but holy smokes, it’s SO JANKY… Looking more carefully at the tutorial finds me this piece:

Sometimes, people try to react to KeyPressed events directly to implement smooth movement. Doing so will not produce the expected effect, because when you hold a key you only get a few events (remember, the repeat delay). To achieve smooth movement with events, you must use a boolean that you set on KeyPressed and clear on KeyReleased; you can then move (independently of events) as long as the boolean is set.

The doc could be a little less passive-agresive about it, but ok. In that case, let’s follow it’s suggestion and define the additional values:

# src/the_empire.cr, class TheEmpire

property moving_up_speed = 0, moving_left_speed = 0

I did ints instead of booleans, because that will allow us to encode all 4 directions with 2 variables. How we react to SF::Event::KeyPressed also needs to change, plus we need SF::Event::KeyReleased:

# src/the_empire.cr, class TheEmpire

def handle_event(event : SF::Event::KeyPressed)
  case event.code
  when SF::Keyboard::Key::W then self.moving_up_speed = -MOVING_SPEED
  when SF::Keyboard::Key::S then self.moving_up_speed = MOVING_SPEED
  when SF::Keyboard::Key::A then self.moving_left_speed = -MOVING_SPEED
  when SF::Keyboard::Key::D then self.moving_left_speed = MOVING_SPEED
  end
end

def handle_event(event : SF::Event::KeyReleased)
  case event.code
  when SF::Keyboard::Key::W then self.moving_up_speed = 0
  when SF::Keyboard::Key::S then self.moving_up_speed = 0
  when SF::Keyboard::Key::A then self.moving_left_speed = 0
  when SF::Keyboard::Key::D then self.moving_left_speed = 0
  end
end

If we press AWSD, we are now moving in the respective direction, and once we release, we are no longer moving. This, by itself, doesn’t do anything quite yet. We need to introduce additional element into the main loop: updating;

# src/main.cr

require "./the_empire"

the_empire = TheEmpire.new

while the_empire.running?
  the_empire.handle_events
  the_empire.update
  the_empire.render
end

First we handle any user events, then we update the world, and finally render. For now, the update is very simple:

# src/the_empire.cr, class TheEmpire

def update
  if moving_up_speed != 0 || moving_left_speed != 0
    move({moving_left_speed, moving_up_speed})
  end
end

def move(vector)
  @shape.move(vector)
end

Et voila:

I am aware it doesn’t immedietely look all that better, but that is because of poor quality of the recorded clip. In actual app, it’s silky smooth.

Moving around with mouse

Second thing I want to do is to drag and drop the ball. There are 3 events, which I’m interested in for this: SF::Event::MouseButtonPressed, SF::Event::MouseButtonReleased and SF::Event::MouseMoved. This will happen in 3 stages:

This would have been trivial if SF::Event::MouseMoved had deltas (that is, gave us information about how much movement happened since the last one), but it doesn’t. We will have to calculate it ourselves:

# src/the_empire.cr, class TheEmpire

def handle_event(event : SF::Event::MouseButtonPressed)
  @moving_around = true
  @mouse_button_initial_x = event.x
  @mouse_button_initial_y = event.y
end

def handle_event(event : SF::Event::MouseButtonReleased)
  @moving_around = false
end

def handle_event(event : SF::Event::MouseMoved)
  if @moving_around
    x_delta = event.x - @mouse_button_initial_x
    y_delta = event.y - @mouse_button_initial_y

    move({x_delta, y_delta})

    @mouse_button_initial_x = event.x
    @mouse_button_initial_y = event.y
  end
end

Which gives us this beauty:

This leaves us with…

Zooming

This one is fairly easy:

# src/the_empire.cr, class TheEmpire

def handle_event(event : SF::Event::MouseWheelMoved)
  if event.delta > 0
    scale(1.25)
  else
    scale(0.8)
  end
end

def scale(factor)
  @shape.scale(factor, factor)
end

When we scroll up, delta is positive, and we want to make things 25% bigger. When we scroll down, delta is negative, and we want to make things 20% smaller. This way, if we scroll up, scrolling down will set us back on the value we were before. No chance we’ll end up on any weird values.

Easy enough:

Final code

And there it is. We can now move our black ball around and zoom it. In full, the code is currently like this:

require "crsfml"

class TheEmpire
  MOVING_SPEED = 10

  property moving_up_speed = 0, moving_left_speed = 0

  def initialize
    @window_width = 1920
    @window_height = 1080

    @window = SF::RenderWindow.new(SF::VideoMode.new(@window_width, @window_height), "My window")
    # If I don't do that, it actually renders ~20000 FPS, lol
    @window.framerate_limit = 60
    # I have 3 screens, and if I don't set it, it renders in my left-most screen.
    # I would appreciate the option to define main screen, actually.
    @window.position = SF.vector2(6500, 1800)

    @shape = SF::CircleShape.new(300)
    @shape.fill_color = SF::Color::Black

    @moving_around = false
    @mouse_button_initial_x = 0
    @mouse_button_initial_y = 0
  end

  def running?
    @window.open?
  end

  def handle_events
    while event = @window.poll_event
      handle_event(event)
    end
  end

  # Handle the close event specifically
  def handle_event(event : SF::Event::Closed)
    @window.close
  end

  def handle_event(event : SF::Event::KeyPressed)
    case event.code
    when SF::Keyboard::Key::W then self.moving_up_speed = -MOVING_SPEED
    when SF::Keyboard::Key::S then self.moving_up_speed = MOVING_SPEED
    when SF::Keyboard::Key::A then self.moving_left_speed = -MOVING_SPEED
    when SF::Keyboard::Key::D then self.moving_left_speed = MOVING_SPEED
    end
  end

  def handle_event(event : SF::Event::KeyReleased)
    case event.code
    when SF::Keyboard::Key::W then self.moving_up_speed = 0
    when SF::Keyboard::Key::S then self.moving_up_speed = 0
    when SF::Keyboard::Key::A then self.moving_left_speed = 0
    when SF::Keyboard::Key::D then self.moving_left_speed = 0
    end
  end

  def handle_event(event : SF::Event::MouseButtonPressed)
    @moving_around = true
    @mouse_button_initial_x = event.x
    @mouse_button_initial_y = event.y
  end

  def handle_event(event : SF::Event::MouseButtonReleased)
    @moving_around = false
  end

  def handle_event(event : SF::Event::MouseMoved)
    if @moving_around
      x_delta = event.x - @mouse_button_initial_x
      y_delta = event.y - @mouse_button_initial_y

      move({x_delta, y_delta})

      @mouse_button_initial_x = event.x
      @mouse_button_initial_y = event.y
    end
  end

  def handle_event(event : SF::Event::MouseWheelMoved)
    if event.delta > 0
      scale(1.25)
    else
      scale(0.8)
    end
  end

  # Ignore any other event
  def handle_event(event)
  end

  def update
    if moving_up_speed != 0 || moving_left_speed != 0
      move({moving_left_speed, moving_up_speed})
    end
  end

  def move(vector)
    @shape.move(vector)
  end

  def scale(factor)
    @shape.scale(factor, factor)
  end

  def render
    @window.clear(SF::Color::White)
    @window.draw(@shape)
    @window.display
  end
end

So now, that the basics are done, I would like to start thinking about the UI for map painting app!