Refactor, entity / sprite

This commit is contained in:
Alex Clink 2022-01-02 19:39:17 -05:00
parent 3e7685c613
commit ce0f055bf5
18 changed files with 412 additions and 262 deletions

View file

@ -1,7 +1,6 @@
require "../src/game"
require "../src/controller"
require "../src/sprite"
require "../src/sprite/vector_sprite"
require "../src/pixel"
require "../src/point"
require "../src/pixel_text"

77
examples/balls.cr Normal file
View file

@ -0,0 +1,77 @@
require "../src/game"
require "../src/shape"
require "../src/entity"
require "../src/entity/circle_collision"
module PF
class Ball < Entity
include CircleCollision
getter frame : Array(Point(Float64))
def initialize(size : Float64)
@frame = Shape.circle(size.to_i32, size.to_i32)
@mass = size
@radius = size
end
end
class Balls < Game
@balls : Array(Ball) = [] of Ball
def initialize(*args, **kwargs)
super
15.times do
position = Point(Float64).new(rand(0.0_f64..@width.to_f64), rand(0.0_f64..@height.to_f64))
ball = Ball.new(rand(10.0..30.0))
ball.position = position
ball.velocity = Point(Float64).new(rand(-50.0..50.0), rand(-50.0..50.0))
@balls << ball
end
end
# override to wrap the coordinates
def draw_point(x : Int32, y : Int32, pixel : PF::Pixel, surface = @screen)
x = x % @width
y = y % @height
x = @width + x if x < 0
y = @height + y if y < 0
super(x, y, pixel, surface)
end
def update(dt, event)
@balls.each do |b|
b.update(dt)
b.position = b.position % viewport # wrap coords
end
collission_pairs = [] of Tuple(Ball, Ball)
@balls.each do |a|
@balls.each do |b|
next if a == b
next if collission_pairs.includes?({a, b})
if a.collides_with?(b)
collission_pairs << {a, b}
a.resolve_collision(b)
end
end
end
end
def draw
clear(10, 10, 30)
# @balls.each { |b| draw_circle(b.position.to_i32, b.radius.to_i32) }
@balls.each do |ball|
fill_shape(Shape.translate(ball.frame, translation: ball.position).map(&.to_i32))
end
end
end
end
balls = PF::Balls.new(600, 400, 2)
balls.run!

113
examples/snow.cr Normal file
View file

@ -0,0 +1,113 @@
require "crystaledge"
require "../src/game"
require "../src/controller"
require "../src/sprite"
require "../src/pixel"
require "../src/point"
require "../src/pixel_text"
class Wind
property width : Int32
property height : Int32
property density : Int32
property gusts : Array(Gust) = [] of Gust
@step : Float64?
struct Gust
property position : PF::Point(Float64)
property strength : PF::Point(Float64)
def initialize(@position, @strength)
end
end
def initialize(@width, @height, @density = 20)
setup_vectors
end
def step
@step ||= (@width / @density)
end
def setup_vectors
@gusts = [] of Gust
y = step / 2
while y < @height
x = step / 2
while x < @width
@gusts << Gust.new(PF::Point(Float64).new(x, y), PF::Point(Float64).new(rand(-1.0..1.0), rand(-1.0..1.0)))
x += step
end
y += step
end
end
end
class Flake
property shape : UInt8
property position : PF::Point(Float64)
property z_pos : Float64
property velocity : PF::Point(Float64)
def initialize(@position, @shape = rand(0_u8..2_u8), @z_pos = rand(0.0..1.0), velocity : PF::Point(Float64)? = nil)
@velocity = velocity || PF::Point(Float64).new(rand(-2.0..2.0), rand(0.0..20.0))
end
def update(dt)
@velocity.y = @velocity.y + 1.0 * dt
@position += @velocity * dt
end
end
class Snow < PF::Game
@wind : Wind
@pixels : Slice(UInt32)
@last_flake : Float64 = 0.0
@flakes : Array(Flake) = [] of Flake
def initialize(*args, **kwargs)
super
@wind = Wind.new(@width, @height)
@pixels = Slice.new(@screen.pixels.as(Pointer(UInt32)), @width * @height)
clear(0, 0, 15)
end
def update(dt, event)
@last_flake += dt
if @last_flake >= 0.025
@last_flake = 0.0
@flakes << Flake.new(position: PF::Point.new(rand(0.0..@width.to_f64), 0))
end
@flakes.reject! do |flake|
@wind.gusts.each do |gust|
size = @wind.step / 3
if flake.position > gust.position - size && flake.position < gust.position + size
flake.velocity = flake.velocity + gust.strength * 3 * dt
end
end
flake.update(dt)
flake.position.y > @height
end
end
def draw
clear(0, 0, 15)
@flakes.each do |flake|
color = PF::Pixel.white * flake.z_pos
if flake.shape == 0
draw_point(flake.position.to_i32, color)
else
draw_circle(flake.position.to_i32, flake.shape, color)
end
end
end
end
engine = Snow.new(1200, 800, 1)
engine.run!

