2014年12月18日木曜日

近況

今年の12月17日でロックマンが27周年だとか。スマブラにもロックマンが参戦したからマリカー8入り Wii U (白) を買って、スマブラも買ってしまったよ。それだけで Wii U のハードディスクはほとんどいっぱいになってしまった。なんてことだ。 マリカーは第一作は友人の家でよくプレイしていたが、それ以外は64版を1回やっただけだった。当然のことながらビジュアル面はものすごく進化してまったく別物だが、なんとなく走っているだけでも楽しいのは変わらない。スマブラは実は1作もやったことがなかったのだが、ロックマン出てるから買う以外しようがないね。

さて、そのロックマンはストリートファイター×鉄拳に出てきたような北米版のMEGAなおっさんではなく、欧州版の青年でもなく、日本版の少年で3Dモデルの出来栄えも素晴らしく、SEも本家から輸入しているものが含まれているので懐古厨も安心のクオリティ。粘土っぽい質感のイエローデビルが四角いブロックに分裂して移動し人型を再構成するのも雅である。

ゲームパートはどれをやってもいいが、桜井さんがやっているようにリアル8人集合で大乱闘するのがもっとも超エキサイティン!だろう。非リア充には厳しいが、ぐぬぬ。あと、サウンドテストでなつかしいあの曲 (どれ?) のオリジナル版やアレンジ版を聴くことができ、「もう BGM 聞いてるだけでいいんじゃないかな」という気分になるくらい素晴らしい。FE外伝推しの私としてはミラの加護とともに (セリカマップ1) の岩垂さんのアレンジ版トカ垂涎モノであった。

しかし、実際にはマリカー8もスマブラも半ば積みゲーと化しており、最近は『電装天使ヴァルフォース』という『電脳戦機バーチャロン』(以下バーチャロン)のクローンゲームをやっている。仮想ゲームセンターに接続して快適に対戦または観戦することができ、5年くらい前に出たゲームにもかかわらず毎夜の数人規模の野試合や毎月の10数人規模の大会が開かれている。

実は今年になってから有料の同人ゲームに手を出すようになり、Macbook Air 上の Parallels Desktop で Windows 8.1 仮想マシン走らせ、[erka:es] によるロックマンクローン作品である『Rosenkreutzstilette』、『Rosenkreutzstilette Freudenstachel』などをやっていた。過去にほんのちょろっとやって高速機動戦闘がいい感じだったバーチャロンについてクローンを探したらヴァルフォースが見つかって体験版を試したものの、仮想環境のおかげか体験版が(パッチが存在するにもかかわらず見つからない) エラーで正常に動作せずであったが、試行錯誤の末、ゲームのフォルダーをルートに配置したらなぜか動いたのでポチった。仮想マシンでゲームやろうとして起動できなくて投げた人がいるのかどうかは知らないが参考になれば。

つまり、その、アレだ。制作の進捗は… (文章はここで途切れている)

2014年11月4日火曜日

敵を発生させるスクリプト

またも間が空いてしまった。ブログの記事に時間を割くよりは制作を優先したらこうなった。今回は Nostalgia のマップに配置したタイルの ID に対応する敵をタイルの位置に発生させるスクリプトを晒す。マップコンポーネントを追加したゲームオブジェクトに以下のスクリプトを追加して使う。例によって品質は KS だ。

using UnityEngine;
using System.Collections;
using Nostalgia;

public class EnemySpawner : MonoBehaviour
{
  [SerializeField] private GameObject[] enemyPrefabs;
  private Map enemySpawnerMap;
  private Vector3[] tilePositions;
  private int[] tileIds;
  private Cell[] cells;

  void Start ()
  {
    enemySpawnerMap = GetComponent<Map> ();
    cells = enemySpawnerMap.cells;
    tilePositions = new Vector3[cells.Length];
    tileIds = new int[cells.Length];
    for (int i = 0; i < cells.Length; i++) {
      tilePositions[i] = enemySpawnerMap.MapPointToWorldPoint (cells[i].position);
      tileIds [i] = cells [i].tileID;
    }
    StartCoroutine (SpawnEnemy ());
  }

  IEnumerator SpawnEnemy ()
  {
    while (true) {
      for (int i = 0; i < cells.Length; i++) {
        bool b1 = true; // 待ち時間以外の敵インスタンスの出現条件を書く
        bool b2 = true; // 敵インスタンスの向き変更条件を書く
        if (b1) {
          GameObject enemyInstance = (GameObject)Instantiate (enemyPrefabs [tileIds[i]], tilePositions[i], (b2 ? Quaternion.identity : Quaternion.Euler (0f, 180f, 0f)));
        }
      }
      yield return new WaitForSeconds (5f); // 待ち時間
    }
  }
}

敵の種類だけ異なるタイルを作るわけだが、全部透明の同じグラフィックにするとインスペクターなしで区別できない。これでは扱いづらいのでどうしたものか。それにしても Tile Component の使いどころがわからんな〜。

2014年10月14日火曜日

ルビを表示する

制作が止まっているわけではないのだが、また長らく間があいてしまった。ドット絵を打ったり。プログラムを書いたりしていた。

テキストにルビを振りつつ 1 文字ずつ表示するのを作った。基本的なアイデアは大きさと間隔を調整した GUI Text を並べ、表示速度を調整するだけ。テキストは XML 文書というか以下のような HTML 文書から読みこんで使う。指定したノード番号の dl 要素に含まれる、dt 要素をルビ 1 ページ, dd 要素を本文 1 ページとして表示する。


<html>
<head><title>タイトル</title></head>
<body>
<dl>
<dt>ほんじつ  せいてん     
ほんじつ  せいてん    </dt>
<dd>本日は晴天なり
本日は晴天なり</dd>
<dt>じゅ  げ む      じゅ  げ む
  ご  こう    す   き </dt>
<dd>寿限無   寿限無 
五劫の擦り切れ </dd>
<dt>かいじゃ  り  すいぎょ  
すいぎょうまつ   うんらいまつ  ふうらいまつ</dt>
<dd>海砂利水魚の
水 行 末 雲来末 風来末</dd>
<dt>  く   ね  ところ  す  ところ
やぶ  こう  じ    やぶこう  じ </dt>
<dd>食う寝る処に住む処
藪ら柑子の藪柑子   </dd>
<dt>            
                      
                            </dt>
