シンプルな追いかけっこから始めるゲームメカニクス


はじめに

今日はHot Soup Processor(HSP)でシンプルな「追いかけっこ」ゲームを作ってみたいと思います。
敵(CPU)とプレイヤーが追いかけっこをして、捕まったら終わり…というルールです。
クラシカルで単純すぎるゲームなので、何だこれ? と思われるかもしれませんが、ゲームを作りたいという人や、もう作り始めているという人にも、シンプルだからこそ学べることもあると思います。
HSPを作った簡単なサンプルとしても参考になれば嬉しいです。
HSPのインストール方法や、細かい仕様などはこちらを参照してください。

Hot Soup Processor(HSP)って何? 実際に使ってみる
https://qiita.com/onionsoftware/items/630a93355de3c8140255

画面を描画する

まずは、追いかけっこのプレイヤーから表示させてみましょう。

この記事で使用しているサンプルスクリプトは以下のURLからダウンロードすることができます。
http://www.onionsoft.net/hsp/file/sample_alien.zip

alien0.hsp
    wx=800:wy=600   ; 画面サイズ
    screen 0,wx,wy

    mx=400:my=300   ; プレイヤーの座標
    msp=4       ; プレイヤーの移動スピード
    ms=20       ; 円のサイズ

    frame=0
*main
    title "追いかけっこ["+frame+"]"

    stick key,15                ; キー入力
    if key&1 : mx=limit( mx-msp, 0, wx )    ; 左移動
    if key&4 : mx=limit( mx+msp, 0, wx )    ; 右移動
    if key&2 : my=limit( my-msp, 0, wy )    ; 上移動
    if key&8 : my=limit( my+msp, 0, wy )    ; 下移動

    redraw 0                ; 画面更新開始
    color 0,0,64:boxf           ; 背景を消す
    color 0,255,255
    circle mx-ms,my-ms,mx+ms,my+ms      ; プレイヤーの円を描く

    redraw 1                ; 画面更新終わり
    await 1000/60               ; 1/60秒で画面を更新
    frame++                 ; フレームを進める
    goto *main              ; 繰り返す

HSPがインストールされている環境であれば、このスクリプトをそのままスクリプトエディタにコピペして[F5]キーを押せば実行できるはずです。
まずは、変数mx,myにプレイヤーのX,Y座標を保持して、それをキー入力(カーソルキー)で動かす単純なスクリプトです。
今回は、プレイヤーも敵も塗りつぶしの円を使用しています。もちろん、画像を使用してキャラクターを動かすことも可能です。
HSPのお約束として、「redraw 0」のある行から画面の更新を開始して、「redraw 1」で一気に画面を更新します。
その後、await命令により60分の1秒の速さでループさせるスクリプトになっています。
背景を塗りつぶした後、プレイヤーの円をcircle命令で描画して、1コマ分の画面を作っています。1コマ表示するたびに、変数frameが0,1,2…とカウントアップしていきます。これがタイトルバーに表示され、スコアの代わりになっています。
命令の意味や使い方がわからない場合は、キーワードにカーソルを合わせて[F1]を押すとヘルプが表示されますので、参考にしてみてください。
スクリプトの中で、ちょっと見慣れないlimit関数があります。
これは、次のように使います。

変数 = limit( もとの数, 最小値, 最大値 )

「もとの数」を最小値~最大値の範囲内にまとめる関数になります。これはつまり、

変数 = もとの数
if 変数 > 最大値 { 変数 = 最大値 }
if 変数 < 最小値 { 変数 = 最小値 }

と同じことです。うまく使用することで、if命令の数を抑えることができます。

敵を表示する

さて、次に敵を表示して、自分を追いかけてくるようにしましょう。
敵の座標をex,eyという変数で管理して、後はプレイヤーを追いかけるようにします。

alien1.hsp
    wx=800:wy=600   ; 画面サイズ
    screen 0,wx,wy

    mx=400:my=300   ; プレイヤーの座標
    msp=4       ; プレイヤーの移動スピード
    ms=20       ; 円のサイズ
    ex=50:ey=50 ; 敵の座標

    frame=0
