talosのプログラミング教室

【Ruby】懐かしのゲームを作ってオブジェクト指向をなんとなく学ぶ

スポンサーリンク

はじめに

こんにちは。talosです。

今回はcursesを使って懐かしのゲームを作ります。

ただ作るだけでなく、オブジェクト指向プログラミングを意識して作ろうと思います。


cursesは端末の制御を可能にするライブラリです。

これを使って、こんな感じの「パッ〇マン」のようなゲームを作っていきます。

f:id:talosta:20190612000845g:plain

コードはGitHubにもあげてあります。

ダウンロードしたい方はREADMEに従って行ってください。

オブジェクト指向とは

オブジェクト指向とは、コンピュータプログラムの設計や実装についての考え方の一つで、互いに密接に関連するデータと手続き(処理手順)をオブジェクト(object)と呼ばれる一つのまとまりとして定義し、様々なオブジェクトを組み合わせて関連性や相互作用を記述していくことによりシステム全体を構築していく手法。

引用:オブジェクト指向(OO)とは - IT用語辞典 e-Words


らしいです。

よくわからないですね。

例を挙げて説明しましょう。

f:id:talosta:20190612134628p:plain

「消防車」と「トラック」の「持っているもの」と「できること」をまとめました。

赤い文字の部分は両方に共通しています。

この表をそのままプログラムにしようとすると、同じようなものを二つ書かなくてはなりません。

次に、表に「車」という項目を入れます。

f:id:talosta:20190612135953p:plain

「車」という項目を入れたことで、共通の部分を省略できるようになりました

これがだいたいのオブジェクト指向の考え方です。

詳しい説明はプログラムの説明をしながらしていこうと思います。

cursesのメソッド

まず、今回使用したcursesのメソッドを紹介します。

init_screen()

スクリーンの初期化。

curs_set(int)

カーソルの表示設定。0ならば非表示、1ならば表示。

noecho()

キーボードから入力された文字を表示しないように設定。

cbreak()

1文字入力を受け取るたびに反応するように設定。

stdscr.keypad(bool)

キーパッド(方向キーなど)の設定。trueならば使用可能。falseならば使用不可。

setpos(double, double)

カーソルの座標を設定。第一引数がy座標、第二引数がx座標

addstr(string)

文字列を表示。

refresh

端末に出力。

getch()

キーボードからの文字の入力を受け付ける。

clear()

画面をまっさらにする。

プログラムを解説

GitHubと並べると見やすいと思います。

monster.rb

Monsterクラスを定義しています。

ここでは「モンスター」という抽象的な定義をし、後でモンスター1、モンスター2…とインスタンスを生成できるようにしています。

def appear
    random = Random.new
    @monster_pos = [random.rand(lines - 3), random.rand(cols - 1)]
    setpos(@monster_pos[0], @monster_pos[1])
    addstr('M')
end

appearメソッドは、乱数で指定した座標にモンスターを出現させます。

Rubyでは

random = Random.new
randam.rand(num)

で1~numまでの乱数を生成できます。

ここで注意してほしいのは、setpos(y, x)関数は第一引数がy座標ということです。

数学とかの書き方とは違うので気をつけてください。

ちなみにcursesの座標軸は以下のようになっています。

f:id:talosta:20190612153124p:plain

3行目の

@monster_pos = [random.rand(lines - 3), random.rand(cols - 1)]

では、端末の下から2行はタイマーなどを表示するため、(lines-1-2)で(lines-3)としています。

def move(hunter_pos)
    y = 0
    x = 0

    scalar = Math.sqrt((hunter_pos[0] - @monster_pos[0])**2 + (hunter_pos[1] - @monster_pos[1])**2)
    y = (hunter_pos[0] - @monster_pos[0]) / scalar
    x = (hunter_pos[1] - @monster_pos[1]) / scalar

    setpos(@monster_pos[0], @monster_pos[1])
    addstr(' ')

    @monster_pos[0] += y
    @monster_pos[1] += x
    setpos(@monster_pos[0], @monster_pos[1])
    addstr('M')
end

moveメソッドは、ハンターを追うようにモンスターを移動させます。

モンスターがハンターを追うようにするには、モンスターからハンターへの単位ベクトルを計算し、元のモンスターの座標に足してやればいいでしょう。

単位ベクトルは次のように計算します。

1. モンスターからハンターへのベクトルを計算

f:id:talosta:20190612155402p:plain

2. ベクトルをベクトルの大きさで割る

f:id:talosta:20190612160726p:plain

これが単位ベクトルです。

def catch_hunter(hunter_pos)
    if (@monster_pos[0] - hunter_pos[0]).abs < 0.5 && (@monster_pos[1] - hunter_pos[1]).abs < 0.5
        return true
    else
        return false
    end
