目次

CrSFML Pong

src/crsfml-pong-take-5.cr

require "crsfml/graphics"
require "./shapeaux"

GAME_WIDTH   =       800
GAME_HEIGHT  =       600
PADDLE_SPEED = 300.0_f32
BALL_SPEED   = 400.0_f32

alias ActionMap = Hash(String, SF::Keyboard::Key)

class Paddle < SF::Transformable
  include SF::Drawable

  def initialize(position : SF::Vector2f, size : SF::Vector2f,
                 color : SF::Color, speed : Float32, action_map : ActionMap)
    super()

    @shape = SF::RectangleShape.new(size)
    @shape.origin = {size.x / 2, size.y / 2}
    @shape.fill_color = color

    @speed = speed
    @action_map = action_map
    @input_direction = SF.vector2f 0, 0

    self.position = position
  end

  def process_input
    @input_direction.y = 0
    if SF::Keyboard.key_pressed? @action_map["up"]
      @input_direction.y = -1.0
    end
    if SF::Keyboard.key_pressed? @action_map["down"]
      @input_direction.y = +1.0
    end
  end

  def update(delta_time : SF::Time)
    dy = @input_direction.y * @speed * delta_time.as_seconds
    move 0, dy

    rect = collision_rect
    if top(rect) < 0
      self.position = {position.x, @shape.origin.y }
    end
    if bottom(rect)> GAME_HEIGHT
      self.position = {position.x,
                       GAME_HEIGHT - rect.height + @shape.origin.y }
    end
  end

  def draw(target : SF::RenderTarget, states : SF::RenderStates)
    states.transform *= transform
    target.draw @shape, states
  end

  def collision_rect : SF::FloatRect
    SF::FloatRect.new(position - @shape.origin, @shape.size)
  end
end

class Ball < SF::Transformable
  include SF::Drawable

  def initialize(position : SF::Vector2f, radius : Float32,
                  color : SF::Color, velocity : SF::Vector2f)
    super()

    @shape = SF::CircleShape.new radius
    @shape.origin = { radius, radius }
    @shape.fill_color = color

    @velocity = velocity

    self.position = position
  end

  def update(delta_time : SF::Time, paddles : Array(Paddle))
    update_x delta_time, paddles
    update_y delta_time, paddles
  end

  def draw(target : SF::RenderTarget, states : SF::RenderStates)
    states.transform *= transform
    target.draw @shape, states
  end

  private def update_x(delta_time : SF::Time, paddles : Array(Paddle))
    dx = @velocity.x * delta_time.as_seconds
    move dx, 0

    paddles.each do |p|
      c = collision_circle
      r = p.collision_rect

      if circle_rect_collide? c, r
        if c.center.x > right(r)
          self.position = {right(r) + @shape.origin.x, position.y}
          @velocity.x *= -1
        elsif c.center.x < left(r)
          self.position = {left(r) - 2 * c.radius + @shape.origin.x,
                           position.y}
          @velocity.x *= -1
        end
      end
    end

    c = collision_circle
    if c.center.x - c.radius < 0
      @velocity.x = +@velocity.x.abs
    end
    if c.center.x + c.radius > GAME_WIDTH
      @velocity.x = -@velocity.x.abs
    end
  end

  private def update_y(delta_time : SF::Time, paddles : Array(Paddle))
    dy = @velocity.y * delta_time.as_seconds
    move 0, dy

    paddles.each do |p|
      c = collision_circle
      r = p.collision_rect

      if circle_rect_collide? c, r
        if c.center.y < top(r)
          self.position = {position.x, top(r) - 2*c.radius + @shape.origin.y}
          @velocity.y *= -1
        elsif c.center.y > bottom(r)
          self.position = {position.x, bottom(r) + @shape.origin.y}
          @velocity.y *= -1
        end
      end
    end

    c = collision_circle
    if c.center.y - c.radius < 0
      @velocity.y = +@velocity.y.abs
    end
    if c.center.y + c.radius > GAME_HEIGHT
      @velocity.y = -@velocity.y.abs
    end
  end

  def collision_circle : Circle
    x = position.x - @shape.origin.x + @shape.radius
    y = position.y - @shape.origin.y + @shape.radius
    Circle.new(SF.vector2f(x, y), @shape.radius)
  end
