ラベル C# の投稿を表示しています。 すべての投稿を表示
ラベル C# の投稿を表示しています。 すべての投稿を表示

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年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月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 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).