<dd>パイポパイポ
パイポのシューリンガン
シューリンガンのグーリンダイ</dd>
<dt>                          
                   ちょうきゅうめい         ちょう すけ</dt>
<dd>グーリンダイのポンポコピーの
ポンポコナーの  長 久 命の 長  助</dd>
</dl>
</body>
</html>

KS コードは以下の通り。Canvas を 2 つ (本文用、ルビ用) 用意して、currentTextType をそれぞれ TextType.Body, TextType.Ruby としておき、それぞれの recoveryTime, fontSizeFactor を調整する。例えば、本文の recoveryTime を ルビの recoveryTime の 2 倍にし、本文のフォントサイズがルビのフォントサイズの 2 倍になるように fontSizeFactor を調整する。あとは、それぞれの LoadSelectedList (int i)を呼べばいいんじゃないかな。


using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Xml;

public class DisplayLetterByLetter : MonoBehaviour
{

  Text textarea;
  float timer;
 // 次の文字が表示されるまでの間隔。適当に調整
  public float recoveryTime = 0.1f;

  XmlDocument xmlDocument;

  // 読み込む XML (TextAsset として)
  public TextAsset xmlTextAsset;
  XmlNodeList[] DefinitionLists;
  XmlNodeList currentList;
  int currentPage;
  string fullText;
  int fullTextLength;

  // true のときテキストを表示
  public bool displayingText = false;

  int numberOfLettersDisplayed;

  // 全文字表示してから自動ページ送りするまでの秒数
  float autopagingTime = -2;
  bool paging;

 // 自動ページ送り
  public bool autopaging;


  public enum textType
  {
    Body,
    Ruby,
  };

  // 本文 Body, ルビ Ruby
  public textType currentTextType = textType.Body;

  // 画面の高さに対する割合
  public float fontSizeRatioToScreenHeight = 0.01875f;

  void Start ()
  {
    textarea = GetComponent ();
    textarea.fontSize = (int)(Screen.currentResolution.height * fontSizeRatioToScreenHeight);

    xmlDocument = new XmlDocument ();
    xmlDocument.LoadXml (xmlTextAsset.text);
    XmlNodeList xmlNodeList = xmlDocument.SelectNodes ("/html/body/dl");
    int numberOfDefinitionLists = xmlNodeList.Count;
    DefinitionLists = new XmlNodeList [numberOfDefinitionLists];
    for (int j = 1; j <= numberOfDefinitionLists; j++) {
      DefinitionLists [j - 1]
      = currentTextType == textType.Body ?
        xmlDocument.SelectNodes ("/html/body/dl[" + j + "]/dd") :
        xmlDocument.SelectNodes ("/html/body/dl[" + j + "]/dt");
    }
  }


  public void LoadSelectedList (int i)
  {
    currentList = DefinitionLists [i];
    currentPage = 0;
    LoadCurrentPage ();
  }
  //
  void LoadCurrentPage ()
  {
    fullText = currentList.Item (currentPage).InnerXml;
    fullTextLength = fullText.Length;
    timer = recoveryTime * (currentTextType == textType.Body ? 1 : 2);
    numberOfLettersDisplayed = 0;
  }
  void Update ()
  {
    // jInput を使っていない人は普通に Input.GetButtonDown ("お好みのボタン") 
    paging = jInput.GetButtonDown (Mapper.InputArray [5]);
  }
  void FixedUpdate ()
  {
    if (displayingText) {
      Display (autopaging);
    }
  }

  void Display (bool autopaging)
  {
    textarea.text = fullText.Substring (0, numberOfLettersDisplayed);
    timer -= Time.deltaTime;
    if (numberOfLettersDisplayed < fullTextLength) {
      if (paging) {
        timer = 0;
        paging = false;
      }
      if (timer < 0) {
        numberOfLettersDisplayed++;
        timer += recoveryTime;
      }
    } else {
      if (paging) {
        OnPageEnd ();
        paging = false;
      } else if (timer <= autopagingTime && autopaging) {
        OnPageEnd ();
      }
    }
  }

  void OnPageEnd ()
  {
    if (currentList [currentPage + 1] != null) {
      currentPage++;
      LoadCurrentPage ();
    } else {
      CloseMessageWindow ();
    }
  }

  void CloseMessageWindow ()
  {
    displayingText = false;
  }

}

再生の様子を動画でご覧あれ。表示位置とタイミングは半角スペースやら全角スペースやら改行やらを使って調整するという力業なのでいろいろ微妙。力こそパワーの精神でひとつヨロシク。

ルビの表示 displaying ruby text from hide behind on Vimeo.

各文字の位置から表示するタイミングを計算すればもう少しマシになるんだろうかねぇ。

2014年10月5日日曜日

デザイン変更。

しばらく間が開いてしまったが、ドット絵素材をチマチマと作成していた。全然進んだ気がしない。

夜中に真っ白な背景だと目に堪えるので、背景色を変更した。ちょっとはマシな配色になっただろうか。

ドット絵をやっていると滅入るので、気分転換にフリーの音楽素材を探して整理したりもしていた。ドメイン名/(必要に応じて)ユーザー名/ みたいな感じで分けた。ジングルというかサウンドロゴというかがなかなかイメージに合うものが見つけられない。部分的に切り取るとかも考えた方がいいかも知れない。チップチューン系だと素材探しが大変で、そういうのでないやつだと自作のドット絵がショボすぎて負けるような気がするので品質をあげないとナー。とはいえ KS 品質でもいいから全部のドット絵素材を用意する方が優先順位は高いけども。

2014年9月25日木曜日

DRN.008 エレキマンの行動パターンを考える。

まえがき

連載第6回はエレキマンを取り上げる。ちょっとよくわからない。