*main
    title "追いかけっこ["+frame+"]"

    stick key,15                ; キー入力
    if key&1 : mx=limit( mx-msp, 0, wx )    ; 左移動
    if key&4 : mx=limit( mx+msp, 0, wx )    ; 右移動
    if key&2 : my=limit( my-msp, 0, wy )    ; 上移動
    if key&8 : my=limit( my+msp, 0, wy )    ; 下移動

    dist=abs(ex-mx)+abs(ey-my)      ; 敵との距離を簡易的に求める
    if dist<(ms*2) : goto *gameover     ; 衝突していたら*gameoverへ

    ex=alien(ex,mx) ; 敵のX座標を動かす
    ey=alien(ey,my) ; 敵のY座標を動かす

    redraw 0                ; 画面更新開始
    color 0,0,64:boxf           ; 背景を消す
    color 0,255,255
    circle mx-ms,my-ms,mx+ms,my+ms      ; プレイヤーの円を描く
    color 255,0,0
    circle ex-ms,ey-ms,ex+ms,ey+ms      ; 敵の円を描く

    redraw 1                ; 画面更新終わり
    await 1000/60               ; 1/60秒で画面を更新
    frame++                 ; フレームを進める
    goto *main              ; 繰り返す

*gameover
    ;   つかまった時の処理
    font msgothic,120
    color 255,255,255:pos 50,50
    mes "つかまった!"
    stop

#defcfunc alien int value, int value2

    ;   敵が追いかけてくる関数
    esp=2                   ; 敵の移動スピード
    res=limit( value2-value, -esp, esp )
    return value+res

敵がプレイヤーを追いかける時は、どうすればいいでしょうか?
単純に、敵のX座標とプレイヤーのX座標を比較して、敵の座標が大きな値であれば少なく、敵の座標が小さい値ならば大きくすればX方向に近づいてくるはずです。同じことをY座標についても行います。
今回は、座標を近づけるためにalien関数というものを使用しました。
X座標、Y座標ごとにalien関数によって移動後の値を取得しています。

ex=alien(ex,mx) ; 敵のX座標を動かす
ey=alien(ey,my) ; 敵のY座標を動かす

alien関数は次のように使います。

新しい座標 = alien( 現在の座標, 追いかける座標 )

こんな関数聞いたことないですよね。これは、スクリプトの中で定義されています。
HSPでは、よく関数が作れないというイメージを持つ人が多いのですが、#defcfunc命令によって定義することができます。(命令を定義する場合は、#deffunc命令を使います。)

#defcfunc alien int value, int value2

    ;   敵が追いかけてくる関数
    esp=2                   ; 敵の移動スピード
    res=limit( value2-value, -esp, esp )
    return value+res

alien関数が返すのは、現在の座標と追いかける(プレイヤーの)座標を比べて、移動させた後の座標です。
戻り値は、return命令の後にパラメーターとして記述します。
敵につかまったかどうかの当たり判定は、簡易的にプレイヤーと敵の距離を計算したものを使用しています。

dist=abs(ex-mx)+abs(ey-my)      ; 敵との距離を簡易的に求める
if dist<(ms*2) : goto *gameover     ; 衝突していたら*gameoverへ

距離がある程度まで近づくと「つかまった!」という文字が出てゲームが終了します。
これで、追いかけっこの基本的なシステムができました。

敵のAIを考える

このままでは敵の動きが単純すぎて、すぐ飽きてしまいますよね。
こういったゲームで、敵が自立して移動するようなものは、まとめて「ゲームAI」と呼ばれます。昨今の機械学習のような頭のいいAIではなく、ゲームの内容に特化された人の手で作られたAIです。
昔から、「ゲームAI」の部分は、特にCPUの性能が低かった時代に、プログラマーが職人的な手腕を発揮していた分野です。
シンプルなゲームであっても、ちょっとした味付けで内容が大きく変化し、面白さにつながってくる部分でもあります。
先ほどのalien関数を少し変更して、少しトリッキーな移動にしてみましょう。

#defcfunc alien int value, int value2

    ;   敵が追いかけてくる関数
    if (frame\60)<20 {          ; 60で割った余りが20より小さい場合
        return value
    }
    esp=4       ; 敵の移動スピード
    res=limit( value2-value, -esp, esp )
    return value+res

この例では、60コマのフレームのうち最初の20コマだけは敵が止まるようになっています。
これだけの変更ですが、ちょっと何かを考えているような間を与えることができました。プレイヤーも、最初は少し戸惑うようになると思います。
このままだと、動きが極端なので、時間ごとにゆっくりと動きのスピードが変化するように修正してみましょう。

#defcfunc alien int value, int value2

    ;   敵が追いかけてくる関数
    esp=(frame\60)/13+1         ; 敵の移動スピード
    res=limit( value2-value, -esp, esp )
    return value+res