end

def process_events(window : SF::Window)
  while event = window.poll_event
    if event.is_a? SF::Event::Closed
      window.close
    end

    if event.is_a? SF::Event::KeyPressed
      if event.code == SF::Keyboard::Escape
        window.close
      end
    end
  end
end

def setup_paddles : Array(Paddle)
  paddles = [] of Paddle
  paddles << Paddle.new(
    SF.vector2f(40, GAME_HEIGHT / 2),
    SF.vector2f(20, 80),
    SF::Color::Red,
    PADDLE_SPEED,
    {"up" => SF::Keyboard::Up, "down" => SF::Keyboard::Down}
  )

  paddles << Paddle.new(
    SF.vector2f(GAME_WIDTH - 40, GAME_HEIGHT / 2),
    SF.vector2f(20, 80),
    SF::Color::Red,
    PADDLE_SPEED,
    {"up" => SF::Keyboard::W, "down" => SF::Keyboard::S}
  )

  paddles
end

def setupo_balls : Array(Ball)
  balls = [] of Ball
  n = 100

  n.times do |i|
    sx = rand(0.5 .. 3.0)
    sy = rand(0.5 .. 3.0)
    balls << Ball.new(
      SF.vector2f(GAME_WIDTH / n * (i + 1), GAME_HEIGHT / 2),
      15.0,
      SF::Color.new((255 / n * (i + 1)).to_i,
                    (255 - (255 / n * (i + 1))).to_i,
                    128),
      SF.vector2f(BALL_SPEED * sx, BALL_SPEED * sy)
    )
  end

  balls
end

######################################################################

window = SF::RenderWindow.new(
  SF::VideoMode.new(GAME_WIDTH, GAME_HEIGHT), "Pong take 5"
)

paddles = setup_paddles
balls = setupo_balls

clock = SF::Clock.new
elapsed_time = SF::Time::Zero

while window.open?
  # process events and players' inputs
  process_events window
  paddles.each { |p| p.process_input }

  # update
  paddles.each { |p| p.update elapsed_time }
  balls.each { |b| b.update elapsed_time, paddles}

  # render
  window.clear
  paddles.each { |p| window.draw p }
  balls.each { |b| window.draw b }
  window.display

  elapsed_time = clock.restart
end

src/shapeaux.cr

require "crsfml/graphics"

def left(r : SF::FloatRect)
  r.left
end

def right(r : SF::FloatRect)
  r.left + r.width
end

def top(r : SF::FloatRect)
  r.top
end

def bottom(r : SF::FloatRect)
  r.top + r.height
end

struct Circle
  property center : SF::Vector2f
  property radius : Float32

  def initialize(@center, @radius)
  end
end

def circle_rect_collide?(circle : Circle, rect : SF::FloatRect) : Bool
  c = circle.center

  t = c
  t.x = left(rect) if c.x < left(rect)
  t.x = right(rect) if c.x > right(rect)
  t.y = top(rect) if c.y < top(rect)
  t.y = bottom(rect) if c.y > bottom(rect)

  ct = t - c
  distance2 = ct.x ** 2 + ct.y ** 2
  radius2 = circle.radius ** 2

  distance2 < radius2
end

shard.yml

name: crsfml-pong-take-5
version: 0.1.0

authors:
  - watercat <watercat@example.com>

targets:
  crsfml-pong-take-5:
    main: src/crsfml-pong-take-5.cr

crystal: 1.7.3

license: WTFPL

dependencies:
  crsfml:
    github: oprypin/crsfml
    version: ~> 2.5.2