エレキマン本体の行動パターン

  • 非負の整数を k として、移動した回数 n が n = 4 * k のとき
    1. 部屋の右側へ移動
  • 非負の整数を k として、移動した回数 n が n = 4 * k + 1 または n = 4 * k + 3のとき
    1. 部屋の中央に移動
  • 非負の整数を k として、移動した回数 n が n = 4 * k + 2 のとき
    1. 部屋の左側に移動/li>
  • 特定の x 座標に到達したとき
    1. サンダービームを発射
  • 自機がショットしたとき
    1. ジャンプする。
  • 壁に接触したとき
    1. ジャンプする。
  • 被弾したとき
    1. 行動をキャンセルし、ノックバック
    2. 一定時間無敵になる

こちらから攻撃しない限りはアイスマンと同様に部屋の右側、中央、左側を行ったり来たりしながら、指定 x 座標に到達するとサンダービームを発射する。ガッツブロックに引っかかったり、ノックバックさせたりで指定座標への到達できないとくり返し同じ座標に向かおうとするので、どうやら指定座標に到達してはじめて次の目標地点が変わるようだ。

サンダービームの行動パターン

  • 発射されたとき
    1. 直線運動
    2. 自機に当たると拡散

攻略法

サンダービームの予備動作中にタイミングよくショットを当てると、ハメられる。エレキマンステージ、ワイリーステージ2の両方で可能。動画を参照。

Defeat ELEC MAN without walking, jumping from hide behind on Vimeo.

あとがき

ハメると初期位置から一歩も動かずに撃破することもできるが、高火力のアイススラッシャーを上回る超火力のため事故りやすい。ノックバックするのが唯一の救い。

2014年9月24日水曜日

DRN.007 ファイヤーマンの行動パターンを考える。

まえがき

連載第5回はファイヤーマンを取り上げる。fireman は消防士のことなんだが、コイツは廃棄物処理ロボットである。

ファイヤーマン本体の行動パターン

  • 自機との距離が一定以上のとき、一定確率で?
    1. 自機の方を向いてファイヤーストームを撃つ
  • 壁に接触したとき
    1. 向き反転
  • 被弾したとき
    1. 一定時間無敵になる
    2. 自機の方を向いてファイアーストームを撃つ
  • 上記以外のとき
    1. 移動

離れているとファイヤーストームを撃ってくるが、間合いを詰めたままだと攻撃しない限りは移動するだけである。攻撃すると ファイヤーストームで反撃する。

ファイヤーストームの行動パターン

  • 発射されたとき
    1. 自機狙い、直進
    2. 自機足下に火がしばらく残る。

攻略法

ファイヤーマンステージでは、ある程度距離を詰めてファイヤーストームを封じつつ、ショットとジャンプを同時押しでジャンプショット、着地直後にまたジャンプショットのくり返しでハメられる。

ワイリーステージ4はワープカプセルがあって部屋が狭い。ワープカプセルの外に出て同様にする。

Defeat FIRE MAN with ROCK BUSTER from hide behind on Vimeo.

あとがき

攻撃面ではとにかく撃ちまくればよかった他のボスに対するアンチテーゼとでもいう位置づけか、弱点を衝けばゴリ押し可能なこのゲームでは珍しく、弱点を衝いてもパターンを無視してゴリ押ししようとすればゴリ押し返されることがある。また、なるべく敵から離れるという定石が裏目に出るのも嫌らしい。

2014年9月23日火曜日

ミサイルは渡り移るもの。

『ロックマン』の6ボスまで予約投稿にしたのでここで一段落。制作中のゲームの動画をさらす。

ミサイル渡りjump from one missile to another. from hide behind on Vimeo.

ミサイルからミサイルへ飛び移っているのは全部敵キャラクターなのだが、敵キャラクターのグラフィックがまだないのでプレイヤーので代用。プレイヤーキャラクターはミサイルにぶら下がっているやつ。ぶら下がり状態で左右に移動できるのだが、グラフィックがまd (以下略)。爆煙やらバックファイアを書きたいが、デカいのを書くと目障りになってしまうのをどうしたらよいかな。

そんなつもりはなかったが、クラウド・ストライフと同じようなカラーリングになっちゃった。イメージイラストもなくいきなり雑なドット絵打っているので、こうしてさらしておいたらエレガントなイラストを誰か書いてくれたりしませんかネェ。プレイヤーのドット絵もまだまだ足りない。被弾時のとぶら下がり移動、壁張り付きと壁張り付き移動は 最低でも必要だ。

DRN.006 ボンバーマンの行動パターンを考える。

まえがき

連載第4回はボンバーマンを取り上げる。ボンバーマンといってもハドソンのアレではない。というか今やハドソンって (会社) (ブランド) もなくなってしまったのか。何てことだ。ボンバーマンはスーパーファミコンで出た『スーパーボンバーマン』、『スーパーボンバーマン 2』、『スーパーボンバーマン 3』、『ぱにっくボンバー W』のソフトウェアだけでなくボンバーマンの顔のデザインのスーパーマルチタップ2も持っていたし、裏ステージを出すためだけにスーパージョイカードも買ったというのに。その他にも、ハドソンのソフトウェアでは『高橋名人の冒険島 Ⅱ』、『スーパー桃太郎電鉄 DX』、『アースライト 2・ルナストライク』、『ゲームボーイウォーズ 3』、『NECTARIS』のPlayStation 版、iPhone 版も買ったナー。脱線が過ぎた。

ボンバーマン本体の行動パターン

  • 一定確率で
    1. ハイパーボムを投げる。
    2. 一定確率でさらにハイパーボムを投げる。
    3. 一定確率でさらにハイパーボムを投げる。
  • 一定確率で
    1. 自機に向かって小ジャンプ
  • 一定確率で
    1. 自機に向かって大ジャンプ
  • 被弾したとき
    1. 一定時間無敵になる

ハイパーボムの行動パターン

  • 発射されたとき
    1. 自機狙い、放物線運動
    2. 接地すると爆発

ジャンプをこれにも流用できそうだ。

攻略法

ボンバーマンの小ジャンプ、大ジャンプは下をくぐれるので端に追いつめられないように適宜下をくぐって切り返す。ハイパーボムの爆風の範囲が広いので x 座標を大きくずらすと躱しやすいが、連続で投げてくることもあるので大きく逃げ過ぎて端に追いつめられないように。タイミングがよければジャンプでも避けられる。

