From 7d3cff5e5d6d53d7f4def94a9dc831e099e03e67 Mon Sep 17 00:00:00 2001 From: Tom Black Date: Tue, 30 Jan 2018 00:23:34 -0800 Subject: Sprite class redesign See #33 for discussion --- ext/ruby2d/ruby2d.c | 14 ++++- lib/ruby2d/sprite.rb | 171 ++++++++++++++++++++++++++++++++------------------- test/media | 2 +- test/sprite.rb | 99 +++++++++++++++++++++++++++++ test/sprite_spec.rb | 6 +- 5 files changed, 223 insertions(+), 69 deletions(-) create mode 100644 test/sprite.rb diff --git a/ext/ruby2d/ruby2d.c b/ext/ruby2d/ruby2d.c index 0b9e338..2daa7ed 100644 --- a/ext/ruby2d/ruby2d.c +++ b/ext/ruby2d/ruby2d.c @@ -379,6 +379,14 @@ static R_VAL ruby2d_sprite_ext_init(R_VAL self, R_VAL path) { #endif S2D_Log(S2D_INFO, "Init sprite: %s", RSTRING_PTR(path)); S2D_Sprite *spr = S2D_CreateSprite(RSTRING_PTR(path)); + + // Get width and height from Ruby class. If set, use it, else choose the + // native dimensions of the sprite image. + R_VAL w = r_iv_get(self, "@width"); + R_VAL h = r_iv_get(self, "@height"); + r_iv_set(self, "@width" , r_test(w) ? w : INT2NUM(spr->width)); + r_iv_set(self, "@height", r_test(h) ? h : INT2NUM(spr->height)); + r_iv_set(self, "@data", r_data_wrap_struct(sprite, spr)); return R_NIL; } @@ -392,6 +400,8 @@ static R_VAL ruby2d_sprite_ext_render(mrb_state* mrb, R_VAL self) { #else static R_VAL ruby2d_sprite_ext_render(R_VAL self) { #endif + r_funcall(self, "update", 0); + S2D_Sprite *spr; r_data_get_struct(self, "@data", &sprite_data_type, S2D_Sprite, spr); @@ -402,8 +412,8 @@ static R_VAL ruby2d_sprite_ext_render(R_VAL self) { spr, NUM2INT(r_iv_get(self, "@clip_x")), NUM2INT(r_iv_get(self, "@clip_y")), - NUM2INT(r_iv_get(self, "@clip_w")), - NUM2INT(r_iv_get(self, "@clip_h")) + NUM2INT(r_iv_get(self, "@clip_width")), + NUM2INT(r_iv_get(self, "@clip_height")) ); S2D_DrawSprite(spr); diff --git a/lib/ruby2d/sprite.rb b/lib/ruby2d/sprite.rb index 7c66f97..a9d9e98 100644 --- a/lib/ruby2d/sprite.rb +++ b/lib/ruby2d/sprite.rb @@ -2,11 +2,12 @@ module Ruby2D class Sprite + include Renderable - attr_accessor :x, :y, :clip_x, :clip_y, :clip_w, :clip_h, :data - attr_reader :z + attr_accessor :x, :y, :width, :height, :loop, + :clip_x, :clip_y, :clip_width, :clip_height, :data - def initialize(x, y, path, z=0) + def initialize(path, opts = {}) unless RUBY_ENGINE == 'opal' unless File.exists? path @@ -14,88 +15,130 @@ module Ruby2D end end - @x, @y, @path = x, y, path - @clip_x, @clip_y, @clip_w, @clip_h = 0, 0, 0, 0 - @default = nil - @animations = {} - @current_animation = nil - @current_frame = 0 - @current_frame_time = 0 - @z = z - - ext_init(path) - if Module.const_defined? :DSL - Application.add(self) - end + @path = path + @x = opts[:x] || 0 + @y = opts[:y] || 0 + @z = opts[:z] || 0 + @width = opts[:width] || nil + @height = opts[:height] || nil + @start_time = 0.0 + @loop = opts[:loop] || false + @frame_time = opts[:time] || 300 + @animations = opts[:animations] || {} + @playing = false + @current_frame = opts[:default] || 0 + @last_frame = 0 + + ext_init(@path) + @clip_x = opts[:clip_x] || 0 + @clip_y = opts[:clip_y] || 0 + @clip_width = opts[:clip_width] || @width + @clip_height = opts[:clip_height] || @height + @animations[:default] = 0..(@width / @clip_width) - 1 # set default animation + + @defaults = { + animation: @animations.first[0], + frame: @current_frame, + frame_time: @frame_time, + clip_x: @clip_x, + clip_y: @clip_y, + clip_width: @clip_width, + clip_height: @clip_height, + loop: @loop + } + + add end - def start(x, y, w, h) - @default = [x, y, w, h] - clip(x, y, w, h) - end + def play(animation = nil, loop = nil) + if !@playing || (animation != @playing_animation && animation != nil) + + @playing = true + @playing_animation = animation || :default + frames = @animations[@playing_animation] + + case frames + # When animation is a range, play through frames horizontally + when Range + @first_frame = frames.first || @defaults[:frame] + @current_frame = frames.first || @defaults[:frame] + @last_frame = frames.last + # When array... + when Array + @first_frame = 0 + @current_frame = 0 + @last_frame = frames.length - 1 + end - def add(animations) - @animations.merge!(animations) - end + # Set looping + @loop = loop == :loop || @defaults[:loop] ? true : false - def animate(animation) - if @current_animation != animation - @current_frame = 0 - @current_frame_time = 0 - @current_animation = animation + set_frame + restart_time end - animate_frames(@animations[animation]) end - def reset - clip(@default[0], @default[1], @default[2], @default[3]) - @current_animation = nil + # Stop the current animation and set to the default frame + def stop + @playing = false + @playing_animation = @defaults[:animation] + @current_frame = @defaults[:frame] + set_frame end - # TODO: Sprite already has an `add` method, have to reconsile - # def add - # if Module.const_defined? :DSL - # Application.add(self) - # end - # end + # Reset frame to defaults + def reset_clipping_rect + @clip_x = @defaults[:clip_x] + @clip_y = @defaults[:clip_y] + @clip_width = @defaults[:clip_width] + @clip_height = @defaults[:clip_height] + end - def remove - if Module.const_defined? :DSL - Application.remove(self) + # Set the position of the clipping retangle based on the current frame + def set_frame + frames = @animations[@playing_animation] + case frames + when Range + reset_clipping_rect + @clip_x = @current_frame * @clip_width + when Array + f = frames[@current_frame] + @clip_x = f[:x] || @defaults[:clip_x] + @clip_y = f[:y] || @defaults[:clip_y] + @clip_width = f[:width] || @defaults[:clip_width] + @clip_height = f[:height] || @defaults[:clip_height] + @frame_time = f[:time] || @defaults[:frame_time] end end - def width - @current_animation ? @animations[@current_animation][@current_frame][2] : @default[2] + # Calculate the time in ms + def elapsed_time + (Time.now.to_f - @start_time) * 1000 end - def height - @current_animation ? @animations[@current_animation][@current_frame][3] : @default[3] + # Restart the timer + def restart_time + @start_time = Time.now.to_f end - private + # Update the sprite animation, called by `Sprite#ext_render` + def update + if @playing - def clip(x, y, w, h) - @clip_x, @clip_y, @clip_w, @clip_h = x, y, w, h - end + # Advance the frame + unless elapsed_time <= (@frame_time || @defaults[:frame_time]) + @current_frame += 1 + restart_time + end - def animate_frames(frames) - if @current_frame_time < frames[@current_frame][4] - clip_with_current_frame(frames) - @current_frame_time += 1 - else - @current_frame += 1 - if @current_frame == frames.length - @current_frame = 0 + # Reset to the starting frame if all frames played + if @current_frame > @last_frame + @current_frame = @first_frame + unless @loop then stop end end - clip_with_current_frame(frames) - @current_frame_time = 0 - end - end - def clip_with_current_frame(frames) - clip(frames[@current_frame][0], frames[@current_frame][1], - frames[@current_frame][2], frames[@current_frame][3]) + set_frame + end end end diff --git a/test/media b/test/media index 0d54e76..44ae8c4 160000 --- a/test/media +++ b/test/media @@ -1 +1 @@ -Subproject commit 0d54e768f8da2217203649cb270f4d0add82328f +Subproject commit 44ae8c4a971e3ba6520ad9953fb4dd12eb36d2c7 diff --git a/test/sprite.rb b/test/sprite.rb new file mode 100644 index 0000000..713375a --- /dev/null +++ b/test/sprite.rb @@ -0,0 +1,99 @@ +require 'ruby2d' + +if RUBY_ENGINE == 'opal' + media = "../test/media" +else + media = "media" +end + +set title: "Ruby 2D — Sprite", width: 350, height: 150 + + +coin = Sprite.new( + "#{media}/coin.png", + clip_width: 84, + time: 300, + loop: true +) + +coin.play + +boom = Sprite.new( + "#{media}/boom.png", + x: 109, + clip_width: 127, + time: 75 +) + +hero = Sprite.new( + "#{media}/hero.png", + x: 261, + clip_width: 78, + time: 250, + animations: { + walk: 1..2, + climb: 3..4, + cheer: 5..6 + } +) + +atlas = Sprite.new( + "#{media}/texture_atlas.png", + x: 10, y: 100, + animations: { + count: [ + { + x: 0, y: 0, + width: 35, height: 41, + time: 300 + }, + { + x: 26, y: 46, + width: 35, height: 38, + time: 400 + }, + { + x: 65, y: 10, + width: 32, height: 41, + time: 500 + }, + { + x: 10, y: 99, + width: 32, height: 38, + time: 600 + }, + { + x: 74, y: 80, + width: 32, height: 38, + time: 700 + } + ] + } +) + +atlas.play :count, :loop + + +on :key_down do |e| + close if e.key == 'escape' + + case e.key + when 'p' + coin.play + boom.play + atlas.play :count + when 's' + coin.stop + hero.stop + atlas.stop + when 'right' + hero.play :walk, :loop + when 'up' + hero.play :climb, :loop + when 'down' + hero.play :cheer + end +end + + +show diff --git a/test/sprite_spec.rb b/test/sprite_spec.rb index e44ea62..a9cbf31 100644 --- a/test/sprite_spec.rb +++ b/test/sprite_spec.rb @@ -3,13 +3,15 @@ require 'ruby2d' RSpec.describe Ruby2D::Sprite do describe '#new' do + it "raises exception if file doesn't exist" do - expect { Sprite.new(0, 0, "bad_sprite_sheet.png") }.to raise_error(Ruby2D::Error) + expect { Sprite.new("bad_sprite_sheet.png") }.to raise_error(Ruby2D::Error) end it 'creates a new sprite' do - Sprite.new(0, 0, "test/media/sprite_sheet.png") + Sprite.new("test/media/coin.png") end + end end -- cgit v1.2.3