回転させよう(Pivot編) †いよいよ操作ブロック最後の山,回転を作ろう. 考察 †まずこれまで,操作ブロックは回転の際,軸となるブロックが一つあり,それを中心として回転する,としてきた. これは正しい.そしてそれを元にしてコントロールブロックは作られている. しかし,ぷよぷよフィーバーを考えるとそうも行かない. ぷよぷよフィーバーでは通常の2個ブロックの他に3個のブロックや4個のブロック,さらには色を変えられる4個のブロックも存在する. このうち前述の仕様が通用するのは2個,3個ブロックのみである. そのため,例によってControlBlock?クラスはコントロールブロックの共通の機能のみをもった親クラスとし,サブクラスにピボットを持つPivotControlBlock?,四つ組みのブロックで,それらが回転するCycleControlBlock?を定義した. class ControlBlock
def initialize
clear
end
def clear
@postpone = 0
end
def set(postpone)
@postpone = postpone
end
def start(row, line)
end
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
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
def fix_dent(y)
blocks.each do |block|
block.draw_pos[1] += y
end
end
def update_postpone
@postpone -= 1
end
def blocks
return []
end
def move_x?
blocks.each do |block|
return true if block.move_x?
end
return false
end
def move_y?
blocks.each do |block|
return true if block.move_y?
end
return false
end
def move?
blocks.each do |block|
return true if block.move?
end
return false
end
def postpone?; @postpone > 0; end
end
=begin
blocks = [b0,b1,b2,b3]
b1 b2
(b0) b3
(block) is base block
=end
class CycleControlBlock < ControlBlock
def clear
super
@blocks = []
end
def set(*blocks, postpone)
super(postpone)
@blocks = Array.new(4){|i| blocks[i] }
end
def start(row, line)
@blocks.each.with_index do |block, i|
next unless block
case i
when 0; row_shift = 0; line_shift = 0
when 1; row_shift = 0; line_shift = 1
when 2; row_shift = 1; line_shift = 1
when 3; row_shift = 1; line_shift = 0
end
block.row = row + row_shift
block.line = line + line_shift
end
end
def blocks
return @blocks.compact
end
end
=begin
pivot = p
belongs = [b0, b1, b2, b3]
b0
b3 p b1
b2
=end
class PivotControlBlock < ControlBlock
def clear
super
@pivot = nil
@belongs = []
end
def set(pivot, *belongs, postpone)
super(postpone)
@pivot = pivot
@belongs = Array.new(4){|i| belongs[i] }
end
def start(row, line)
if @pivot
@pivot.row = row
@pivot.line = line
end
@belongs.each.with_index do |block, i|
next unless block
case i
when 0; row_shift = 0 ; line_shift = -1
when 1; row_shift = 1 ; line_shift = 0
when 2; row_shift = 0 ; line_shift = 1
when 3; row_shift = -1; line_shift = 0
end
block.row = row + row_shift
block.line = line + line_shift
end
end
def blocks
return [@pivot, *@belongs].compact
end
end
サイクルブロックは四つ組みのブロックで,フィーバーの四個ブロックや,色変えブロックが表現できそうである. ピボットブロックはピボットを中心として最大4個その周囲にブロックを持たせることができるので,二個,三個ブロックが表現できる. さて,このように違うタイプのコントロールクラスが存在しうるようになったので, これらの切り替えなどをまとめて管理してくれる人を作ろう. class ControlBlockManager
def initialize
init_ctrl_blocks
end
def init_ctrl_blocks
@ctrl_blocks = {
PivotControlBlock => PivotControlBlock.new,
CycleControlBlock => CycleControlBlock.new
}
@type = PivotControlBlock
end
def set_type(type)
ctrl_block.clear unless @type.nil?
@type = type
end
def ctrl_block
@ctrl_blocks[@type]
end
end
ControlBlockManager?クラスはその名の通りコントロールブロックを管理する. 作ったコントロールブロックの種類だけ@ctrl_blocksに追加してset_typeメソッドで種類を切り替えられる.現在のコントロールブロックはctrl_blockメソッドで取得できる. フィールドの処理 †フィールド側の回転処理はコントロールブロックの種類によらず同じなのでこちらを先に考えてしまおう. class Field def update_control_block(imr,ir,iff) update_control_block_rotate(ir) update_control_block_move_x(imr) active = update_control_block_move_y(iff) end def update_control_block_rotate(ir) return true if @cbm.ctrl_block.rotate? rotate = @cbm.ctrl_block.can_rotate?(ir, @table, @row_s) return false unless rotate @cbm.ctrl_block.rotate(rotate, 8) end end 流れは落下や横移動と全く同じ.回転可能かチェックするcan_rotate?メソッド,実際に回転するrotateメソッドを呼んでいる.imrは右回転の入力なら1,左なら-1,それ以外は0が入っている. 回転アニメーション †回転のアニメーションもブロックの種類によらず同様である*1ので, 先に定義しよう. class FreeBlock < Block
def init_animation
@move_x = nil
@rotate_x = nil
@rotate_y = nil
end
def set_rotate_x(from, to, time, push_shift)
# x : rotate distance list based on to
from_rad = from * Math::PI / 180
to_rad = to * Math::PI / 180
time_divide = (to_rad - from_rad) / time
shift = Math.cos(to_rad) * @block_s
ps_divide = (push_shift * @block_s).quo time
rotate = []
rad = from_rad
ps = push_shift * @block_s
time.times do
rad += time_divide
ps -= ps_divide
rotate.push(Math.cos(rad) * @block_s - shift - ps)
end
return rotate
end
def set_rotate_y(from, to, time)
# y : rotate speed list base on from
from_rad = from * Math::PI / 180
to_rad = to * Math::PI / 180
time_divide = (to_rad - from_rad) / time
rotate = []
rad = from_rad
time.times do
old = rad
rad += time_divide
rotate.push((Math.sin(rad) - Math.sin(old)) * @block_s)
end
return rotate
end
def set_rotate(from, to, time, shift)
@rotate_x = set_rotate_x(from, to, time, shift)
@rotate_y = set_rotate_y(from, to, time)
end
def update_rotate(rotate)
return nil unless rotate
x_shift = rotate.shift
yield if rotate.empty?
return x_shift
end
def update
# update move_x
x = update_move(@move_x){@move_x = nil}
@draw_pos[0] = x ? x : @row * @block_s
# update rotate
x_shift = update_rotate(@rotate_x){@rotate_x = nil}
y_shift = update_rotate(@rotate_y){@rotate_y = nil}
@draw_pos[0] += x_shift if x_shift
@draw_pos[1] += y_shift if y_shift
# update line
@line = (@draw_pos[1].round + @block_s / 2) / @block_s
end
end
回転アニメのセットはset_rotateメソッドで行う. 引数には初期角度,移動角度,移動時間,シフトを指定する. 例えば set_rotate(0,90,60,1) とすれば0度から90度まで60フレームでアニメーションする.シフトについては後述する. ところで,0度から90度ってなんだよって思うだろう. 今回の定義では回転の原点は現在のブロックの描画位置となっている. また,0度とは数学の時間によく見たように単位円の右側を表す(座標で言うと[1,0]). 初期角度 < 移動角度 なら左回転,逆なら右回転のアニメーションとなる. コントロールブロックについて,xは現在のrowの値から決定され,yは現在の描画位置そのものである. これはすなわち以下を意味する.
それらを考慮した上で回転のアニメーションの仕様を以下のように設定した.
set_rotate_x †まずはx方向の回転のセットを見てみよう. 引数のpush_shiftは後で説明するとして, 何をしているのかざっと見てみる. from_rad = from * Math::PI / 180 to_rad = to * Math::PI / 180 まず角度をラジアンに変換している.ここはいいだろう. time_divide = (to_rad - from_rad) / time 次に1フレームあたりどのくらい角度が変化するのかを計算. shift = Math.cos(to_rad) * @block_s 最終的な角度はどのくらいシフト量があるのかを計算している. rotate = [] rad = from_rad ps = push_shift * @block_s time.times do rad += time_divide ps -= ps_divide rotate.push(Math.cos(rad) * @block_s - shift - ps) end return rotate 結果の配列rotateを作り,現在の角度radを初期角度にセットする. そしてアニメーションのフレーム数だけループを回してシフト量を配列にプッシュしていく. シフト量は数学の時間で習ったとおり,x方向についてはコサインで求められる. これにブロックサイズをかけて1ブロック分のシフト量にする. 前述した通り,x方向に関しては毎フレームrowに基づいた描画座標がセットされるので, アニメーションが終了したときは正規の描画座標になるようにアニメーションさせなければならない. すなわち,rotate_xの配列の最後は0になるはずである. よって(Math.cos(rad) - shift)はrad=to_radの時0となり,仕様を満たすことがわかる. set_rotate_y †x方向との違いだけ説明する. というか話は簡単で,まずy方向なのでサインでシフト量が求められる. あとはy方向に関しては速度が知りたいので,今回のシフト量から前回のシフト量を引けば今回のフレームの移動量がわかる.それだけ. ピボットブロックの回転 †いよいよピボットブロックの回転処理を考えよう. ピボットブロックはピボットを中心に最大4個のブロックを周囲に配置できるが, 2個ブロック,3個ブロックの構成は以下のようになる. b0 b0 p or p b1 すなわち,縦か横に3個並ぶことはない. 今回は簡単のためにこの制約の下での回転を考えることにする. can_rotate? †回転可能かチェックするca_rotate?メソッドを以下のように定義した. class PivotControlBlock < ControlBlock
def can_rotate?(ir, table, row_s)
return false if ir == 0 # no rotate
r = @pivot.row; l = @pivot.line
max_cond = r < row_s - 1
min_cond = r > 0
@belongs.each.with_index do |block,i|
next if !block || i % 2 != 0
row_dir = ir * (1 - i)
if (row_dir > 0 ? max_cond : min_cond) && !table[r+row_dir][l] # OK
return {:dir => ir, :shift => 0}
elsif (row_dir > 0 ? min_cond : max_cond) && !table[r-row_dir][l] # OK: shift
return {:dir => ir, :shift => -row_dir}
end
# NG
return false
end
return {:dir => ir, :shift => 0}
end
end
引数のirは右回転なら1,左なら-1,それ以外なら0が入っている. まずピボットを取り囲むブロックそれぞれについてループを回しているが, その位置にブロックが存在しないか位置がピボットの横の場合はスキップしてよい*2. なぜなら横にあるブロックは回転するとピボットの上か下に来る事になり,上ならばぷよぷよの性質上そこには絶対にブロックは存在しないし,下ならばめり込むと自動的にコントロールブロック全体が上にずれてうまく処理できるからである. そして,回転できるかの判定部分だがなんかわかりにくいことが書いてある気がするので大まかに説明すると, 回転の結果としてピボットの右にブロックが来る場合はピボットの位置の右側にすでにブロックがあるかテーブルをチェックする. もしなければ回転できるので結果を返す. ここで返す結果は回転方向とシフト量を持ったハッシュである. また,ここですぐに結果を返せるのは先ほどの制約のおかげである. もし制約がなければ左側にブロックが来るかどうかも考慮しなければならない. 次に,すでにブロックがあった場合,ピボットを左にずらすことができれば回転処理を行うことができる. このときのピボットのrowの移動量が今までさんざんスルーしてきたシフトである. なので,ピボットの右にもしすでにブロックがあった場合はピボットの左のマスをチェックし,もしあいていたらシフトをセットした上で回転可能という結果を返す. それ以外は回転できない. rotate †実際に回転するrotateメソッドは以下のように定義した. class PivotControlBlock < ControlBlock def rotate(rotate, time) # pivot @pivot.row += rotate[:shift] @pivot.set_rotate(0, 0, time, rotate[:shift]) if rotate[:shift] != 0 # belongs @belongs.rotate!(-rotate[:dir]) @belongs.each.with_index do |block, i| next unless block # set animation to = 90 * (1 - i) block.set_rotate(to + 90 * rotate[:dir], to, time, rotate[:shift]) case i when 0; block.row += rotate[:dir] when 1; block.row += 1 when 2; block.row += -rotate[:dir] when 3; block.row += -1 end # shift block.row += rotate[:shift] end end end 引数のrotateはcan_rotate?の戻り値,すなわち回転方向とシフトのハッシュである. メソッド内部ではまずピボットの処理を行っている. ピボットはシフト以外の影響を受けない. もしシフトがある場合は実質シフトの影響のみとなるが,回転アニメーションをセットしている. 次に周囲のブロックについての処理を行っている. まずrotate!メソッドにより配列の中身をごっそり回転させている. どういうことかというとピボットブロックは配列の順番でピボットの上下左右どこに配置されるブロックかを表しているので,例えば右回転なら次の様にすれば良い. old = [b0, b1, b2, b3] new = [b3, b0, b1, b2] b0 b3 b3 p b1 => b2 p b0 b2 b2 配列の順番の入れは概念的な回転処理にすぎないので,あとはブロックそれぞれについて実際に位置を更新するための処理を施す. すでに配列的には回転させているので,ループ時に持つ配列のインデックスは回転後の位置を表すことになる. よって,まずは回転後の角度toを計算し,それを元にfromを求め,回転アニメーションをセットする. さらに,しつこいようだがx方向についてはrowが基準で描画座標が,y方向については描画座標を基準にlineが決定されるので,rowを更新しないとブロックの位置が正しく更新されない.y座標はアニメーションで更新されるので問題ない. 最後にシフトに基づいてさらにrowを更新すれば回転終了である. set_rotate_xのシフトについて †ここでシフトの概念が登場したので,少し戻ってset_rotate_xのシフトについて見てみよう. class FreeBlock < Block def set_rotate_x(from, to, time, push_shift) # x : rotate distance list based on to from_rad = from * Math::PI / 180 to_rad = to * Math::PI / 180 time_divide = (to_rad - from_rad) / time shift = Math.cos(to_rad) * @block_s ps_divide = (push_shift * @block_s).quo time rotate = [] rad = from_rad ps = push_shift * @block_s time.times do rad += time_divide ps -= ps_divide rotate.push(Math.cos(rad) * @block_s - shift - ps) end return rotate end end このメソッド内ではピボット自体が移動するシフトをプッシュシフトと呼んでいる. プッシュシフトとはx方向に移動することであるから,ようするにmove_xの処理を擬似的にここで再現していることになる. また,前述の通りx方向は移動後の位置を元にしてシフト配列を作らなければならないので,プッシュシフトpsはradP=to_radのときには0なっていなければならない. よってpsの初期値はプッシュシフト*ブロックサイズとなり,あとはこの値がアニメーションの時間によって1フレームあたりの減少量ps_divideを求めれば良い. 実行 †いよいよそれっぽくなってきた. Loading the player ...
|