ワイリーステージ4はワープカプセルがあって部屋が狭い。ワープカプセルの外に出て同様にする。

あとがき

ジャンプの大小は自機との距離だトカ被弾しただトカが関係していそうな気もするんだけどよくわからんので確率依存にした。困ったときは確率依存、確率依存万能説。あー、でもそれにしたおかげで内心「こっちくんな」と思いながら戦うことになるので業が深いな。このボンバーマンには関係ないけども。

2014年9月22日月曜日

DRN.005 アイスマンの行動パターンを考える。

まえがき

連載第3回はアイスマンを取り上げる。パターンが見え見えなので余り面白くないな。

アイスマン本体の行動パターン

  • 非負の整数を k として、移動した回数 n が n = 4 * k または n = 4 * k + 3 のとき
    1. 右に向かって前進
  • 非負の整数を k として、移動した回数 n が n = 4 * k + 1 または n = 4 * k + 2 のとき
    1. 左に向かって前進
  • 移動した後
    1. ジャンプ
    2. 自機の方を向いて等速で下降しながらアイススラッシャーを発射 * 3
    3. 自機の方を向いて等速で上昇しながらアイススラッシャーを発射 * 3
    4. 落下
  • 被弾したとき
    1. 一定時間無敵になる

各行動パターンが単純でパターン変更に確率が関係ない。パターン変更が確率だけに依存するガッツマンよりもさらに大味な感じがする。ジャンプ・落下は放物線運動、上昇・下降は等速直線運動を表現したつもり。ジャンプの頂点、着地点でパターン切り替えれば OK ね。

アイススラッシャーの行動パターン

  • 発射されたとき
    1. 直進
    2. 6 * n 発?撃つごとに弾速がアップ

攻略法

アイスマンステージでは床が滑る床なので多少動かしづらいが、慣れれば後方に滑って距離をかせぎながら前方に攻撃できる。部屋の端でア イススラッシャーをかわしながら 攻撃するだけ。アイススラッシャーは下をくぐれるが上を飛び越えることができない上段、下をくぐることもできれば上を飛び越えることもできる中段、下をくぐれず上を飛び越えるしかない下段の3種類があり、上段、中段、下段、下、中段、上段の順に撃ってくるので、適当にかわす。アイススラッシャーは撃てば撃つ程速くなるので早期に決着をつけるとよい。

ワイリーステージ4では床が滑らず、 ワープカプセルがあって部屋が狭い。ワープカプセルの外に出て同様にする。

あとがき

AI 的にはかなり単調なアイスマンだが、アイススラッシャーは高火力なので意外と強敵。アレだ。力こそパワー。

2014年9月21日日曜日

DRN.004 ガッツマンの行動パターンを考える。

まえがき

連載第2回は DRN.004 ガッツマンを題材にする。便宜上、自機が特殊武器スーパーアームで持ち上げられるブロックをガッツブロック (P) 、ガッツマンが投げるブロックをガッツブロック (E) とする。

ガッツマン本体の行動パターン

  • 一定確率で
    1. 前方にジャンプ
    2. 着地時に地震が発生し、地上にいる自機を転ばす。
    3. 揺れが収まるまで待機。
  • 一定確率で
    1. 後方にジャンプ
    2. 着地時に地震が発生し、地上にいる自機を転ばす
    3. 揺れが収まるまで待機。
  • 一定確率で
    1. 垂直にジャンプ
    2. 着地時に地震が発生し、地上にいる自機を転ばす
    3. 揺れが収まるまで待機。この間にガッツマンの頭上の天井からが降ってきたガッツブロック (E) を受け止める
    4. 自機を狙ってガッツブロック (E) を投げる
  • 被弾したとき
    1. 一定時間無敵になる

行動パターンの切り替えは確率だけなので単純。一連の行動が終わったらフラグを立てて、フラグが立っていたら乱数を生成してその値に応じて状態遷移。カットマンの場合、ジャンプの頂点でローリングカッターをねじ込む必要があったが、ガッツマンでは着地時に地震等をねじ込む必要がある。その他、被弾したときも一定時間無敵になるのは同じだが、行動パターンには影響がないので、前3つのパターンとは別レイヤーになっているとでもいうべきか。カメラを揺らしたり、自機を転ばしたりと、本体とその武器以外に直接影響を与えるのも特徴だ。

ガッツブロック (E) の行動パターン

  • ガッツマンが垂直にジャンプしたとき
    1. ガッツマンの頭上の天井から落下
  • ガッツマンに投げられたとき
    1. 自機に向かって直進
  • 地面に当たったとき
    1. 4つの破片に分かれ、破片のうちの2つは水平方向に直進し、残り2つは斜め上45°の方向に直進する

攻略法

ガッツマンステージではシャッター際に高台とガッツブロック (P) があるのでこの上で戦うとよい。地震はガッツマンが着地した瞬間からしばらくの間続くので、この間ジャンプで空中にいれば巻き込まれない。もちろん地震に巻き込まれないようにジャンプできるならそのほうがよいが、下手にジャンプすると地震で転ぶのが遅れ、転び状態から復帰するのも遅れてガッツブロック (E) や体当たりでダメージを受けてしまうので、ガッツマンとの距離があるときは敢えてジャンプしないで最速で転び復帰するのも手だ。ガッツブロックはガッツマンの頭の高さから自機の足下に向かって飛ぶ。高台やガッツブロック (P) の上にいると画面中央側のガッツブロック (P) の際で分裂することが多いのでシャッター際よりはある程度画面中央に寄ったほうがよい。ガッツマンに距離を詰められるかどうかは運なのだが、一応ガッツマンのジャンプをくぐることはできる。

