はじめに
当記事は、別段先進的な内容もなく、とある英会話スクールで子どもたちにプログラミングを教えた経験から、得た事をメモしています。
というのも、最近スクラッチとかマイクラ系のプログラミングスクールのコンテンツを食い尽くした子ども達が、新天地を求めてご相談いただく事が増えてきた為です。
普段から開発に携わっている方々にとって特に新しい内容はありませんので、「ふーん」程度で読んでいただけると幸いです。
環境と使用Gem
- Ubuntu(WSL) or Mac で動作確認済
- ruby (3.4.3)
- ruby2d (0.12.1)
プログラミング必須化が叫ばれてから随分立ちました
ひょんな頼まれ事からはじまり、キッズプログラミングというカテゴリーに身を置いて随分経過していますが。
何年も経っての感想ですが、子供のプログラミング教育というのは、つくづくカオスでボランティアだなぁと感じています。
しかし、何年も同じ子に関わっていると、その成長が見られるのはとても楽しみでもあります。
ちょこちょこ大人のメンターなどもやっていますが、やはり大人と違ったアプローチが必要で、子供を退屈させないノウハウの確保に随分苦労しました。
結局得た結論としては、「大人と同じ事を楽しい題材でやる」です。
会話が通じる相手ではないので、徹底して体験させる方向ですが、一番の大敵は「面白くない事」です。
子供はweb系全然面白くないんだってさ
最初全くツボがわからなかったのですが、HTML/CSS のような結果が即座に分かるようなものですら全然集中力が持たず、RailsでScaffoldを使ってお手軽に動的なモノを作れたとしても、それを操作する事は全くもって響かないようです。
webアプリは高校生くらいからで全然良いと思いました。
逆に不思議なのは、一見退屈そうな「Windowsから拡張機能使わずにWSLを立ち上げてVSCodeでコーディングする」的な流れは面白いらしく・・・いやほんま謎です。
子供は説明聞きたくないんだってさ
そもそも、子供(1年生〜4年生あたり)には説明的なパートが全く駄目で、5分と持ちません。3分でも駄目です。
基本手を動かしつつ、いつか覚えてくれたらいいなー的に用語を呟く程度でとどめておく事が完成形となりました。
もう清々しく説明を放棄しています。
伸びる子と伸びない子の謎
これも最大級の謎ですが、教室で暴れまくったり、トイレに籠城していたりするような子供が、数年後一気にレベルが上がったり、真面目な子がいつまでも条件式で悩んでいたりと、子供の伸びるタイミングが全く未知数なのです。
下手したら、2年くらい授業の殆どを遊んでいる子達もいるわけです。
でも、だいたい手のかかった記憶がある子の方が最終的に書けたり、プログラミングを楽しめるようになっています。
暴れる事と何か関係があるのか謎は深まるばかりです。
そして行き着く先は
よし、話が退屈ならゲーム作って遊ばせよう!
となるわけですが、教室のPCスペックで何かしようとすると、せいぜい2D系のクラシカルなゲームだろうという事になります。
Scratchはタイピング力向上の面で候補から外してますし、python + turtle 辺りが妥当かとも思いましたが、小学生だと 割とインデントで引っかかる子もいるので、「You!いっそRubyを使っちゃいなよ!」という選択肢になります。
子供にプログラミングを教えるために ruby2d を選定した理由ですが
- ruby2dはWSL上で動かせて音も出る
- ruby2dはOS問わず動くらしい(Winは未検証)
- ruby2dはクラスの概念を教えやすい
- ruby2dは広い範囲のRubyバージョンで問題なく動く
そして一番重要なのが、
Rubyが子供でも情報を調べやすい言語
という事です。
ruby2dのインストール
ここからはハンズオンパートです。
Get strartedのページの通り、
$ gem install ruby2d
でインストール後、
require 'ruby2d'
show
を用意し、実行
$ ruby main.rb
end
すると、ウインドウが立ち上がれば成功です。
Macだと基本Rubyさえ使えれば問題なく動きます。
WSLでもWin11であれば別段手こずることもないですが、
https://www.ruby2d.com/learn/linux/ で、各カーネルで必要なパッケージが記載されていますので、入れ忘れに注意です。
ruby2dでブロック崩しを作りながらクラスの概念を教えてみる
では、実際にruby2dを動かしてみます。
ruby2dは、シンプルなライブラリで、 あまり特殊な機能も実装されておらず、キャラクターに関しては大体以下の3つを覚えておくとどうにかなります。
Circle クラスは、中心を座標とする丸い図形
Rectangle クラスは、左上を基準とする豆腐(正方形はSquareがある)
Sprite クラスは画像を読み込んで左上を基準とするスプライト
です。
その中で今回は Ciecle と Rectangle のみを使います。
他クラス等、詳しく知りたい方は、shapes のページを見てください。
で、これらには移動速度や特殊なフラグなどのアクセサは想定されていないので、一つずつ変数を作るよりも、継承させたクラスを作ると捗る訳ですが、まずはベタに書いてみます。
自機(bar)を作る
ブロック崩しだと、まずは自機として、横長の棒が必要です。
main.rb を作り、
require 'ruby2d'
bar = Rectangle.new(width: 100, height: 15)
bar.x = (Window.width - bar.width) / 2
bar.y = Window.height - bar.height * 3
show
こんな感じで書いて実行
$ ruby main.rb
すると、棒が現れます。
移動は on :key で連続的に監視できますので、下記のコードを追加します。
略
# 追記
bar_speed = 3
on :key do |event|
key = event.key
bar.x -= bar_speed if key == 'left'
bar.x += bar_speed if key == 'right'
end
# ここまで
show
次にbarがウインドからはみ出さないように制限を加えます。
略
on :key do |event|
key = event.key
bar.x -= bar_speed if key == 'left'
bar.x += bar_speed if key == 'right'
# 追記
# 左上が図形の起点なので、左側のリミットは 0 だが、右側のリミットは、画面幅から自身の幅を引いた座標の位置となる。
bar.x = 0 if bar.x < 0
bar.x = Window.width - bar.width if bar.x > Window.width - bar.width
# ここまで
end
show
全体では、
require 'ruby2d'
bar = Rectangle.new(width: 100, height: 15)
bar.x = (Window.width - bar.width) / 2
bar.y = Window.height - bar.height * 3
bar_speed = 3
on :key do |event|
key = event.key
bar.x -= bar_speed if key == 'left'
bar.x += bar_speed if key == 'right'
bar.x = 0 if bar.x < 0
bar.x = Window.width - bar.width if bar.x > Window.width - bar.width
end
show
こんな感じです。
この時点で、全ての実装が終わる頃にはコード数がえげつない事になると想像できますので、まずはファイル自体を分けていきます。
新規に bar.rb というファイルを作成します。
そして、Ractangleを継承するBarクラスを作ります。
クラスとインスタンスの説明は有名な「たいやき」の話とかありますが、子供には通じません。彼らは説明など聞かないのです。
別のファイルに書き出す事だけを説明しておきます。
あとはひたすら真似をしてもらいましょう。
class Bar < Rectangle
end
この中に、 main.rb の
bar = Rectangle.new(width: 100, height: 15)
bar.x = (Window.width - bar.width) / 2
bar.y = Window.height - bar.height * 3
bar_speed = 3
この部分を移植すると、
class Bar < Rectangle
attr_accessor :speed
def initialize
super(width: 100, height: 15)
self.x = (Window.width - self.width) / 2
self.y = Window.height - self.height * 3
self.speed = 3
end
end
このようになりますので、できたBarクラスを main.rb から呼び出して動作するか試します。
attr_accessor の説明も、変数の代わりという程度だけ話しておきます。
当然 main.rbの方も変更が必要になり、変更点は以下となります。
-
requireでbar.rbを呼ぶ必要がある -
barはRectangleクラスからBarクラスに置き換える -
bar_speedの変数はなくなっているので、bar.speedに書き換える
具体的には、下記のようになります。
require 'ruby2d'
require './bar' # bar.rbの読み込み
bar = Bar.new # RectangleクラスからBarクラスに
on :key do |event|
key = event.key
bar.x -= bar.speed if key == 'left' # bar_speed書き換え
bar.x += bar.speed if key == 'right' # bar_speed書き換え
bar.x = 0 if bar.x < 0
bar.x = Window.width - bar.width if bar.x > Window.width - bar.width
end
show
ここまでで、動作に変更がなければ成功です。
ついでに、もう一息いきましょう。
main.rbの on :key 部分のコードを
on :key do |event|
key = event.key
bar.move(key)
end
のようにしたいので、ruby:bar.rb に moveメソッドを作ります。
def move(key)
self.x -= self.speed if key == 'left'
self.x += self.speed if key == 'right'
self.x = 0 if bar.x < 0
self.x = Window.width - self.width if bar.x > Window.width - self.width
end
で、Window.width - self.width のように複数使われており、コードが伸びているような部分に対して、 private メソッドに切り出す話もしますが、privateメソッドの説明も簡単に「クラス内でしか使わないメソッド」というニュアンスのみ伝えています。細かい話はしません。
そして、bar.rb 全体を以下の状態とします。
class Bar < Rectangle
attr_accessor :speed
def initialize
super(width: 100, height: 15)
self.x = (Window.width - self.width) / 2
self.y = Window.height - self.height * 3
self.speed = 3
end
def move(key)
self.x -= self.speed if key == 'left'
self.x += self.speed if key == 'right'
# 変更
self.x = left_limit if self.x < left_limit
self.x = right_limit if self.x > right_limit
# ここまで変更
end
# 追記
private
def left_limit
0
end
def right_limit
Window.width - self.width
end
# ここまで追記
end
main.rbもすっきりさせます。
require 'ruby2d'
require './bar'
bar = Bar.new
on :key do |event|
key = event.key
bar.move(key)
end
show
このような感じで、リファクタリングをしながらコードを書いてもらい話を進めます。
このパターンはあまり嫌がらないようです。リファクタリング後と動きが変わらないのに退屈していないのが謎です。
いきなり飛んで最終形態
進行の話を入れていくと結論までかなり遠いので、ここからは完成形のコードを貼ります。
ファイルは全部で4つ
- main.rb
- bar.rb
- ball.rb
- block.rb
となります。+Gemfile等置いても良いと思います。
require 'ruby2d'
require './bar'
require './ball'
require './block'
bar = Bar.new
ball = Ball.new
blocks = Block.set
update do
ball.move(bar)
bar.refrect(ball)
Block.refrect(ball, blocks)
end
on :key_down do |event|
key = event.key
ball.game_start(key)
end
on :key do |event|
key = event.key
bar.move(key)
end
show
class Bar < Rectangle
attr_accessor :speed
def initialize
super(width: 100, height: 15)
self.x = (Window.width - self.width) / 2
self.y = Window.height - self.height * 3
self.speed = 3
end
def move(key)
self.x -= self.speed if key == 'left'
self.x += self.speed if key == 'right'
self.x = left_limit if self.x < left_limit
self.x = right_limit if self.x > right_limit
end
def refrect(ball)
x_range = ball.x >= self.x && ball.x <= self.x + self.width
y_contain = ball.y + ball.radius >= self.y
if x_range && y_contain
ball.y_flug = false
end
end
private
def width_limit(ball)
self.x
end
def left_limit
0
end
def right_limit
Window.width - self.width
end
end
class Ball < Circle
attr_accessor :speed, :status, :x_flug, :y_flug
def initialize
super(radius: 10)
self.speed = 3
self.status = :follow
self.x_flug = true
self.y_flug = false
end
def move(bar)
case status
when :follow
follow(bar)
when :launch
launch
end
end
def game_start(key)
if key == 'space' && self.status == :follow
self.status = :launch
end
end
private
def follow(bar)
self.x = bar.x + bar.width - self.radius
self.y = bar.y - self.radius
end
def launch
behavior
wall_refrect
bottom_fall
end
def behavior
if self.x_flug
self.x += self.speed
else
self.x -= self.speed
end
if self.y_flug
self.y += self.speed
else
self.y -= self.speed
end
end
def wall_refrect
top_refrect
left_recrect
right_refrect
end
def top_refrect
if self.contains?(self.x, 0)
self.y_flug = true
end
end
def left_recrect
if self.contains?(0, self.y)
self.x_flug = true
end
end
def right_refrect
if self.contains?(Window.width, self.y)
self.x_flug = false
end
end
def bottom_fall
if self.y > Window.height
self.status = :follow
self.x_flug = true
self.y_flug = false
end
end
end
class Block < Rectangle
def initialize
super(width: 60, height: 20)
self.x = 80
self.y = 50
end
def self.set
blocks = []
row_colors = %w(red orange yellow green blue purple)
cols = (0..7)
row_colors.each_with_index do |color, row_index|
cols.each_with_index do |col, col_index|
block = self.new
block.color = color
block.x = block.x + block.width * col_index
block.y = block.y + block.height * row_index
block.width -= 1
block.height -= 1
blocks << block
end
end
blocks
end
def self.refrect(ball, blocks)
blocks.each do |block|
block.refrect(ball, blocks)
end
end
def refrect(ball, blocks)
bottom_refrect(ball, blocks)
top_refrect(ball, blocks)
left_refrect(ball, blocks)
right_refrect(ball, blocks)
end
def self.remove(blocks)
blocks.each do |block|
block.remove
end
end
private
def bottom_refrect(ball, blocks)
if ball.contains?(ball.x, self.y + self.height) && width_include?(ball)
ball.y_flug = true
self.remove
blocks.delete(self)
true
end
end
def top_refrect(ball, blocks)
if ball.contains?(ball.x, self.y) && width_include?(ball)
ball.y_flug = false
self.remove
blocks.delete(self)
true
end
end
def left_refrect(ball, blocks)
if ball.contains?(self.x, ball.y) && height_include?(ball)
ball.x_flug = false
self.remove
blocks.delete(self)
true
end
end
def right_refrect(ball, blocks)
if ball.contains?(self.x + self.width, ball.y) && height_include?(ball)
ball.x_flug = true
self.remove
blocks.delete(self)
true
end
end
def width_include?(ball)
self.x <= ball.x && self.x + self.width >= ball.x
end
def height_include?(ball)
self.y <= ball.y && self.y + self.height >= ball.y
end
end
動かすと、こんな感じです。
これでも最低限遊ぶ事が出来る事と、書き進める上で
- クラス
- インスタンス
- requireとパスの指定
- インスタンスメソッド
- クラスメソッド
- メソッドの戻り値
- privateメソッド
の説明を盛り込む事が出来ています。
実際には、このパターンの前に1ファイルで完結するゲームを1〜2種類経験して貰っていたり、+αの要素で、
- ライフ機能
- ゲームオーバー画面
- コンティニュー機能
- リスタート機能
- スコア機能
だったりを足して作り込んでいきます。
後は適当に遊ばせていると、色々な数値を変更したり、スピードを上げたりといろいろ遊んでくれます。
すんごい速度にしたり、ブロックを死ぬほど増やしたり、
子供の想像力は素晴らしいと思います。
また、次のステップとして、スプライトを使ったシューティングゲームなども進めていますが、フォルダ構造をフレームワークっぽくしてモデルやモジュール、config系のファイルなども作ったり、rakeでコマンド起動化したり、少しずつRailsっぽい構造に寄せるよう仕向けています。
きっと、これが役に立つはずだと信じて・・・。
最後に
ruby2d自体は2年ほどメンテナンスが止まっています。
Platform supportの情報も当然古いので、現状書いたプログラムの吐き出し方法をどうするか?などの問題も抱えています。
現状はゲームを作る事がゴールなので、これでも良いのですが、そろそろ次なるネタを考えねばなりません。
ここまで御覧いただきありがとうございます。良いクリスマスを。


