目次へもどる

回転させよう(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は現在の描画位置そのものである. これはすなわち以下を意味する.

  1. x座標については毎回rowの値から算出されるため,アニメーションはその位置からどのくらいずれるかを毎フレーム計算する
  2. y座標については描画位置そのものを保持しているため,アニメーションは毎フレームどのくらい移動するかの距離を計算する よって,x方向とy方向で計算の仕方が多少異なる.

それらを考慮した上で回転のアニメーションの仕様を以下のように設定した.

  • インスタンス変数@rotate_xと@rotate_yを用意
  • それぞれnilならアニメーションしていない
  • それ以外の場合は配列が格納されている
  • @rotate_xは毎フレームごとのx座標のシフト量が入っている
  • @rotate_yは毎フレームごとのy座標の移動量が入っている
  • アニメーションは配列の先頭から一つ要素を取り出してx,y座標を変化させる
  • 配列が空になったらそれぞれにnilをセットしてアニメーションを終了させる

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 ...

*1 正確には同様にした
*2 無条件に回転してよい

添付ファイル: filePuyoPuyoChap16.flv 647件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2013-08-30 (金) 22:12:08 (3891d)