ワイリーステージ4には高台等がないので前述の戦法は使えない。まずは天井の低いカプセルの外に出て高くジャンプできるようにする。地震は上と同様に対応する。ガッツブロック (E) が地面に当たって分裂するポイントでジャンプするとかわしやすいわけだが、ガッツブロック (E) は自機の足下狙いなのでふつうにタイミングジャンプすればいいことが多い。部屋が狭くなっている分、プレイヤーの運が試される (ぉ

あとがき

ガッツマンのように単純なパターンでパターン変更も完全に確率で制御にだとものすごく大味な気がするな。ジャンプは頂点と着地時にフックポイントを仕込んでおけばカットマンとガッツマンで共通化できそうだ。

2014年9月18日木曜日

DRN.003 カットマンの行動パターンを考える。

まえがき

2Dアクションゲームの敵の AI とでもいうべきものを作りたい。これまで赤ノコノコ等比較的単純なものをつくってきたが、もっと複雑なものをどうやって作るのか。これまで同様の単純な行動パターンの上位に、条件に応じてどれを実行するかを指示する制御があればいいかな。

面白い行動パターンがすぐに思いつくわけもないし、制作中のそれをここでネタバラシしてしまうのもアレなので、『ロックマン』シリーズのボス敵がどうなっているのか取り上げよう。1-7 まではやったから、これでしばらくはネタに困らないね。ヤッター。というわけでカットマン (FC版) の行動パターンを分析してどういう制御をすればいいのか考える。 当然のことながら、以下の行動パターンは外観上でしかないので実際とは違う。悪しからずご了承ください。

カットマン本体の行動パターン

  • 自機との距離 (X軸) が一定以上あるとき
    1. 自機に向かって歩く。
  • 自機との距離が一定未満のとき
    1. 軌道修正できないジャンプ。
    2. ジャンプの頂点付近で画面上にローリングカッターがなければ、一定の確率? でローリングカッターを投げる。
  • 被弾したとき
    1. 行動をキャンセルしノックバックし、短時間無敵状態になる。
    2. 立ち止まって、チョッキンチョッキン。
    3. 画面上にローリングカッターがなければローリングカッターを投げる。

大きく分けて 3 つのパターンがあり、自機との距離、被弾がトリガー。思いつきだけで KS コードを書いてみる。


using UnityEngine;
using System.Collections;

public class CutMan: MonoBehaviour
{
  bool counterAttackFlag = false;

  // 画面外のどこかにローリングカッターを隔離
  Vector3 positionOfOuterScreen = - Vector3.one;

  // Jump, Walk 切り替えの閾値
  float xThreshold = 5;
  Transform thisTransform;
  Transform playerTransform;
  enum ActionPattern
  {
    CounterAttack,
    Jump,
    Walk,
  };
  ActionPattern currentActionPattern = ActionPattern.Walk;
  bool switchActionPatternFlag = false;

  void Start ()
  {
    thisTransform = transform;
    playerTransform = GameObject.FindWithTag ("Player").transform;

  }

  void FixedUpdate ()
  {
    if (switchActionPatternFlag) {
      SwitchActionPattern ();
    }
    DoActionPattern ();
  }

  void SwitchActionPattern ()
  {
    Vector3 displacementFromThisToPlayer = playerTransform.position - thisTransform.position;
    float xDistance = Mathf.Abs (displacementFromThisToPlayer.x);
    float xDistanceMinusXThreshold = xDistance - xThreshold;
    
    if (counterAttackFlag) {
      currentActionPattern = ActionPattern.CounterAttack;
      counterAttackFlag = false;
      switchActionPatternFlag = false;
    } else if (xDistanceMinusXThreshold >= 0) {
      currentActionPattern = ActionPattern.Walk;
      switchActionPatternFlag = false;
    } else {
      currentActionPattern = ActionPattern.Jump;
    }
  }

  void DoActionPattern ()
  {
    switch (currentActionPattern) {
      case ActionPattern.CounterAttack:
        CounterAttack ();
        break;
      case ActionPattern.Jump:
        Jump ();
        break;
      case ActionPattern.Walk:
        Walk ();
        break;
    }
  }

  void CounterAttack ()
  {
    // ImplementMe
  }

  void Jump ()
  {
    // ImplementMe
  }

  void Walk ()
  {
    // ImplementMe
  }

  void OnTriggerEnter2D (Collider2D other)
  {
    if (other.tag == "PlayerBullet") {
      counterAttackFlag = true;
      switchActionPatternFlag = true;
    }
  }
}

パターン別のスクリプトを作って、その .enabled をこのスクリプトのswitch 文中で呼ばれるメソッドから操作するように書けばいいかな。色々書き足りないけどまた後日にでも加筆するとしよう。

ローリングカッターの行動パターン

カットマンの武器、ローリングカッターの外観上の行動パターンを以下に示す。例によって外観上のパターンで実際とは異なる。

  1. 自機狙いの射角で画面端まで直進
  2. 本体狙いで角度修正しつつ移動。本体が撃破されたときはそのまま直進。
  3. 本体に接触すると消滅

こんなところか。疲れたのでこっちは全部省略 (ぇ。

攻略法

せっかくだから攻略法も書いておく。どうやったら倒せるかも考えておかないと、『カイザーナックル』のジェネラルみたいに好き放題やったら、普通の人には倒せなくなってしまう。ところで『カイザーナックル』はいつ家庭用ゲーム機に移植されるんだろう。

カットマンステージのボス部屋には高台とガッツブロックがある。高台のシャッター際に陣取り、ローリングカッターをジャンプでかわしながら、飛びかかってきたカットマンを撃ち、下の床に落とす。自機の近くまで飛び込まれた場合、カットマンが下の床に落ちないで高台に残ることがあるが、被弾後の無敵時間終了直後に被弾させるようにしてたたき落とす。

ワイリーステージ2の道中の小部屋には高台がないので前述の戦法は使えない。行動キャンセル狙いでカットマンを攻撃。敵の無敵時間中にチョッキンチョッキンしはじめるので、無敵解除直後にヒットするのを狙って攻撃しつつ、行きのローリング・カッターを (自機が画面際の場合、画面中央へ向かって) ジャンプしてかわす。帰りのローリング・カッターはジャンプしたカットマンに連られて軌道修正されるので垂直ジャンプではなくカットマンから遠ざかるようにジャンプしてかわす。

2014年9月13日土曜日

配列色々

参考資料 1を読んで、なるほどと思いつつも、私は List<T> を余り使っていないのだった。反論でも何でもなく、ただ単に要素数が固定のものばかりを扱っているだけだけなので、可変のものを扱う必要があれば使うだろう。ArrayList を使わないのも同意する。

今、制作中のゲームでは type[] と SortedDictionary<TKey, TValue> をよく使う。それらの使い分けは、要素数が固定かつキーが整数でも分かりやすいもの、スコープが狭く他から参照しないもの、インスペクターから値を入れたいものには type[] を使い、それ以外には SortedDictionary<TKey, TValue> を使うようにしている。キーが整数でも分かりやすいというのは、格納した値を参照するために type[0], type[1], ... と書くときに適切なキーがわかるということだ。

キーが整数でも適切なキーが分かりやすい例は座標の履歴を格納する場合で、数字の若い方が新しいのか古いのかの流儀はあるにしても自分の分かりやすい方にすればいい。なお、私は若い方が新しい流儀である。

他方、キーが整数だと適切なキーが分かりにくい例は東西南北に対応するベクトルを配列に格納する場合だ。私がやるとすれば北を 0、東を 1、南を 2、西を 3 にするだろうが、読者の皆さん (誰?) も果たして同じだろうか。同じだったらボクと握手。

キーに凝るのは余所から参照するときにわかりやすくするためなので、スコープが狭く他から参照しないものにまで凝ったキーを付ける必要はない。もう一つの理由の、インスペクターから値を入れたいというのは、List<T>, SortedList<T>, Dictionary<T>, SortedDictionary<T> ではできないからという消極的な理由だ。

なぜ Dictionary<TKey, TValue> でなく SortedDictionary<TKey, TValue> なのかは参考資料 2 が詳しいのでそちらをご参照下さい。

さて、SortedDictionary<TKey, TValue> では型で束縛されるが好きなキーが使えるので分かりやすいキーにしやすい。キーを string にすれば制限はほとんどなくなるが、補完が効かないので私は要素数が固定の場合はキーを Enum にする。いやぁ、補完が効くって素晴らしい。

あ、書き忘れていたが、SortedDictionary<TKey, TValue> や Dictionary<TKey, TValue>を使うには以下を書いて名前解決しておくとよい。

using System.Collections.Generic;

参考資料

  1. Myouji. "[Unity] Array/配列的な奴らとの付き合い方-type[]とかListとか". myoujing!!. 2014-08-24. http://myoujing.wpblog.jp/2014/08/874/, (参照 2014-09-11)
  2. Vladimir Bodurov. "IDictionary Options - Performance Test - SortedList vs. SortedDictionary vs. Dictionary vs. Hashtable". Vladimir Blog. 2007-12-24. http://blog.bodurov.com/Performance-SortedList-SortedDictionary-Dictionary-Hashtable/, (参照 2014-09-11)

2014年9月11日木曜日

賽の河原で小石を積み上げる単純作業

やはり雑談よりは開発ネタを書くべきなのか。記事を書かない日も KS コードを書いては調整と見直しをやっている。どのように動いて欲しいかは割とハッキリしているが、プログラミング自体は手探りなので、できたコードは KS ばかりで手戻りが多い。とはいえ、例え KS コードでも少なくとも 2, 3 作ってみないことには多少なりとも再利用できそうなものの作り方が見えてこないのでやるしかない。

制作中のゲームでは、今のところ、キー入力に応じて動くプレイヤー、行き先の足場がなくても進む、壁際で反転する敵 (マリオにおける緑ノコノコに相当)、行き先の足場がないまたは壁際で反転する敵 (マリオにおける赤ノコノコに相当)、行き先の足場がないとジャンプ、壁際で反転する敵 (何かあったっけ?) 等を作った。もっと複雑な行動パターンを作りたい。

なお、キャラクターの座標計算には、下記のサイトを参考に、velocity Vertlet 法を用いた。

参考資料

  1. みその計算物理学. "velocity Verlet法(PDF形式)". みその計算物理学. http://www.geocities.jp/supermisosan/moleculardynamics/velocityverlet.pdf, (参照 2014-09-10)
  2. T. Koishi. "数値積分の実行". Koishi's Page. http://polymer.apphy.u-fukui.ac.jp/~koishi/lecture/md_program5/, (参照 2014-09-10)

2014年9月8日月曜日

弾切れ

メーデーメーデーたまぎれすんぜん、どころか書き溜めておいた (という程の量ではないが) 記事は全て放出してしまったので真に弾切れだ。間があきすぎてもアレなので弾切れの話をしてお茶を濁そう。

ゲームで武器が弾切れでクリア不能なんていうのはやだなートカ思う。AC2 でレオス・クラインを倒したのに軌道コントロール装置を破壊する弾が足りないトカ、AC3 で管理者部隊AC を倒したのに管理者を破壊する弾が足りないトカね。私が好きな『ロックマン』シリーズのうち 2, 3, 4はラスボスに弾数無限の通常武器が通用せず、エネルギー制限のある特殊武器しか効かないのでまさに弾切れで詰む要素のあるゲームだ。でも案外気にならなかったのは無限コンテニューがあり、自殺してやり直すことができたからだろう。エネルギーが尽きても残機があればそれが尽きるまで自殺を強いられるわけではあるが。同シリーズの後発の作品 (例えば『イレギュラーハンターX』) では1機ミスすれば武器エネルギーは全回復するようになっており、さらに後発の作品 (同シリーズではないが開発者的な意味で同じ系統に属する『蒼き雷霆ガンヴォルト』) ではエネルギー切れになる前ならいつでもチャージ可能でさらにエネルギー切れになっても時間が経てば回復するという具合であり、エネルギー切れに陥っている時間は短くなっている。これなら弾切れで面倒なことになるのが嫌いな人でもOKよね。

2014年9月6日土曜日

下からすり抜けて上に乗ることができる床の実装例 (Unity C#) Nostalgia (Version 1.0.2) 対応版

Nostalgia (Version 1.0.2) では前回示したようなコライダーごとにスクリプトを付ける手法は使えないようなので、プレイヤーの近傍のセルを調べてcollider.enabled を切り替える方法にした。

図で示すとこういう感じ。画像の赤色のセルはプレイヤーのいるセル、緑色のセルは、中にコライダーがあれば collider.enabled = false にするセル、青色のセルは、中にコライダーがあれば collider.enabled = true にするセルを示す。

KS コードを公開するよ。コメントをつけたやたらに長い名前の3つの変数の値をお好みで適当にいじればいいんじゃないかな。


using UnityEngine;
using System.Collections;
using Nostalgia;

public class NostalgiaColliderEnabledSwithcher : MonoBehaviour
{
  Transform playerTransform;
  Map map;
  Vector3 playerPosition;
  int numberOfHalfOfHorizontalCells;

  // 中にコライダーがあれば collider.enabled の値を変更するセル (図の青色または緑色のセル) の水平方向の数。
  int numberOfHorizontalCells = 7;
    
  // 中にコライダーがあれば collider.enabled = false にするセル (図の緑色のセル) の垂直方向の数。
  int numberOfVerticalCellsEachOfWhichMayHaveColliderToBeDisabled = 6;
    
  // 中にコライダーがあれば collider.enabled = true にするセル (図の青色のセル) の垂直方向の数。
  int numberOfVerticalCellsEachOfWhichMayHaveColliderToBeEnabled = 1;
  Point2[] offset1;
  Point2[] offset2;

  void Start ()
  {
    playerTransform = GameObject.FindGameObjectWithTag ("Player").transform;
    map = GetComponent <Map> ();
    numberOfHalfOfHorizontalCells = (numberOfHorizontalCells - 1) / 2;
    offset1 = new Point2[numberOfHorizontalCells * numberOfVerticalCellsEachOfWhichMayHaveColliderToBeDisabled];
    for (int i = 0; i < numberOfHorizontalCells; i++) {
      for (int j = 0; j < numberOfVerticalCellsEachOfWhichMayHaveColliderToBeDisabled; j++) {
        offset1 [numberOfVerticalCellsEachOfWhichMayHaveColliderToBeDisabled * i + j] = new Point2 (i - numberOfHalfOfHorizontalCells, j);
      }
    }

    offset2 = new Point2[numberOfHorizontalCells * numberOfVerticalCellsEachOfWhichMayHaveColliderToBeEnabled];
    for (int i = 0; i < numberOfHorizontalCells; i++) {
      for (int j = 0; j < numberOfVerticalCellsEachOfWhichMayHaveColliderToBeEnabled; j++) {
        offset2 [numberOfVerticalCellsEachOfWhichMayHaveColliderToBeEnabled * i + j] = new Point2 (i - numberOfHalfOfHorizontalCells, - 1 - j);
      }
    }
  }
  
  void FixedUpdate ()
  {
    playerPosition = playerTransform.position;
    Point2 mapPointOfPlayerPosition = map.WorldPointToMapPoint (playerPosition);

    Point2 [] listOfPoint2InCellWhichMayHaveColliderToBeDisabled = new Point2[numberOfHorizontalCells * numberOfVerticalCellsEachOfWhichMayHaveColliderToBeDisabled];
    for (int i = 0; i < numberOfHorizontalCells; i++) {
      for (int j = 0; j < numberOfVerticalCellsEachOfWhichMayHaveColliderToBeDisabled; j++) {
        listOfPoint2InCellWhichMayHaveColliderToBeDisabled [numberOfVerticalCellsEachOfWhichMayHaveColliderToBeDisabled * i + j] = mapPointOfPlayerPosition + offset1 [numberOfVerticalCellsEachOfWhichMayHaveColliderToBeDisabled * i + j];
      }
    }
    foreach (Point2 p in listOfPoint2InCellWhichMayHaveColliderToBeDisabled) {
      SwitchColliderEnabled (p, false);
    }

    Point2[] listOfPoint2InCellWhichMayHaveColliderToBeEnabled = new Point2[numberOfHorizontalCells * numberOfVerticalCellsEachOfWhichMayHaveColliderToBeEnabled];
    for (int i = 0; i < numberOfHorizontalCells; i++) {
      for (int j = 0; j < numberOfVerticalCellsEachOfWhichMayHaveColliderToBeEnabled; j++) {
        listOfPoint2InCellWhichMayHaveColliderToBeEnabled [numberOfVerticalCellsEachOfWhichMayHaveColliderToBeEnabled * i + j] = mapPointOfPlayerPosition + offset2 [numberOfVerticalCellsEachOfWhichMayHaveColliderToBeEnabled * i + j];
      }
    }
    foreach (Point2 p in listOfPoint2InCellWhichMayHaveColliderToBeEnabled) {
      SwitchColliderEnabled (p, true);
    }
  }

  void SwitchColliderEnabled (Point2 givenPoint2, bool b)
  {
    Cell cellOfGivenPoint2 = map.GetCell (givenPoint2);
    if (cellOfGivenPoint2 != null) {
      Collider2D colliderOfCell = cellOfGivenPoint2.collider;
      if (colliderOfCell != null) {
        colliderOfCell.enabled = b;
      }
    }
  }
}

使用した Nostalgia の Version 1.0.2 を追記した。 2014-09-25

2014年9月4日木曜日

Unity 用タイルマップエディタ AutoTileSet (Version 1.3) と Nostalgia (Version 1.0.2) の比較

  1. 背景
  2. AutoTileSet の特徴
  3. Nostalgia の特徴
  4. 両者の比較

1. 背景

Unity で作成中のゲーム (2D ジャンプアクション) でマップを作成するに当たり、手作業で床、壁、足場等のプレハブをシーンビューに配置したところ、非常にダルかった。それで、テキスト (CSV, TSV, XML等) でタイルマップのデータを作成し、これを読み込んで表示する、ということを検討したところ、Tiled Map Editor (http://www.mapeditor.org/) を使うことで画像の作成は簡単だったが、画像とは別にコライダーやレイヤーの情報を手入力する必要があるため非常に面倒で、Unity 内で作業が完結しないのもいまいちだった。

そこで、 Asset Store にて Unity のシーンビューで直接タイルマップの作成が可能なアセットを検索したところ、いくつか見つかった。本稿では、その中で無料であった AutoTileSet (Version 1.3)と、RPGツクールVX規格またはWOLF RPG Editor規格のオートタイルに対応していると見てとれた Nostalgia (Version 1.0.2) を取り上げ比較した。

2. AutoTileSet の特徴

長所

  • 無料なので資金に余裕の無い個人開発者にとってはありがたい。
  • ペンと消しゴムの切り替えが自動。シーンビューでタイルがないところをクリックするとタイルを配置でき、タイルがあるところでは消去できる。また、ドラッグすると最初のクリック時の動作をし続ける。
  • ヒエラルキーのAutoTileSet のプレハブ以下に各タイルに対応するタイルの名前の GameObject が生成される。それにスクリプトをアタッチして色々できる。
  • 段差があるところを段々にするか斜面にするか切り替えることができる。
  • タイルにバリエーションをもたせることができる。元々のタイルセットに加えて1つ以上のバリエーションをもつときにタイルを配置するとそれらの中のいずれかのタイルセットから適切なタイルが配置される。これにより見た目に単調なマップにならずに済む。
  • サンプルシーンの見栄えがよい。洞窟に光が差し込んでいる感じが出ている。

短所

  • ツールがないので、まっすぐな線を引くのがむずかしく、塗りつぶしするのも面倒。
  • AutoTileSet用のオートタイルセットは48パターンも必要なので素材を自作するのはちょっと大変かも。せっかくあるバリエーションの利点を殺しかねない。同じ開発元の AutoTileGen (Steam等 にて $25 くらい?で販売中) を使えば5パターン (うち2パターンはマスク) の画像を元に48パターンの画像からなるタイルセットを生成できるようだ。
  • サポートが英語。他の外国語よりはずいぶんマシだが。

Nostalgia の特徴

長所

  • ペン、消しゴムの他、矩形範囲のタイル配置 / 消去、塗りつぶしツールがある。
  • RPGツクール VX または WOLF RPG Editor の形式のオートタイルを扱える。
  • 作者が日本人なので日本語でサポートが受けられる。
  • Refreshボタンがあり、タイルセットを更新したときに再描画できる。シーンを複製してタイルセットだけを総換えする等でマップの再利用に便利。

短所

  • 矩形ツールまたは塗りつぶしツールの適用範囲が広いともたつき、広過ぎると最悪 Unity が応答無しになった。死んでもいいプロジェクト等でその辺の感覚をつかんでおいたほうがよい。
  • サンプルシーンが微妙。
  • 斜面のサポートはないようだ。今のところ使う予定はないけど。

その他

  • Map以下の構造 (Cell, Collider等) はインスペクター上では隠蔽されているが、スクリプトでアクセスする手段は用意されているのでそちらを使うべし。作者さんに教えてもらうまで気付いていなかったが、使うの自体はむずかしくない。

4. Nostalgia と AutoTileSet の比較

以下に比較の表を示す。

表. Nostalgia と AutoTileSet の比較
項目 Nostalgia AutoTileSet コメント
ペン Nostalgiaのペン先の矩形の大きさは可変。
消しゴム Nostalgiaの消しゴム先の矩形の大きさは可変。
矩形選択配置 × Nostalgiaの大量配置に難あり。
矩形選択消去 × Nostalgiaの大量消去に難あり。
塗りつぶし × Nostalgiaの大量配置に難あり。
Refresh × あるとマップの再利用等に便利。
バリエーション × あるとマップが視覚的に単調になりにくい。
1タイルセットのパターン数 5 48 少ないほうが素材の自作は楽。
サポート言語 日本語 英語 英語でも可だが、母国語である日本語の方がいい。英語以外の外国語は辛い。
斜面 × -
例は例でしかないが、例買いもある?
価格 20$ 0$ -

◎: よい, ○: わるくない, ▲: よくない, △: わるい, ×: 機能なし.

RPGツクールVX規格またはWOLF RPG Editor 規格のオートタイルに対応という時点で Nostalgia 以外の選択肢がなかった (あるいは筆者が分からなかっただけで実はどこかにあったのかもしれないが、分からないものはないのと同じ)。素材を自作する場合でもわずか5タイルのパターンを作成すればよく、そうでない場合でもフリー素材が利用可能なので、両規格に対応していることはかなりのメリットだと思う。タイルセットのコンバーターを書くのは比較的容易だと思われるが、都度別途コンバートするのは面倒。元から対応していればその面倒はない。AutoTileSet の方が優れている項目のうち斜面、例は今回は重視しない機能だったので不問、バリエーションはあればよかったカモというところ。それよりも矩形選択配置または消去、塗りつぶしの重さが課題か。

機能の優先順位は人それぞれなので、自分の要求にあったものを使えばいいと思うよ。事実誤認トカありましたら教えてください。

両者のバージョン (AutoTileSet (Version 1.3)と Nostalgia (Version 1.0.2)) を書き漏らしていたので追記した。この記事の内容は各アセットの最新版には当てはまらない可能性がある。 (2014-09-25)

下からすり抜けて上に乗ることができる床の実装例 (Unity C#)

すり抜け床の基本的な考え方は参考資料 1 のサイトを参考にした。角度で切り分けるのが面倒だったので y 座標でざっくり切り分けることにした。

KS コードは以下。すり抜け床にしたいゲームオブジェクトにアタッチして使う。


using UnityEngine;
using System.Collections;

public class ColliderController : MonoBehaviour
{
  Transform playerTransform;

  void Start ()
  {
    playerTransform = GameObject.FindGameObjectWithTag ("Player").transform;
  }

  void FixedUpdate ()
  {
    float f = playerTransform.position.y - thisCollider.bounds.max.y;
    if (f <= 0) {      
      collider2D.enabled = false;
    } else {
      collider2D.enabled = true;
    }
  }
}
  1. 株式会社スマイルブーム. "「すり抜け床」を考えてみる". スマイルブーム.com http://smileboom.com/blog/tkool/throughfloor.html, (参照 2014-09-02).

2014年9月2日火曜日

ブログはじめました

今時冷やし中華じゃあるまいし、そのタイトルはなかろうというものだが、ゲーム関連の話を書いていく所存、デモンズブレイゾン。

Blogger のシステムでつらつら書いたらソースが KS 過ぎてものすごくアレだが、その辺はなるべく気にしない方向で。