[[目次へもどる>PuyoPuyo]] * 回転させよう(Pivot編) [#p5c2cbb9] #contents いよいよ操作ブロック最後の山,回転を作ろう. ** 考察 [#h79a6a7c] まずこれまで,操作ブロックは回転の際,軸となるブロックが一つあり,それを中心として回転する,としてきた. これは正しい.そしてそれを元にしてコントロールブロックは作られている. しかし,ぷよぷよフィーバーを考えるとそうも行かない. ぷよぷよフィーバーでは通常の2個ブロックの他に3個のブロックや4個のブロック,さらには色を変えられる4個のブロックも存在する. このうち前述の仕様が通用するのは2個,3個ブロックのみである. そのため,例によってControlBlockクラスはコントロールブロックの共通の機能のみをもった親クラスとし,サブクラスにピボットを持つPivotControlBlock,四つ組みのブロックで,それらが回転するCycleControlBlockを定義した. #sh(ruby){{ 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 }} #sh(ruby){{ =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 }} #sh(ruby){{ =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個その周囲にブロックを持たせることができるので,二個,三個ブロックが表現できる. さて,このように違うタイプのコントロールクラスが存在しうるようになったので, これらの切り替えなどをまとめて管理してくれる人を作ろう. #sh(ruby){{ 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メソッドで取得できる. ** フィールドの処理 [#a335630d] フィールド側の回転処理はコントロールブロックの種類によらず同じなのでこちらを先に考えてしまおう. #sh(ruby){{ 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が入っている. ** 回転アニメーション [#h8d0bd75] 回転のアニメーションもブロックの種類によらず同様である((正確には同様にした))ので, 先に定義しよう. #sh(ruby){{ 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は現在の描画位置そのものである. これはすなわち以下を意味する. + x座標については毎回rowの値から算出されるため,アニメーションはその位置からどのくらいずれるかを毎フレーム計算する + y座標については描画位置そのものを保持しているため,アニメーションは毎フレームどのくらい移動するかの距離を計算する よって,x方向とy方向で計算の仕方が多少異なる. それらを考慮した上で回転のアニメーションの仕様を以下のように設定した. - インスタンス変数@rotate_xと@rotate_yを用意 - それぞれnilならアニメーションしていない - それ以外の場合は配列が格納されている - @rotate_xは毎フレームごとのx座標のシフト量が入っている - @rotate_yは毎フレームごとのy座標の移動量が入っている - アニメーションは配列の先頭から一つ要素を取り出してx,y座標を変化させる - 配列が空になったらそれぞれにnilをセットしてアニメーションを終了させる *** set_rotate_x [#fc96db05] まずはx方向の回転のセットを見てみよう. 引数のpush_shiftは後で説明するとして, 何をしているのかざっと見てみる. #sh(ruby){{ from_rad = from * Math::PI / 180 to_rad = to * Math::PI / 180 }} まず角度をラジアンに変換している.ここはいいだろう. #sh(ruby){{ time_divide = (to_rad - from_rad) / time }} 次に1フレームあたりどのくらい角度が変化するのかを計算. #sh(ruby){{ shift = Math.cos(to_rad) * @block_s }} 最終的な角度はどのくらいシフト量があるのかを計算している. #sh(ruby){{ 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 [#ye4348ef] x方向との違いだけ説明する. というか話は簡単で,まずy方向なのでサインでシフト量が求められる. あとはy方向に関しては速度が知りたいので,今回のシフト量から前回のシフト量を引けば今回のフレームの移動量がわかる.それだけ. ** ピボットブロックの回転 [#r0c71d7e] いよいよピボットブロックの回転処理を考えよう. ピボットブロックはピボットを中心に最大4個のブロックを周囲に配置できるが, 2個ブロック,3個ブロックの構成は以下のようになる. b0 b0 p or p b1 すなわち,縦か横に3個並ぶことはない. 今回は簡単のためにこの制約の下での回転を考えることにする. *** can_rotate? [#o21e7ce0] 回転可能かチェックするca_rotate?メソッドを以下のように定義した. #sh(ruby){{ 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が入っている. まずピボットを取り囲むブロックそれぞれについてループを回しているが, その位置にブロックが存在しないか位置がピボットの横の場合はスキップしてよい((無条件に回転してよい)). なぜなら横にあるブロックは回転するとピボットの上か下に来る事になり,上ならばぷよぷよの性質上そこには絶対にブロックは存在しないし,下ならばめり込むと自動的にコントロールブロック全体が上にずれてうまく処理できるからである. そして,回転できるかの判定部分だがなんかわかりにくいことが書いてある気がするので大まかに説明すると, 回転の結果としてピボットの右にブロックが来る場合はピボットの位置の右側にすでにブロックがあるかテーブルをチェックする. もしなければ回転できるので結果を返す. ここで返す結果は回転方向とシフト量を持ったハッシュである. また,ここですぐに結果を返せるのは先ほどの制約のおかげである. もし制約がなければ左側にブロックが来るかどうかも考慮しなければならない. 次に,すでにブロックがあった場合,ピボットを左にずらすことができれば回転処理を行うことができる. このときのピボットのrowの移動量が今までさんざんスルーしてきたシフトである. なので,ピボットの右にもしすでにブロックがあった場合はピボットの左のマスをチェックし,もしあいていたらシフトをセットした上で回転可能という結果を返す. それ以外は回転できない. *** rotate [#v9075e66] 実際に回転するrotateメソッドは以下のように定義した. #sh(ruby){{ 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のシフトについて [#db40770f] ここでシフトの概念が登場したので,少し戻ってset_rotate_xのシフトについて見てみよう. #sh(ruby){{ 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を求めれば良い. ** 実行 [#dbe3c5d8] いよいよそれっぽくなってきた. #media(PuyoPuyoChap16/PuyoPuyoChap16.flv);