テトリスの作り方
前回からの続き。
前回までにStar Rubyでスカッシュゲームを作ったのだけど、たった60行のシンプルなゲームであった。ほとんど全ての処理を、Game.runブロックの中に書いていた。
今度はもうちょっと本格的なゲームを作ってみたい。そうだ、テトリスを作ろう!テトリスはサンプルコードの中に含まれているのだけど、敢えて自分の頭で考えて独自の実装をしてみるのだ。落ちものパズルゲームの元祖。果たしてちゃんとできるだろうか?
前回のスカッシュのように、すべての処理をGame.runブロックの中に書いてしまうと、長過ぎるループに惑わされて、たぶん破綻しそう。部品ごとにどのように処理を分担するべきか、テトリスワールドに踏み込んで考えてみた。
作るテトリスの仕様
- 4つの正方形を組み合わせたブロック(テトリミノと呼ばれる)が、上から落下してくる。
- テトリミノには7つの形状がある。
テトリス - Wikipedia
- テトリミノを回転、移動させながら、積み上げていく。
- テトリミノは最下部、または他のテトリミノの上まで来て、それ以上落下できなくなると、その位置に固定される。
- テトリミノが固定されると、新たなテトリミノが上から落ちてくる。
- ブロックが隙間なく1段すべてに積み上がると、その1段はクリアされる。
- テトリミノの形状によって、2段、3段、あるいは4段同時にクリアされることもある。
- クリアされた位置より上のブロックは、クリアされた段数だけ落下する。
- テトリミノが一番上まで積み上がってしまったら、ゲームオーバー。
- 次々と落下してくるテトリミノを隙間なく積み上げて、より多くの段をクリアすることが目的。
落ちるブロック(GitHub差分)
- サンプルコードのhelloworld.rbを以下のように修正すると、ブロックが落下するコードとなる。
require "starruby" include StarRuby white = Color.new(255, 255, 255) y = 0 x = 8 Game.run(320, 240, :title => "tetris") do |game| break if Input.keys(:keyboard).include?(:escape) x += 8 if Input.keys(:keyboard).include?(:right) x -= 8 if Input.keys(:keyboard).include?(:left) y += 1 y = 0 if y > 240 game.screen.clear game.screen.render_rect(x, y, 16, 16, white) end
- 「game.screen.render_rect(x, y, 16, 16, white)」によって、ブロックを描画している。
- (x,y)座標に、幅16px、高さ16px、白塗りの四角を描くのだ。
- 正方形のブロックが1つ、繰り返し落下する。
テトリスワールドの定義(GitHub差分)
テトリス - Wikipediaによると...
- ブロックを積み上げるエリアは、幅10ブロック、高さ20ブロックのサイズである。
- よって、その定義に従って、Game.runウィンドウを以下のように定義してみた。
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW white = Color.new(255, 255, 255) y = 0 x = 8 Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| break if Input.keys(:keyboard).include?(:escape) x += 8 if Input.keys(:keyboard).include?(:right) x -= 8 if Input.keys(:keyboard).include?(:left) y += 1 y = 0 if y > 240 game.screen.clear game.screen.render_rect(x, y, 16, 16, white) end
ブロック単位の処理に変更(GitHub差分)
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW white = Color.new(255, 255, 255) y = 0 x = 3 Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| break if Input.keys(:keyboard).include?(:escape) x += 1 if Input.keys(:keyboard).include?(:right) x -= 1 if Input.keys(:keyboard).include?(:left) y += 0.125 y = 0 if y >= FIELD_ROW game.screen.clear game.screen.render_rect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE, white) end
テトリミノを描画する(GitHub差分)
- いつまでもブロックの落下だけでは進歩がない...。
- この辺でテトリスらしく、4つのブロックを組み合わせたテトリミノを描画してみたい。
- テトリミノに関することは、Tetriminoクラスで処理することにする。
- Tetriminoクラスを追加すると、今までのコードが劇的に変化する。
- まだ完全でないけど、暫定的に動くコードは以下のようになった。
- テトリミノのデータは、2次元配列に保存した。
- いずれ回転させる時のことを考えて、対称性を保つため正方行列となるようにした。
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW class Tetrimino attr_reader :blocks, :x, :y @@minos = [] @@minos << [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]] @@minos << [[1,1], [1,1]] @@minos << [[0,1,1], [1,1,0], [0,0,0]] @@minos << [[1,1,0], [0,1,1], [0,0,0]] @@minos << [[1,0,0], [1,1,1], [0,0,0]] @@minos << [[0,0,1], [1,1,1], [0,0,0]] @@minos << [[0,1,0], [1,1,1], [0,0,0]] def initialize @id = rand(0..6) @blocks = @@minos[@id] @x, @y = 3, 0 end def side_step(dx) @x += dx end def fall(dy) @y += dy end end def draw_block(game, x, y) game.screen.render_rect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE, Color.new(255, 255, 255)) end def draw_tetrimino(game, tetrimino) return if !tetrimino tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(game, tetrimino.x + c , tetrimino.y + r) if col == 1 end end end Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| @tetrimino ||= Tetrimino.new dx = 0 dy = 0.125 break if Input.keys(:keyboard).include?(:escape) dx = 1 if Input.keys(:keyboard).include?(:right) dx = -1 if Input.keys(:keyboard).include?(:left) @tetrimino.side_step(dx) @tetrimino.fall(dy) @tetrimino = nil if @tetrimino.y >= FIELD_ROW game.screen.clear draw_tetrimino(game, @tetrimino) end
描画はTextureクラスに任せる(GitHub差分)
- 描画について、1つ発見があった!
- コード末尾の「draw_tetrimino(game, @tetrimino)」は、書いた時から格好悪いなと思っていた。
- 本当は「game.screen.draw_tetrimino(@tetrimino)」と、書きたいのだ。
その方法が分かった!
- game.screen.class とは StarRuby::Texture なので、Textureクラスを拡張すればいいのだ!
- ついでに、個々のブロックの存在が見えるように、1px小さい正方形のブロックを描くようにした。
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW class Texture def draw_block(x, y) render_rect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 1, BLOCK_SIZE - 1, Color.new(255, 255, 255)) end def draw_tetrimino(tetrimino) return if !tetrimino tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(tetrimino.x + c , tetrimino.y + r) if col == 1 end end end end class Tetrimino attr_reader :blocks, :x, :y @@minos = [] @@minos << [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]] @@minos << [[1,1], [1,1]] @@minos << [[0,1,1], [1,1,0], [0,0,0]] @@minos << [[1,1,0], [0,1,1], [0,0,0]] @@minos << [[1,0,0], [1,1,1], [0,0,0]] @@minos << [[0,0,1], [1,1,1], [0,0,0]] @@minos << [[0,1,0], [1,1,1], [0,0,0]] def initialize @id = rand(0..6) @blocks = @@minos[@id] @x, @y = 3, 0 end def side_step(dx) @x += dx end def fall(dy) @y += dy end end Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| @tetrimino ||= Tetrimino.new dx = 0 dy = 0.125 break if Input.keys(:keyboard).include?(:escape) dx = 1 if Input.keys(:keyboard).include?(:right) dx = -1 if Input.keys(:keyboard).include?(:left) @tetrimino.side_step(dx) @tetrimino.fall(dy) @tetrimino = nil if @tetrimino.y >= FIELD_ROW game.screen.clear game.screen.draw_tetrimino(@tetrimino) end
テトリミノを回転させる(GitHub差分)
- 二次元配列をどうやって回転しようかと悩んでいたのだけど、素晴らしい方法があった!(感謝です!)
- rubyで2次元配列を使用しての行列表現&行列の回転 - simanmanのブログ
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW class Texture def draw_block(x, y) render_rect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 1, BLOCK_SIZE - 1, Color.new(255, 255, 255)) end def draw_tetrimino(tetrimino) return if !tetrimino tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(tetrimino.x + c , tetrimino.y + r) if col == 1 end end end end class Tetrimino attr_reader :blocks, :x, :y @@minos = [] @@minos << [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]] @@minos << [[1,1], [1,1]] @@minos << [[0,1,1], [1,1,0], [0,0,0]] @@minos << [[1,1,0], [0,1,1], [0,0,0]] @@minos << [[1,0,0], [1,1,1], [0,0,0]] @@minos << [[0,0,1], [1,1,1], [0,0,0]] @@minos << [[0,1,0], [1,1,1], [0,0,0]] def initialize @id = rand(0..6) @blocks = @@minos[@id] @x, @y, @angle = 3, 0, 0 end def blocks(angle = @angle) case angle % 4 when 0 @blocks when 1 @blocks.transpose.map(&:reverse) #右90度回転 when 2 @blocks.reverse.map(&:reverse) #180度回転 when 3 @blocks.transpose.reverse #左90度回転(右270度回転) end end def rotate(dr) @angle += dr end def side_step(dx) @x += dx end def fall(dy) @y += dy end end Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| @tetrimino ||= Tetrimino.new dx = 0 dy = 0.125 dr = 0 break if Input.keys(:keyboard).include?(:escape) dx = 1 if Input.keys(:keyboard).include?(:right) dx = -1 if Input.keys(:keyboard).include?(:left) dr = 1 if Input.keys(:keyboard).include?(:x) dr = 3 if Input.keys(:keyboard).include?(:z) @tetrimino.rotate(dr) @tetrimino.side_step(dx) @tetrimino.fall(dy) @tetrimino = nil if @tetrimino.y >= FIELD_ROW game.screen.clear game.screen.draw_tetrimino(@tetrimino) end
テトリミノの移動を制限する(GitHub差分)
- テトリミノを制限なく動かせてしまう状況を改善した。
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW class Texture def draw_block(x, y) render_rect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 1, BLOCK_SIZE - 1, Color.new(255, 255, 255)) end def draw_tetrimino(tetrimino) return if !tetrimino tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(tetrimino.x + c , tetrimino.y + r) if col == 1 end end end end class Tetrimino attr_reader :state, :x, :y @@minos = [] @@minos << [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]] @@minos << [[1,1], [1,1]] @@minos << [[0,1,1], [1,1,0], [0,0,0]] @@minos << [[1,1,0], [0,1,1], [0,0,0]] @@minos << [[1,0,0], [1,1,1], [0,0,0]] @@minos << [[0,0,1], [1,1,1], [0,0,0]] @@minos << [[0,1,0], [1,1,1], [0,0,0]] def initialize @id = rand(0..6) @blocks = @@minos[@id] @x, @y, @angle = 3, 0, 0 @state = :live end def blocks(angle = @angle) case angle % 4 when 0 @blocks when 1 @blocks.transpose.map(&:reverse) #右90度回転 when 2 @blocks.reverse.map(&:reverse) #180度回転 when 3 @blocks.transpose.reverse #左90度回転(右270度回転) end end def rotate(dr) if can_move?(0, 0, dr) then @angle += dr end end def side_step(dx) if can_move?(dx, 0, 0) then @x += dx end end def fall(dy) if can_move?(0, 1, 0) then @y += dy else @state = :dead end end def can_move?(dx, dy, dr) x = @x + dx y = @y + dy angle = @angle + dr blocks(angle).each_with_index do |row, r| row.each_with_index do |col, c| if col == 1 then if x + c < 0 || x + c >= FIELD_COL || y + r >= FIELD_ROW then return false end end end end true end end Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| @tetrimino ||= Tetrimino.new dx = 0 dy = 0.125 dr = 0 break if Input.keys(:keyboard).include?(:escape) dx = 1 if Input.keys(:keyboard).include?(:right) dx = -1 if Input.keys(:keyboard).include?(:left) dr = 1 if Input.keys(:keyboard).include?(:x) dr = 3 if Input.keys(:keyboard).include?(:z) @tetrimino.rotate(dr) @tetrimino.side_step(dx) @tetrimino.fall(dy) @tetrimino = nil if @tetrimino.state == :dead game.screen.clear game.screen.draw_tetrimino(@tetrimino) end
テトリミノを積み上げる(GitHub差分)
- 現状、テトリミノは一番下まで落ちると、消えてしまう...。
- しかし、テトリスワールドでは、テトリミノは積み上がらなくてはならない。
- そこで、テトリミノを積み上げる「場」として、Fieldクラスを組み込んでみた。
- Fieldクラスは10×20の二次元配列を持っていて、
- テトリミノが落下できなくなると、その時のテトリミノを取り込むのだ。
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW class Texture def draw_block(x, y) render_rect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 1, BLOCK_SIZE - 1, Color.new(255, 255, 255)) end def draw_tetrimino(tetrimino) return if !tetrimino tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(tetrimino.x + c , tetrimino.y + r) if col == 1 end end end def draw_field(field) return if !field field.matrix.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(c, r) if col != nil end end end end class Tetrimino attr_reader :state, :x, :y @@minos = [] @@minos << [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]] @@minos << [[1,1], [1,1]] @@minos << [[0,1,1], [1,1,0], [0,0,0]] @@minos << [[1,1,0], [0,1,1], [0,0,0]] @@minos << [[1,0,0], [1,1,1], [0,0,0]] @@minos << [[0,0,1], [1,1,1], [0,0,0]] @@minos << [[0,1,0], [1,1,1], [0,0,0]] def initialize(field) @field = field @id = rand(0..6) @blocks = @@minos[@id] @x, @y, @angle = 3, 0, 0 @state = :live end def blocks(angle = @angle) case angle % 4 when 0 @blocks when 1 @blocks.transpose.map(&:reverse) #右90度回転 when 2 @blocks.reverse.map(&:reverse) #180度回転 when 3 @blocks.transpose.reverse #左90度回転(右270度回転) end end def rotate(dr) if can_move?(0, 0, dr) then @angle += dr end end def side_step(dx) if can_move?(dx, 0, 0) then @x += dx end end def fall(dy) if can_move?(0, 1, 0) then @y += dy else @state = :dead end end def can_move?(dx, dy, dr) x = @x + dx y = @y + dy angle = @angle + dr blocks(angle).each_with_index do |row, r| row.each_with_index do |col, c| if col == 1 then if x + c < 0 || x + c >= FIELD_COL || y + r >= FIELD_ROW || @field.matrix[y + r][x + c] != nil then return false end end end end true end end class Field attr_reader :matrix, :state def initialize(row, col) @matrix = Array.new(row){Array.new(col)} end def import(tetrimino) tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| @matrix[tetrimino.y + r][tetrimino.x + c] = 1 if col == 1 end end end end Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| @field ||= Field.new(FIELD_ROW, FIELD_COL) @tetrimino ||= Tetrimino.new(@field) dx = 0 dy = 0.125 dr = 0 break if Input.keys(:keyboard).include?(:escape) dx = 1 if Input.keys(:keyboard).include?(:right) dx = -1 if Input.keys(:keyboard).include?(:left) dr = 1 if Input.keys(:keyboard).include?(:x) dr = 3 if Input.keys(:keyboard).include?(:z) @tetrimino.rotate(dr) @tetrimino.side_step(dx) @tetrimino.fall(dy) if @tetrimino.state == :dead then @field.import(@tetrimino) @tetrimino = nil end game.screen.clear game.screen.draw_tetrimino(@tetrimino) game.screen.draw_field(@field) end
敏感過ぎるキー反応を鈍くする(GitHub差分)
- 90度回転したいのに180度回転しているし、1ブロック移動したいのにあっという間に2、3ブロック移動してしまう。
- キーの反応が敏感過ぎるのだ!
- 何も設定していないと、現状の:fps = 30で、1秒間に30回も押されているキーの値を読み取ってしまうのだ。
- その解決方法は、StarRuby::Inputに書かれていた。
Input.keys(device, options = {})
...中略...
options には Hash を指定します。指定できるキーと値は、以下の通りです。
http://www.starruby.info/ja/documentation/api_reference/star_ruby.input
キー 値 デフォルト値 :device_number 0番から始まるデバイス番号。ゲームパッドの場合のみ有効です。 0 :duration キーを押し始めてから、キーが押されていると判別される持続時間 (フレーム数)。 -1 を指定した場合は無限です (押しっぱなしの間ずっと「押された」と判別されます)。 -1 :delay :duration を正数で指定している場合、 2 回目以降「押された」と判別されるまでの遅延時間 (フレーム数)。 -1 を指定した場合は無限です (2 回目以降の判定がありません)。 -1 :interval 2 回目以降「押された」と判断される時間間隔 (フレーム数)。 0
- よって、修正箇所はGame.runブロックの以下のコード。
- ついでに、↓矢印キーで素早く落下するようにしてみた。
-
-
- 修正箇所が限定的なので差分のみ
-
(master)$ git diff
diff --git a/tetris.rb b/tetris.rb
index 2f50a39..a724e19 100644
--- a/tetris.rb
+++ b/tetris.rb
@@ -148,10 +148,11 @@ Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game|
dr = 0
break if Input.keys(:keyboard).include?(:escape)
- dx = 1 if Input.keys(:keyboard).include?(:right)
- dx = -1 if Input.keys(:keyboard).include?(:left)
- dr = 1 if Input.keys(:keyboard).include?(:x)
- dr = 3 if Input.keys(:keyboard).include?(:z)
+ dx = 1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 1}).include?(:right)
+ dx = -1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 1}).include?(:left)
+ dy = 1 if Input.keys(:keyboard, {:duration =>-1, :delay =>-1, :interval => 0}).include?(:down)
+ dr = 1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 3}).include?(:x)
+ dr = 3 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 3}).include?(:z)
@tetrimino.rotate(dr)
@tetrimino.side_step(dx)
ブロックで満たされた段をクリアする(GitHub差分)
- ブロックで満たされた段は、クリアする必要がある。
-
-
- 修正箇所が限定的なので差分のみ
-
(master)$ git diff
diff --git a/tetris.rb b/tetris.rb
index a724e19..04dc0c3 100644
--- a/tetris.rb
+++ b/tetris.rb
@@ -136,6 +136,13 @@ class Field
end
end
end
+
+ def clear_lines
+ @matrix.reject!{|row| !row.include?(nil)}
+ deleted_line = FIELD_ROW - @matrix.size
+ deleted_line.times{@matrix.unshift(Array.new(10){nil})}
+ end
+
end
@@ -160,6 +167,7 @@ Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game|
if @tetrimino.state == :dead then
@field.import(@tetrimino)
+ @field.clear_lines
@tetrimino = nil
end
ラストチャンスを与える(GitHub差分)
- テトリミノが着地して、即動かせなくなると、何だか悲しい...。
- その辺の事情は本家テトリスも察していて、着地しても若干の間、回転や移動ができるようになっている。
- ならば自分のテトリスにも、そのチャンスを与えてみたい。
- Tetriminoクラスに@last_chanceというカウンターを追加して、カウンター数が残っている間は操作できるようにしてみた。
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW class Texture def draw_block(x, y) render_rect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 1, BLOCK_SIZE - 1, Color.new(255, 255, 255)) end def draw_tetrimino(tetrimino) return if !tetrimino tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(tetrimino.x + c , tetrimino.y + r) if col == 1 end end end def draw_field(field) return if !field field.matrix.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(c, r) if col != nil end end end end class Tetrimino attr_reader :state, :x, :y @@minos = [] @@minos << [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]] @@minos << [[1,1], [1,1]] @@minos << [[0,1,1], [1,1,0], [0,0,0]] @@minos << [[1,1,0], [0,1,1], [0,0,0]] @@minos << [[1,0,0], [1,1,1], [0,0,0]] @@minos << [[0,0,1], [1,1,1], [0,0,0]] @@minos << [[0,1,0], [1,1,1], [0,0,0]] def initialize(game, field) @game = game @field = field @id = rand(0..6) @blocks = @@minos[@id] @x, @y, @angle = 3, 0, 0 @state = :live @last_chance = 0 end def blocks(angle = @angle) case angle % 4 when 0 @blocks when 1 @blocks.transpose.map(&:reverse) #右90度回転 when 2 @blocks.reverse.map(&:reverse) #180度回転 when 3 @blocks.transpose.reverse #左90度回転(右270度回転) end end def rotate(dr) if can_move?(0, 0, dr) then @angle += dr end end def side_step(dx) if can_move?(dx, 0, 0) then @x += dx end end def fall(dy) @last_chance -= 1 if can_move?(0, 1, 0) then @y += dy else case when @last_chance < 0 @last_chance = @game.fps / 2 when @last_chance == 0 @state = :dead end end end def can_move?(dx, dy, dr) x = @x + dx y = @y + dy angle = @angle + dr blocks(angle).each_with_index do |row, r| row.each_with_index do |col, c| if col == 1 then if x + c < 0 || x + c >= FIELD_COL || y + r >= FIELD_ROW || @field.matrix[y + r][x + c] != nil then return false end end end end true end end class Field attr_reader :matrix, :state def initialize(row, col) @matrix = Array.new(row){Array.new(col)} end def import(tetrimino) tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| @matrix[tetrimino.y + r][tetrimino.x + c] = 1 if col == 1 end end end def clear_lines @matrix.reject!{|row| !row.include?(nil)} deleted_line = FIELD_ROW - @matrix.size deleted_line.times{@matrix.unshift(Array.new(10){nil})} end end Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| @field ||= Field.new(FIELD_ROW, FIELD_COL) @tetrimino ||= Tetrimino.new(game, @field) dx = 0 dy = 0.125 dr = 0 break if Input.keys(:keyboard).include?(:escape) dx = 1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 1}).include?(:right) dx = -1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 1}).include?(:left) dy = 1 if Input.keys(:keyboard, {:duration =>-1, :delay =>-1, :interval => 0}).include?(:down) dr = 1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 3}).include?(:x) dr = 3 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 3}).include?(:z) @tetrimino.rotate(dr) @tetrimino.side_step(dx) @tetrimino.fall(dy) if @tetrimino.state == :dead then @field.import(@tetrimino) @field.clear_lines @tetrimino = nil end game.screen.clear game.screen.draw_tetrimino(@tetrimino) game.screen.draw_field(@field) end
ブロックが潰れる現象の対策(GitHub差分)
- ラストチャンスを与えたら、着地した時にブロックが潰れる現象が気になってきた。
- この現象は、↓矢印キーで素早く落下させた時に発生する。
- よって、↓矢印キーを押した時は、端数のy座標を整数に丸める処理を追加してみた。
-
-
- 修正箇所が限定的なので差分のみ
-
(master)$ git diff
diff --git a/tetris.rb b/tetris.rb
index 86ce962..27cc8b8 100644
--- a/tetris.rb
+++ b/tetris.rb
@@ -97,6 +97,7 @@ class Tetrimino
def fall(dy)
@last_chance -= 1
+ @y = @y.to_i if dy == 1
if can_move?(0, 1, 0) then
@y += dy
else
色付きのテトリミノにしておく(GitHub差分)
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW RGBS = [[ 0, 255, 255], [255, 255, 0], [ 0, 255, 0], [255, 0, 0], [ 0, 0, 255], [255, 127, 0], [255, 0, 255]] class Texture def draw_block(x, y, rgb) render_rect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 1, BLOCK_SIZE - 1, Color.new(*rgb)) end def draw_tetrimino(tetrimino) return if !tetrimino tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(tetrimino.x + c , tetrimino.y + r, RGBS[tetrimino.id]) if col == 1 end end end def draw_field(field) return if !field field.matrix.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(c, r, RGBS[col]) if col != nil end end end end class Tetrimino attr_reader :id, :state, :x, :y @@minos = [] @@minos << [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]] @@minos << [[1,1], [1,1]] @@minos << [[0,1,1], [1,1,0], [0,0,0]] @@minos << [[1,1,0], [0,1,1], [0,0,0]] @@minos << [[1,0,0], [1,1,1], [0,0,0]] @@minos << [[0,0,1], [1,1,1], [0,0,0]] @@minos << [[0,1,0], [1,1,1], [0,0,0]] def initialize(game, field) @game = game @field = field @id = rand(0..6) @blocks = @@minos[@id] @x, @y, @angle = 3, 0, 0 @state = :live @last_chance = 0 end def blocks(angle = @angle) case angle % 4 when 0 @blocks when 1 @blocks.transpose.map(&:reverse) #右90度回転 when 2 @blocks.reverse.map(&:reverse) #180度回転 when 3 @blocks.transpose.reverse #左90度回転(右270度回転) end end def rotate(dr) if can_move?(0, 0, dr) then @angle += dr end end def side_step(dx) if can_move?(dx, 0, 0) then @x += dx end end def fall(dy) @last_chance -= 1 @y = @y.to_i if dy == 1 if can_move?(0, 1, 0) then @y += dy else case when @last_chance < 0 @last_chance = @game.fps / 2 when @last_chance == 0 @state = :dead end end end def can_move?(dx, dy, dr) x = @x + dx y = @y + dy angle = @angle + dr blocks(angle).each_with_index do |row, r| row.each_with_index do |col, c| if col == 1 then if x + c < 0 || x + c >= FIELD_COL || y + r >= FIELD_ROW || @field.matrix[y + r][x + c] != nil then return false end end end end true end end class Field attr_reader :matrix, :state def initialize(row, col) @matrix = Array.new(row){Array.new(col)} end def import(tetrimino) tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| @matrix[tetrimino.y + r][tetrimino.x + c] = tetrimino.id if col == 1 end end end def clear_lines @matrix.reject!{|row| !row.include?(nil)} deleted_line = FIELD_ROW - @matrix.size deleted_line.times{@matrix.unshift(Array.new(10){nil})} end end Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| @field ||= Field.new(FIELD_ROW, FIELD_COL) @tetrimino ||= Tetrimino.new(game, @field) dx = 0 dy = 0.125 dr = 0 break if Input.keys(:keyboard).include?(:escape) dx = 1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 1}).include?(:right) dx = -1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 1}).include?(:left) dy = 1 if Input.keys(:keyboard, {:duration =>-1, :delay =>-1, :interval => 0}).include?(:down) dr = 1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 3}).include?(:x) dr = 3 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 3}).include?(:z) @tetrimino.rotate(dr) @tetrimino.side_step(dx) @tetrimino.fall(dy) if @tetrimino.state == :dead then @field.import(@tetrimino) @field.clear_lines @tetrimino = nil end game.screen.clear game.screen.draw_tetrimino(@tetrimino) game.screen.draw_field(@field) end
ゲームオーバーを追加する(GitHub差分)
- 一番上まで積み上がっても、永遠にテトリミノを描き続けてしまう状況を何とかしたい。
- ゲームオーバーの演出をする必要がある。
- ついでに、落下速度も少し遅くしてみた。
require "starruby" include StarRuby BLOCK_SIZE = 32 FIELD_ROW = 20 FIELD_COL = 10 FIELD_W = BLOCK_SIZE * FIELD_COL FIELD_H = BLOCK_SIZE * FIELD_ROW RGBS = [[ 0, 255, 255], [255, 255, 0], [ 0, 255, 0], [255, 0, 0], [ 0, 0, 255], [255, 127, 0], [255, 0, 255]] class Texture def draw_block(x, y, rgb) render_rect(x * BLOCK_SIZE + 1, y * BLOCK_SIZE + 1, BLOCK_SIZE - 1, BLOCK_SIZE - 1, Color.new(*rgb)) end def draw_tetrimino(tetrimino) return if !tetrimino tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(tetrimino.x + c , tetrimino.y + r, RGBS[tetrimino.id]) if col == 1 end end end def draw_field(field) return if !field alpha = (field.state == :live ? 255 : 64) field.matrix.each_with_index do |row, r| row.each_with_index do |col, c| draw_block(c, r, RGBS[col] + [alpha]) if col != nil end end end end class Tetrimino attr_reader :id, :state, :x, :y @@minos = [] @@minos << [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]] @@minos << [[1,1], [1,1]] @@minos << [[0,1,1], [1,1,0], [0,0,0]] @@minos << [[1,1,0], [0,1,1], [0,0,0]] @@minos << [[1,0,0], [1,1,1], [0,0,0]] @@minos << [[0,0,1], [1,1,1], [0,0,0]] @@minos << [[0,1,0], [1,1,1], [0,0,0]] def initialize(game, field) @game = game @field = field @id = rand(0..6) @blocks = @@minos[@id] @x, @y, @angle = 3, 0, 0 @state = :live @last_chance = 0 end def blocks(angle = @angle) case angle % 4 when 0 @blocks when 1 @blocks.transpose.map(&:reverse) #右90度回転 when 2 @blocks.reverse.map(&:reverse) #180度回転 when 3 @blocks.transpose.reverse #左90度回転(右270度回転) end end def rotate(dr) if can_move?(0, 0, dr) then @angle += dr end end def side_step(dx) if can_move?(dx, 0, 0) then @x += dx end end def fall(dy) @last_chance -= 1 @y = @y.to_i if dy == 1 if can_move?(0, 1, 0) then @y += dy else case when @last_chance < 0 @last_chance = @game.fps / 2 when @last_chance == 0 @state = :dead end end end def can_move?(dx, dy, dr) x = @x + dx y = @y + dy angle = @angle + dr blocks(angle).each_with_index do |row, r| row.each_with_index do |col, c| if col == 1 then if x + c < 0 || x + c >= FIELD_COL || y + r >= FIELD_ROW || @field.matrix[y + r][x + c] != nil then return false end end end end true end end class Field attr_reader :matrix, :state def initialize(row, col) @matrix = Array.new(row){Array.new(col)} @state = :live end def import(tetrimino) tetrimino.blocks.each_with_index do |row, r| row.each_with_index do |col, c| @matrix[tetrimino.y + r][tetrimino.x + c] = tetrimino.id if col == 1 end end end def clear_lines @matrix.reject!{|row| !row.include?(nil)} deleted_line = FIELD_ROW - @matrix.size deleted_line.times{@matrix.unshift(Array.new(10){nil})} end def freeze @state = :dead end end Game.run(FIELD_W, FIELD_H, :title => "tetris") do |game| @field ||= Field.new(FIELD_ROW, FIELD_COL) @tetrimino ||= Tetrimino.new(game, @field) dx = 0 dy = 0.0625 dr = 0 break if Input.keys(:keyboard).include?(:escape) dx = 1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 1}).include?(:right) dx = -1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 1}).include?(:left) dy = 1 if Input.keys(:keyboard, {:duration =>-1, :delay =>-1, :interval => 0}).include?(:down) dr = 1 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 3}).include?(:x) dr = 3 if Input.keys(:keyboard, {:duration => 1, :delay => 3, :interval => 3}).include?(:z) next if @field.state == :dead @tetrimino.rotate(dr) @tetrimino.side_step(dx) @tetrimino.fall(dy) if @tetrimino.state == :dead then @field.import(@tetrimino) @field.clear_lines @field.freeze if @tetrimino.y <= 0 @tetrimino = nil end game.screen.clear game.screen.draw_tetrimino(@tetrimino) game.screen.draw_field(@field) end
これでひとまず、最小限のテトリスの出来上がり。
- コードの構成
コード区分 | 仕事内容 | 種類 |
---|---|---|
requireとinclude | Star Rubyの機能を取り込む | |
定数定義 | コード全体で有効になる定数の設定 | |
Textureクラス定義 | game.screenへの描画処理を担当 | |
Tetriminoクラス定義 | テトリミノの操作とその状態を保存 | モデル的な役割 |
Fieldクラス定義 | ブロックが積み上がる領域の状態を保存 | モデル的な役割 |
Game.runループ | キー入力に対応する処理の依頼、描画処理の依頼 | コントローラー的な役割 |
- 行列の回転
- Tetriminoクラス blocksメソッド
def blocks(angle = @angle) case angle % 4 when 0 @blocks when 1 @blocks.transpose.map(&:reverse) #右90度回転 when 2 @blocks.reverse.map(&:reverse) #180度回転 when 3 @blocks.transpose.reverse #左90度回転(右270度回転) end end
- テトリミノの移動可能かどうかの判定
- Tetriminoクラス can_move?メソッド
def can_move?(dx, dy, dr) x = @x + dx y = @y + dy angle = @angle + dr blocks(angle).each_with_index do |row, r| row.each_with_index do |col, c| if col == 1 then if x + c < 0 || x + c >= FIELD_COL || y + r >= FIELD_ROW || @field.matrix[y + r][x + c] != nil then return false end end end end true end
参考ページ
悩んだ時に、以下のページが大変参考になりました。感謝です!