このようにalien関数の中身を変えるだけでも、結構動きの印象は変わってきます。
逆に考えると、座標を追いかけるという動きの中で、どれだけバリエーションを持たせられるか、ということがゲームの基本的な印象にも繋がってくるわけです。
これは3Dのゲームであっても同様で、どんなに画面がきれいだったり、派手なエフェクトがあったとしても、プレイヤーと敵の関係性や、プレイヤーがやることが一辺倒だと、すぐに飽きられてしまいます。
特にアクションゲームのようなものでは、少ない労力で、多くのバリエーションを得ることのできるアルゴリズムを生み出すことが求められます。
それは、「ゲームAI」の中でプログラマーの手腕が問われる部分でもあり、ゲームの楽しさに直結する大切な要素だと思います。

慣性と物理法則

さらにスクリプトに慣性を追加してみました。
プレイヤーと敵の動きがより自然なものになり、プレイ感覚も大きく変わってきます。

alien4.hsp
    wx=800:wy=600   ; 画面サイズ
    screen 0,wx,wy

    mx=400.0:my=300.0   ; プレイヤーの座標
    px=0.0:py=0.0       ; プレイヤーの慣性
    msp=2           ; プレイヤーの移動スピード
    mspmax=6        ; プレイヤーの最大移動スピード
    friction=0.96       ; 摩擦係数
    ms=20           ; 円のサイズ
    ex=50.0:ey=50.0     ; 敵の座標
    epx=0.0:epy=0.0     ; 敵の慣性
    espmax=10       ; 敵の最大移動スピード

    frame=0
*main
    title "追いかけっこ["+frame+"]"

    px=px*friction : py=py*friction

    stick key,15                ; キー入力
    if key&1 : px=limitf( px-msp, -mspmax, mspmax ) ; 左移動
    if key&4 : px=limitf( px+msp, -mspmax, mspmax ) ; 右移動
    if key&2 : py=limitf( py-msp, -mspmax, mspmax ) ; 上移動
    if key&8 : py=limitf( py+msp, -mspmax, mspmax ) ; 下移動
    mx=limitf( mx+px, 0, wx )
    my=limitf( my+py, 0, wy )

    dist=abs(ex-mx)+abs(ey-my)      ; 敵との距離を簡易的に求める
    if dist<(ms*2) : goto *gameover     ; 衝突していたら*gameoverへ

    epx=alien(epx,ex,mx)
    epy=alien(epy,ey,my)
    ex=limitf( ex+epx, 0, wx )      ; 敵のX座標を動かす
    ey=limitf( ey+epy, 0, wy )      ; 敵のY座標を動かす

    redraw 0                ; 画面更新開始
    color 0,0,64:boxf           ; 背景を消す
    color 0,255,255
    circle mx-ms,my-ms,mx+ms,my+ms      ; プレイヤーの円を描く
    color 255,0,0
    circle ex-ms,ey-ms,ex+ms,ey+ms      ; 敵の円を描く

    redraw 1                ; 画面更新終わり
    await 1000/60               ; 1/60秒で画面を更新
    frame++                 ; フレームを進める
    goto *main              ; 繰り返す

*gameover
    ;   つかまった時の処理
    font msgothic,120
    color 255,255,255:pos 50,50
    mes "つかまった!"
    stop

#defcfunc alien double base, double value, double value2

    ;   敵が追いかけてくる関数
    esp=limitf( value2-value, -0.5, 0.5 )
    res=limitf( base+esp, -espmax, espmax ) ; 敵の移動スピード
    return res

今回は、いままでのデジタルっぽい動きから、アナログっぽいものに変化しました。
もちろん、デジタルっぽい動きにも魅力があり、そこでしか出せない感覚やゲーム性があると思います。
最近のゲームエンジンでは、手軽に物理エンジンを使用することができ、慣性や物理法則に則った深みのある動きを再現できるようになりました。
HSPでも、OBAQと呼ばれる独自の2D物理エンジンや、3D用にBullet Physicsを利用することができます。
それは、ゲームの中で正しい物理法則で動いているから楽しいのではなく、あくまでもゲームの印象とバリエーションを得るためのものだと思います。
単純な動きの中で、いかに工夫するか、印象的なものにするか…ということを、シンプルなプログラムを作りながら考えてみるのも大切ではないでしょうか。

最後に、コレ大切なことですが、3年ぶりのHSPオフィシャル本の新刊、「HSPでつくるはじめてのプログラミング HSP3.5+3Dish入門」が秀和システムより発売中です。
インストールが簡単ですぐにプログラミングを学べる環境として、初めての入門者やワークショップにも最適な1冊となっていますので、ぜひともチェックしてみてくださいね。

HSPでつくるはじめてのプログラミング HSP3.5+3Dish入門
http://www.onionsoft.net/wp/archives/2195