横に動かそう †入力を処理する準備が整ったのでコントロールブロックを横に動かそう. 冷静に考察 †ここで,横に移動するために冷静な考察を行う. コントロールブロックはキー入力によって任意のタイミングで横に移動することができる. このとき,移動先にすでにブロックが存在するときは移動できない. が,少し重なる程度ならばすでに存在するブロックの上に乗る形で横に移動できる. ここで問題になるのが,現在のブロックの落下処理は1マスごとにmove_yをセットしているので,座標単位での処理が扱いづらい. そこで今回はいっそのことすでに表示されているブロックとコントロールブロックの処理を分けてしまう. 今のブロックのクラスであるBlockには共通の機能のみを残し,すでに配置済みのブロックをStableBlock?,コントロールブロックをFreeBlock?とし,それぞれスーパークラスはBlockとする. class Block attr_accessor :color, :row, :line attr_reader :draw_pos def initialize(col, block_s) @color = col @row = -1 @line = -1 @block_s = block_s @draw_pos = [0,0] end def inspect sprintf("|(%2d,%2d):%s|",@row,@line,@color.to_s) end def to_s sprintf("|(%2d,%2d):%s|",@row,@line,@color.to_s) end def get_color(alpha = 255) case @color when :r StarRuby::Color.new(255,128,128,alpha) when :g StarRuby::Color.new(128,255,128,alpha) when :b StarRuby::Color.new(128,128,255,alpha) when :y StarRuby::Color.new(255,255,128,alpha) when :p StarRuby::Color.new(255,128,255,alpha) end end def update @draw_pos[0] = @row * @block_s @draw_pos[1] = @line * @block_s end def draw(ox,oy,alpha=255) x = ox + @draw_pos[0] y = oy - @draw_pos[1] screen = GameMain.screen screen.render_rect(x, y, @block_s, @block_s, get_color(alpha)) end end class StableBlock < Block def initialize(col, block_s) super init_animation end def init_animation @move_x = nil @move_y = nil @collapse = nil @draw_pos = [0,0] end def set_move_x(from, to, speed) @move_x = { :from => from, :to => to, :speed => speed, :counter => 0 } end def set_move_y(from, to, speed) @move_y = { :from => from, :to => to, :speed => speed, :counter => 0 } end def set_collapse(time) @collapse = { :time => time, :counter => time } end def update_move(param) return nil unless param pos = param[:from] + param[:counter] + param[:speed] param[:counter] += param[:speed] if param[:speed] > 0 (pos = param[:to]; yield) if pos > param[:to] else (pos = param[:to]; yield) if pos < param[:to] end return pos end def update_collapse return unless @collapse if @collapse[:counter] == 0 @collapse = nil else @collapse[:counter] -= 1 end end def update # update move_x move_y x = update_move(@move_x){@move_x = nil} y = update_move(@move_y){@move_y = nil} @draw_pos[0] = x ? x : @row * @block_s @draw_pos[1] = y ? y : @line * @block_s # update collapse update_collapse end def move_x?; !@move_x.nil?; end def move_y?; !@move_y.nil?; end def move?; move_x? || move_y?; end def collapse?; !@collapse.nil?; end def animation?; move? || collapse?; end def reasonable_collapse? return false unless @collapse @collapse[:counter] >= @collapse[:time] / 5 end def draw(ox,oy) if @collapse alpha = @collapse[:counter] * 255 / @collapse[:time] else alpha = 255 end super(ox, oy, alpha) end end # row : base on @row [ draw_pos < row ] # line : base on @draw_pos[1] [ draw_pos > line ] class FreeBlock < Block def initialize(col, block_s) super init_animation end def init_animation @move_x = nil end def line=(line) @draw_pos[1] = line * @block_s @line = line end def set_move_x(from, to, speed) @move_x = { :from => from, :to => to, :speed => speed, :counter => 0 } end def update_move(param) return nil unless param pos = param[:from] + param[:counter] + param[:speed] param[:counter] += param[:speed] if param[:speed] > 0 (pos = param[:to]; yield) if pos > param[:to] else (pos = param[:to]; yield) if pos < param[:to] end return pos end def update # update move_x x = update_move(@move_x){@move_x = nil} @draw_pos[0] = x ? x : @row * @block_s # update line @line = (@draw_pos[1].truncate + @block_s / 2) / @block_s end def move_x?; !@move_x.nil?; end def move?; move_x?; end def animation?; move?; end def convert_stable sb = StableBlock.new(@color, @block_s) sb.row = @row sb.line = @line sb end end ステーブルブロックとフリーブロックの主な違いはy座標の扱いである. ステーブルブロックはlineの値を主としてy座標を決定している. 一方フリーブロックではy座標を主としてlineの値を決定している. また,着地したときにフリーブロックをステーブルブロックに変換するconvert_stableメソッドを持つ. ついでにブロック自身もブロックサイズを保持するようにした. フィールドと落下処理 †Fieldクラスではステーブルブロックとフリーブロックの生成,落下処理に変更を行った. def set(r,l,col) if @table[r][l] @active_blocks.delete @table[r][l] end block = StableBlock.new(col, @block_s) block.row = r block.line = l @table[r][l] = block @active_blocks.push block end def start_control_block(colors) pivot = FreeBlock.new(colors.sample, @block_s) belong = FreeBlock.new(colors.sample, @block_s) @ctrl_block.set(pivot, belong, 80) @ctrl_block.start end def update_control_block_move_y(iff) fall_y = @ctrl_block.can_falldown?(@table, @block_s) Debug.print fall_y if fall_y > 0 # fall @ctrl_block.falldown(fall_y > 0.8 ? 0.8 : fall_y) # fall_y > speed ? speed : fall_y elsif fall_y < 0 # dent @ctrl_block.fix_dent(-fall_y) else # postpone or land if @ctrl_block.postpone? @ctrl_block.update_postpone else control_block_land return false end end return true end end update_control_block_move_yメソッドではコントロールブロックのメソッドcan_fall?により落下可能かどうか,落下可能ならばどれだけ落下できるのか,もしくはめり込んでいるときはどれだけ上に上げればめり込みが解消されるのかを返す. can_fall?メソッドを詳しく見てみよう. class ControlBlock def can_falldown?(table, block_s) fall_ys = blocks.group_by{|block| block.row}.map{|row, blks| min_y = blks.min_by{|blk| blk.draw_pos[1]}.draw_pos[1] min_y - table[row].size * block_s } # fall_ys == 0 : can not fall # > 0 : fall y # < 0 : dent y return fall_ys.min end def falldown(y) blocks.each do |block| block.draw_pos[1] -= y end end end まず列ごとにコントロールブロックを分類し,各グループについて最小のy座標を求める.とはテーブルと見比べてちょうど着地状態なのか,どれくらい落下できるのか,もしくはめり込んでいるのか(マイナスの値)を求めて,各列の結果のうちもっとも小さい値を返す. あとはフィールド側の処理で,1フレームに移動できる距離に制限した上でブロックを落下させる. 横に動かす †まずは入力があったときにブロックを横に動かすことが可能かどうかを判断するメソッドcan_move_row?を定義する. class ControlBlock def can_move_row?(imr, table, row_s) return false if imr == 0 # no move blocks.group_by{|block| block.line}.each do |line, blks| if imr > 0 # move right row = blks.max_by{|blk| blk.row}.row return false if row >= row_s - 1 return false if table[row + 1][line] else # move left row = blks.min_by{|blk| blk.row}.row return false if row <= 0 return false if table[row - 1][line] end end return true end def move_row(imr, speed, block_s) blocks.each do |block| x1 = block.row * block_s x2 = x1 + imr * block_s block.set_move_x(x1, x2, imr * speed) block.row += imr end end end 引数imrは右移動が入力されれば1が,左なら-1,それいがいなら0が入っている. さて,横移動なので今度は行についてブロックをグループ分けする. そしてそれぞれのグループについて移動方向によって最も小さいもしくは大きい行をもつブロックの行の値を取得し,それがフィールドの端か隣のマスが埋まっていれば移動できないので即座にfalseを返す. 実際に移動するmove_rowメソッドは単純にアニメーションをセットしつつ移動するだけである. ここで,ポイントとなる部分を見てみよう. class FreeBlock < Block def update # update move_x x = update_move(@move_x){@move_x = nil} @draw_pos[0] = x ? x : @row * @block_s # update line @line = (@draw_pos[1].truncate + @block_s / 2) / @block_s end end フリーブロックの更新処理中に, @draw_pos[1]に基づいて@lineを更新している部分がある. これによればフリーブロックの行は (描画位置+ブロックサイズの半分)/ブロックサイズ である. つまり,ブロックサイズの半分まで描画位置が下がっても,属する行は変わらないということである. これによりcan_move_row?はブロックの位置が少し下がってもtrueを返す. この状態で移動すればブロックは少しめり込む事になるが,落下処理によってめり込みはコントロールブロック側が上にずれることで修正されるため, 最初に想定した仕様通りの動きをすることになる. 最後にフィールド側の横移動の処理をのせておく. class Field def update_control_block_move_x(imr) return true if @ctrl_block.move_x? return false unless @ctrl_block.can_move_row?(imr,@table,@row_s) @ctrl_block.move_row(imr, 4, @block_s) return true end end 実行 †Loading the player ...
回転はできないがとりあえず横に動かせる.また,少し下に下がっていても上にずれて横に動かせていることがわかる. ちなみに上に表示されている数字は,ブロックが落下可能な距離である. すなわちcan_fall?メソッドの戻り値である. |