end

catch_hunterメソッドは、ハンターを捕まえたかどうかを判します。

cursesの座標はdouble型になっていて、x座標、y座標の差が0のときのみtrueを返すようにするとなかなか捕まらないので、0.5にしています。

huter.rb

Hunterクラスを定義しています。

set_centerメソッドは、端末の中心にハンターを配置します。

特に難しいことはやっていないので、解説は省略します。

def move(key)
    y = 0
    x = 0

    case(key)
    when KEY_UP
        if @hunter_pos[0] > 0
            y -= 1
        else
            y += lines - 3
        end
    when KEY_DOWN
        if @hunter_pos[0] < lines - 3
            y += 1
        else
            y -= lines - 3
        end
    when KEY_LEFT
        if @hunter_pos[1] > 0
            x -= 1
        else
            x += cols - 1
        end
    when KEY_RIGHT
        if @hunter_pos[1] < cols - 1
            x += 1
        else
            x -= cols - 1
        end
    end

    setpos(@hunter_pos[0], @hunter_pos[1])
    addstr(' ')

    @hunter_pos[0] += y
    @hunter_pos[1] += x
    setpos(@hunter_pos[0], @hunter_pos[1])
    addstr('@')

    @hunter_pos
end

moveメソッドは、キーボードから入力を受け取ってハンターを移動させます。

getch()で受け取った文字を引数として受け取り、上下左右のどれを入力したかで分岐します。

(実際にはgetch()で受け取った文字はint型に変換されます)

画面の端に到達したら、反対側から出てくるようにしています。

item.rb

Itemクラスを定義しています。

この後紹介するTreasureクラスとMedicineクラスの親クラスです。

オブジェクト指向では継承という仕組みがあります。

継承される側のクラスを親クラスといいます。

一方、継承する側のクラスを子クラスといい、親クラスの変数やメソッドを引き継げます。

def initialize
    @flg = 0
    @item_pos = [0, 0]
    @mark = ''
    @p = 0
end

これはinitializeメソッドといい、オブジェクトが生成されたときに一度だけ呼ばれます。

JavaやC++でいうコンストラクタと同じです。

def reset
    @flg = 0
end

resetメソッドは、一度出現したアイテムをもう一度出現できるようにします。

アイテムは取られると@flgは1になります。

@flgが0の時にしかアイテムが出現しないようにこのあと説明するappearメソッドで設定しているので、@flgを0にしてあげます。

def appear
    random = Random.new
    
    if @flg == 0 && random.rand(@p) == 1
        @item_pos = [random.rand(lines - 1), random.rand(cols - 1)]
        @flg = 1
    end
        
    if @flg == 1
        setpos(@item_pos[0], @item_pos[1])
        addstr(@mark)
    end
end

appearメソッドは、@flgが0(アイテムが出現していない)ときは、1/@pの確率でアイテムを出現させます。

しかし、モンスターに踏まれると(実際にはあるのに)消えてしまうので、@flgが1のときも画面に出力し続けます。


gottenメソッドは、アイテムがハンターに取られたか判定します。

難しいことはしていないので解説は省略します。

treasure.rb

Treasureクラスを定義しています。

class Treasure < Item

でItemクラスを継承しています。

これによってItemクラスの変数やメソッドを使用できるようになります。

def initialize
    super
    @mark = '*'
    @p = 20
end

initializeメソッドは親クラスのItemクラスにもありました。

それをもう一度書いています。

このように、親クラスのメソッドを子クラスで定義し直すことをオーバーライドといいます。

オーバーライドすると親クラスのメソッドに上書きしてしまうので、superで親クラスのメソッドを呼び出しています。

def gotten(hunter_pos)
   if (@item_pos[0] - hunter_pos[0]).abs < 1 && (@item_pos[1] - hunter_pos[1]).abs < 1 && @flg == 1
        @flg = -1
        return true
    else
       return false
    end
end

gottenメソッドもオーバーライドしています。

親クラスではアイテムが取られたときに、@flgを0にしていました。

でも、それだとアイテムがまた出現してしまいます。

「宝」は決まった個数だけ出現するようにしたいので、取られたら@flgを-1にします。

medicine.rb

出力するマークと出現確率を決めているだけです。

main.rb

長くなっていますが、難しいことはやっていません。

コメントを見れば十分理解できると思うので省略します。


おわりに

今回は初めて手作りのプログラムを使ってみました。

今のままではかなり難しいので、新たな「アイテム」を作って易しくしてみてください。

もしわからないところやおかしなところがあればコメントお願いします。