View file

@ -0,0 +1,24 @@
require "../src/game"
require "../src/sprite"
module PF
class SpriteExample < Game
@bricks : Sprite
def initialize(*args, **kwargs)
super
@bricks = Sprite.new("./assets/pf-font.png")
end
def update(dt, event)
end
def draw
clear(255, 255, 255)
@bricks.draw(@screen, width // 2 - @bricks.width // 2, height // 2 - @bricks.height // 2)
end
end
end
game = PF::SpriteExample.new(200, 200, 2)
game.run!

View file

@ -15,7 +15,7 @@ class TextGame < PF::Game
@msg = "Hello, World!"
end
def update(dt)
def update(dt, event)
@x += @dx * dt
@y += @dy * dt

View file

@ -1,71 +1,60 @@
# require "crystaledge"
require "../src/game"
require "../src/controller"
require "../src/sprite"
require "../src/sprite/vector_sprite"
require "../src/entity"
require "../src/pixel"
require "../src/shape"
require "../src/point"
class Triangle < PF::Sprite
include PF::VectorSprite
class Triangle < PF::Entity
property frame : Array(PF::Point(Float64))
def initialize(*args, **kwargs)
@frame = [] of PF::Point(Float64)
end
def update(dt)
end
def draw(engine)
frame = project_points(@frame)
engine.fill_triangle(frame[0], frame[1], frame[2], PF::Pixel.yellow)
_frame = PF::Shape.rotate(@frame, @rotation)
_frame = PF::Shape.translate(_frame, @position)
engine.fill_triangle(_frame.map(&.to_i32), PF::Pixel.yellow)
end
end
class TriangleThing < PF::Game
@tri : Triangle
@paused = true
@paused = false
@controller : PF::Controller(LibSDL::Scancode)
def initialize(@width, @height, @scale)
super(@width, @height, @scale)
@tri = Triangle.build do |t|
t.position = Vector2.new(@width / 2, @height / 2)
t.frame = PF::VectorSprite.generate_circle(3, size = @width / 3)
end
@tri = Triangle.new
@tri.position = PF::Point.new(@width / 2, @height / 2)
@tri.frame = PF::Shape.circle(3, size = @width / 3)
@controller = PF::Controller(LibSDL::Scancode).new({
LibSDL::Scancode::RIGHT => "Rotate Right",
LibSDL::Scancode::LEFT => "Rotate Left",
LibSDL::Scancode::SPACE => "Pause",
LibSDL::Scancode::A => "Move Left",
LibSDL::Scancode::D => "Move Right",
LibSDL::Scancode::W => "Move Up",
LibSDL::Scancode::S => "Move Down",
})
end
def update(dt)
def update(dt, event)
case event
when SDL::Event::Keyboard
@controller.press(event.scancode) if event.keydown?
@controller.release(event.scancode) if event.keyup?
end
@paused = !@paused if @controller.pressed?("Pause")
@tri.rotation += 0.5 * dt if @controller.action?("Rotate Right")
@tri.rotation -= 0.5 * dt if @controller.action?("Rotate Left")
if @controller.action?("Move Up")
@tri.frame[1] = @tri.frame[1] + Vector2.new(0.0, -10.0) * dt
end
if @controller.action?("Move Down")
@tri.frame[1] = @tri.frame[1] + Vector2.new(0.0, 10.0) * dt
end
if @controller.action?("Move Left")
@tri.frame[1] = @tri.frame[1] + Vector2.new(-10.0, 0.0) * dt
end
if @controller.action?("Move Right")
@tri.frame[1] = @tri.frame[1] + Vector2.new(10.0, 0.0) * dt
end
@tri.rotation = @tri.rotation + 0.5 * dt if @controller.action?("Rotate Right")
@tri.rotation = @tri.rotation - 0.5 * dt if @controller.action?("Rotate Left")
unless @paused
@tri.rotation += 1.0 * dt
@tri.rotation = @tri.rotation + 1.0 * dt
end
@tri.update(dt)

26
src/entity.cr Normal file
View file

@ -0,0 +1,26 @@
require "./sprite"
module PF
# An entity is an object with a sprite and a physics body
class Entity
property sprite : Sprite? = nil
property position : Point(Float64) = Point.new(0.0, 0.0)
property velocity : Point(Float64) = Point.new(0.0, 0.0)
property rotation : Float64 = 0.0
property rotation_speed : Float64 = 0.0
property mass : Float64 = 1.0
def initialize(@sprite = nil)
end
def update(dt : Float64)
@rotation += @rotation_speed * dt
@position += @velocity * dt
end
def distance(other : Entity)
position.distance(other.position)
end
end
end

View file

@ -1,33 +1,34 @@
module PF
module CircleCollision
include CrystalEdge
require "../entity"
module PF
# This module contains methods to handle circle entity collision
module CircleCollision
property radius : Float64 = 1.0
# Check if two circles are colliding
def collides_with?(other : Sprite)
distance_between(other) < radius + other.radius
def collides_with?(other : Entity)
distance(other) < radius + other.radius
end
# Move objects so that they don't overlap
def offset_collision(other : Sprite)
distance = distance_between(other)
overlap = distance - radius - other.radius
offset = ((position - other.position) * (overlap / 2)) / distance
def offset_collision(other : Entity)
d = distance(other)
overlap = d - radius - other.radius
offset = ((position - other.position) * (overlap / 2)) / d
self.position -= offset
other.position += offset
self.position = position - offset
other.position = other.position + offset
end
# Resolve a collision by offsetting the two positions
# and transfering the momentum
def resolve_collision(other : VectorSprite)
def resolve_collision(other : Entity)
offset_collision(other)
distance = distance_between(other)
d = distance(other)
# Calculate the new velocities
normal_vec = (position - other.position) / distance
tangental_vec = Vector2.new(-normal_vec.y, normal_vec.x)
normal_vec = (position - other.position) / d
tangental_vec = Point(Float64).new(-normal_vec.y, normal_vec.x)
# Dot product of velocity with the tangent
# (the direction in which to bounce towards)

View file

@ -1,5 +1,5 @@
module PF
module SpriteAge
module EntityAge
property lifespan : Float64 = Float64::INFINITY
property age : Float64 = 0.0

View file

@ -11,8 +11,9 @@ module PF
FPS_INTERVAL = 1.0
SHOW_FPS = true
property width : Int32
property height : Int32
getter width : Int32
getter height : Int32
@viewport : Point(Int32)? = nil
property scale : Int32
property title : String
property running = true
@ -37,6 +38,22 @@ module PF
abstract def update(dt : Float64, event : SDL::Event)
abstract def draw
def width=(value : Int32)
@viewport = nil
@width = value
# TODO: Resize window
end
def height=(value : Int32)
@viewport = nil
@height = value
# TODO: Resize window
end
def viewport
@viewport ||= Point.new(@width, @height)
end
def elapsed_time
Time.monotonic.total_milliseconds
end
@ -61,16 +78,21 @@ module PF
end
end
# Draw a single point
# ditto
def draw_point(vector : Vector2, pixel : Pixel = Pixel.new, surface = @screen)
draw_point(vector.x.to_i32, vector.y.to_i32, pixel, surface)
end
# Draw a single point
def draw_point(point, pixel : Pixel = Pixel.new, surface = @screen)
# ditto
def draw_point(point : Point(Int), pixel : Pixel = Pixel.new, surface = @screen)
draw_point(point.x, point.y, pixel, surface)
end
# ditto
def draw_point(point : Point(Float64), pixel : Pixel = Pixel.new, surface = @screen)
draw_point(point.to_i32, pixel, surface)
end
# Draw a line using Bresenhams Algorithm
def draw_line(x1 : Int, y1 : Int, x2 : Int, y2 : Int, pixel : Pixel = Pixel.new, surface = @screen)
# The slope for each axis
@ -106,13 +128,14 @@ module PF
end
end
# Draw a line using Bresenhams Algorithm
def draw_line(p1 : Vector2, p2 : Vector2, pixel : Pixel = Pixel.new, surface = @screen)
draw_line(p1.x.to_i, p1.y.to_i, p2.x.to_i, p2.y.to_i, pixel, surface)
# ditto
def draw_line(p1 : Point(Int), p2 : Point(Int), pixel : Pixel = Pixel.new, surface = @screen)
draw_line(p1.x, p1.y, p2.x, p2.y, pixel, surface)
end
def draw_line(p1 : Point, p2 : Point, pixel : Pixel = Pixel.new, surface = @screen)
draw_line(p1.x, p1.y, p2.x, p2.y, pixel, surface)
# ditto
def draw_line(p1 : Point(Float), p2 : Point(Float), pixel : Pixel = Pixel.new, surface = @screen)
draw_line(p1.to_i32, p2.to_i32, pixel, surface)
end
# Draw the outline of a square rect

View file

@ -1,7 +1,7 @@
module PF
abstract class Game
# Fill an abitrary polygon. Expects a clockwise winding of points
def fill_shape(*points : Point, color : Pixel = Pixel.new, surface = @screen)
def fill_shape(points : Enumerable(Point), color : Pixel = Pixel.new, surface = @screen)
return if points.empty?
return draw_point(points[0], color, surface) if points.size == 1
return draw_line(points[0], points[1], color, surface) if points.size == 2
@ -61,6 +61,10 @@ module PF
end
end
def fill_shape(*points : Point, color : Pixel = Pixel.new, surface = @screen)
fill_shape(points, color, surface)
end
def draw_shape(*points : Point, color : Pixel = Pixel.new, surface = @screen)
0.upto(points.size - 1) do |n|
draw_line(points[n], points[(n + 1) % points.size], color, surface)

View file

@ -2,13 +2,6 @@ require "../line"
module PF
abstract class Game
def fill_triangle(p1 : Vector2, p2 : Vector2, p3 : Vector2, pixel : Pixel = Pixel.new, surface = @screen)
p1 = Point(Int32).new(x: p1.x.to_i, y: p1.y.to_i)
p2 = Point(Int32).new(x: p2.x.to_i, y: p2.y.to_i)
p3 = Point(Int32).new(x: p3.x.to_i, y: p3.y.to_i)
fill_triangle(p1, p2, p3, pixel, surface)
end
def fill_triangle(p1 : PF::Point, p2 : PF::Point, p3 : PF::Point, pixel : Pixel = Pixel.new, surface = @screen)
# Sort points from top to bottom
p1, p2 = p2, p1 if p2.y < p1.y
@ -61,76 +54,8 @@ module PF
end
end
# Fills a triangle shape by drawing two edges from the top vertex and scanning across left to right
def fill_triangle_bresenham(p1 : Point, p2 : Point, p3 : Point, pixel : Pixel = Pixel.new, surface = @screen)
# Sort points from top to bottom
p1, p2 = p2, p1 if p2.y < p1.y
p1, p3 = p3, p1 if p3.y < p1.y
p2, p3 = p3, p2 if p3.y < p2.y
s1 = p2 - p1
m1 = s1.y / s1.x
edge1 = calculate_edge(p1, p2)
edge2 = calculate_edge(p1, p3)
edge3 = calculate_edge(p2, p3)
if edge1.size > edge2.size
edge2.pop
edge2.concat edge3
else
edge1.pop
edge1.concat edge3
end
0.upto(edge1.size - 1) do |line|
if edge1[line].x < edge2[line].x
edge1[line].x.upto(edge2[line].x) { |x| draw_point(x, edge1[line].y, pixel, surface) }
else
edge2[line].x.upto(edge1[line].x) { |x| draw_point(x, edge1[line].y, pixel, surface) }
end
end
end
# Calculate an edge using Bresenhams Algorithm
def calculate_edge(p1 : Point, p2 : Point)
# The slope for each axis
slope = Point.new((p2.x - p1.x).abs, -(p2.y - p1.y).abs)
# The step direction in both axis
step = Point.new(p1.x < p2.x ? 1 : -1, p1.y < p2.y ? 1 : -1)
# The final decision accumulation
# Initialized to the height of x and y
decision = slope.x + slope.y
edge = [] of Point(Int32)
point = p1
edge << point
loop do
# draw_point(point.x, point.y, Pixel.yellow)
# Break if we've reached the ending point
break if point == p2
# Square the decision to avoid floating point calculations
decision_squared = decision + decision
# if decision_squared is greater than
if decision_squared >= slope.y
decision += slope.y
point.x += step.x
end
if decision_squared <= slope.x
decision += slope.x
point.y += step.y
edge << point
end
end
edge
def fill_triangle(points : Enumerable(PF::Point), pixel : Pixel = Pixel.new, surface = @screen)
fill_triangle(points[0], points[1], points[2], pixel, surface)
end
end
end

View file

@ -1,28 +1,24 @@
require "sdl/image"
require "./sprite"
module PF
class PixelText
class PixelText < Sprite
getter width : Int32
getter height : Int32
@img : SDL::Surface
@chars : String
def initialize(path : String, @width : Int32 = 7, @height : Int32 = 8, mapping : String? = nil)
@img = SDL::IMG.load(path)
super(path)
@chars = mapping || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?().,/\\[]{}$#+-“”‘’'\"@"
end
def convert(surface : SDL::Surface)
@img = @img.convert(surface)
end
def color(pixel : Pixel)
color_val = pixel.format(@img.format)
alpha_mask = @img.format.a_mask
color_val = pixel.format(@surface.format)
alpha_mask = @surface.format.a_mask
0.upto(@img.height - 1) do |y|
0.upto(@img.width - 1) do |x|
loc = pixel_pointer(x, y, @img)
0.upto(@surface.height - 1) do |y|
0.upto(@surface.width - 1) do |x|
loc = pixel_pointer(x, y)
if loc.value & alpha_mask != 0
loc.value = color_val
@ -47,17 +43,12 @@ module PF
char_x *= @width
unless char == ' '
@img.blit(surface, SDL::Rect.new(char_x - 1, char_y, @width, @height), SDL::Rect.new(x + ix * @width, y + iy * @height, @width, @height))
@surface.blit(surface, SDL::Rect.new(char_x - 1, char_y, @width, @height), SDL::Rect.new(x + ix * @width, y + iy * @height, @width, @height))
end
end
ix += 1
end
end
private def pixel_pointer(x : Int32, y : Int32, surface = @img)
target = surface.pixels + (y * surface.pitch) + (x * 4)
target.as(Pointer(UInt32))
end
end
end

View file

@ -49,6 +49,14 @@ module PF
@x < other.x && @y < other.y
end
def %(other : Point)
Point.new(x % other.x, y % other.y)
end
def %(n : Float | Int)
Point.new(x % n, y % n)
end
def abs
Point.new(x.abs, y.abs)
end

44
src/shape.cr Normal file
View file

@ -0,0 +1,44 @@
module PF
module Shape
# Generate an array of points that form a circle
def self.circle(num_points : Int, size = 1.0, jitter = 0.0)
0.upto(num_points).map do |n|
angle = (2 * Math::PI) * (n / num_points)
x = size + rand(-jitter..jitter)
rc = Math.cos(angle)
rs = Math.sin(angle)
Point.new(0.0 * rc - x * rs, x * rc + 0.0 * rs)
end.to_a
end
# Rotate points by *rotation*
def self.rotate(points : Enumerable(Point), rotation : Float64)
rc = Math.cos(rotation)
rs = Math.sin(rotation)
points.map do |point|
Point.new(point.x * rc - point.y * rs, point.y * rc + point.x * rs)
end
end
# Translate points by *translation*
def self.translate(points : Enumerable(Point), translation : Point)
points.map { |p| p + translation }
end
# ditto
def self.translate(*points : Point, translation : Point)
self.translation(points, translation: translation)
end
# Scale points by a certain *amount*
def self.scale(points : Enumerable(Point), amount : Point)
points.map { |p| p * amount }
end
# calculate length from center for all points, and then get the average
def self.average_radius(points : Enumerable(Point))
points.map(&.length).reduce { |t, p| t + p } / frame.size
end
end
end

View file

@ -1,40 +1,39 @@
require "crystaledge"
require "sdl/image"
module PF
abstract class Sprite
include CrystalEdge
class Sprite
@surface : SDL::Surface
def self.build
sprite = new
yield sprite
sprite
delegate :convert, to: @img
def initialize(@surface)
end
property position : Vector2
property velocity : Vector2
property scale : Vector2
property rotation : Float64
property rotation_speed : Float64
property mass : Float64 = 10.0
def initialize
@position = Vector2.new(0.0, 0.0)
@velocity = Vector2.new(0.0, 0.0)
@scale = Vector2.new(1.0, 1.0)
@rotation = 0.0
@rotation_speed = 0.0
def initialize(path : String)
@surface = SDL::IMG.load(path)
end
def update_position(dt : Float64)
@rotation += @rotation_speed * dt
@position += @velocity * dt
def width
@surface.width
end
def distance_between(other)
self.position.distance(other.position)
def height
@surface.height
end
abstract def update(dt : Float64)
abstract def draw(engine : Game)
def draw(surface : SDL::Surface, x : Int32, y : Int32)
@surface.blit(surface, nil, SDL::Rect.new(x, y, width, height))
end
# Raw access to the pixels as a Slice
def pixels
Slice.new(@screen.pixels.as(Pointer(UInt32)), width * height)
end
# Get the pointer to a pixel
private def pixel_pointer(x : Int32, y : Int32)
target = @surface.pixels + (y * @surface.pitch) + (x * 4)
target.as(Pointer(UInt32))
end
end
end

View file

@ -1,73 +0,0 @@
module PF
module VectorSprite
include CrystalEdge
def self.generate_circle(num_points : Int, size = 1.0, jitter = 0.0) : Array(Vector2)
0.upto(num_points).map do |n|
angle = (2 * Math::PI) * (n / num_points)
x = size + rand(-jitter..jitter)
rc = Math.cos(angle)
rs = Math.sin(angle)
Vector2.new(0.0 * rc - x * rs, x * rc + 0.0 * rs)
end.to_a
end
property frame = [] of Vector2
@average_radius : Float64? = nil
def project_points(points : Array(Vector2), rotation = self.rotation, translate : Vector2? = nil, scale : Vector2? = nil)
rc = Math.cos(rotation)
rs = Math.sin(rotation)
translation =
if t = translate
self.position + t
else
self.position
end
points.map do |point|
rotated = Vector2.new(point.x * rc - point.y * rs, point.y * rc + point.x * rs)
scale.try do |scale|
rotated = rotated * scale
end
translation + rotated
end
end
# Calculated as the average R for all points in the frame
def radius
average_radius
end
# Calculated as the average R for all points in the frame
def average_radius
@average_radius ||= begin
# calculate length from center for all points
lengths = frame.map do |vec|
Math.sqrt(vec.x ** 2 + vec.y ** 2)
end
# get the average of the lengths
lengths.reduce { |t, p| t + p } / frame.size.to_f
end
end
def draw_frame(engine : Game, frame = @frame, color : Pixel = Pixel.new)
0.upto(frame.size - 1) do |n|
engine.draw_line(frame[n], frame[(n + 1) % frame.size], color)
end
end
def draw_radius(engine : Game, points = 30, color : Pixel = Pixel.new)
circle = self.class.generate_circle(points, average_radius).map do |point|
point + @position
end
draw_frame(engine, frame: circle, color: color)
end
end
end

View file

@ -1,3 +1,3 @@
module PF
VERSION = "0.0.2"
VERSION = "0.0.3"
end