UDN
Search public documentation:

DungeonDefenseDeveloperBlogJP
English Translation
中国翻译
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

ダンジョンディフェンス (Dungeon Defense) 開発者ブログ

Blog 1: 初日

皆さん、こんにちは。

インディー系の開発者にとって、わくわくするような時代がやってきました。デジタル配布の時代の到来とともに、巨大な市場が出現しました。小さいクリエイティブなチームが製作したあらゆる種類のゲームが恩恵を受けるようになりました。そればかりではありません。私たちは Epic を手に入れたのです。間違いなく、Epic は業界で最先端を走っています。その Epic が、熱意あふれる、未来の Shigeru Miyamoto (宮本茂) や Kim Swift のために、プラットフォームを開放したのです!

私はかなり前から Unreal を使って開発してきたので、この時流に乗ろうと考えました。今後数ヶ月に渡って、「UDK」(=「Unreal Development Kit」(Unreal 開発キット)) を使って簡単なミニゲームのデモを連続して製作してみるつもりです。目標は、オープンソース / オープンコンテンツの例を通じて「UDK」で多様なゲームプレイを実装する方法を、成長しつつあるこのコミュニティに対して提供することにあります。おそらく比較的シンプルなコードとコンテンツになるはずでなので、フルコースの「Unreal Tournament」よりも消化しやすいと思います。

それはともかくとして、さっそくいただきましょう! (今、すごくお腹も空いています)。

まずどんなゲームから作り始めようかと考えたとき、「Unreal」 を使用した三人称視点のさまざまなゲームについて疑問をもっている人たちがいることに気がつきました。(この種のゲームの作成は、やり方さえ分かってしまえば簡単です)。さらに、「PixelJunk Monsters」を一通りプレイし終わったところでしたので、Tower Defense 系 (塔を配置して城を防衛するゲームタイプ) ゲームに幾分気持ちが傾いてきました。戦略的に建物を配置して多数の小動物を撃退するゲームは、おそらくとても楽しいはず。

という次第で、最初に製作するのは、「Dungeon Defense」(ダンジョン防衛) という名前のゲームにしました。視点は、セミトップダウン ( 3/4 透視図法により近い) で、タイプは、アクション / 塔による防衛のハイブリッド型です。プレイヤーは魔法使いとしてプレイします。状況は、師匠が不在の間にその隠れ家を守ろうとして大変な目に遭遇するといったものです (「魔法使いの弟子」 のように)。隠れ家ではさまざまな「召喚」魔法を唱えることによって、魔法的ディフェンスを形成します。ディフェンスが破られた場合は、魔法の杖で撃退します。アクション戦術とリソース管理戦略のハイブリッドによってゲームが楽しくなります。ゲームの実装は本来非常に大変ですが、「Unreal」を使用すると快適なものになります。

さてそれで、週末に設計作業を行ってみました。アセットについて綿密に計画を立て、スキームを調整してみました。これによって今日、正式にプログラミングを開始する運びとになったのです。まず、GameInfo、Pawn/Player、Player、Controller、Camera の各クラスを実装しました。以下でクラスの概要を説明します。

GameInfo クラスには、ゲームの一般的なルールが含まれます。私の場合は、これまでのところ、自分のカスタムの PlayerPawn クラスと PlayerController クラスに反映させる、デフォルトの値をオーバーライドしただけです。PlayerPawn クラスは、もちろんワールドにおける物理的なキャラクターです。Controller クラスは、ユーザーの入力が PlayerPawn クラス 上のゲームプレイアクションに変換される場所です。カメラのクラスについては、UpdateViewTarget 関数を修正することによって、カメラの位置を Pawn 上方に据えるとともに (「Unreal Tournament」のように目から直接眺めません)、プレイヤーが眺める方向に少しカメラが動的にオフセットするようにしました。これによって、カメラがターゲットの方向に向かって回転するようになりました。 (2) Epic に含まれている RInterpTo 関数と VInterpTo 関数を利用して、Rotator と Vector の補間をそれぞれ処理しています。(これらは常に役立つものです)。これによって、現在のプレイヤーの位置 (および回転) からカメラを少し遅延させることができるため、プレイヤーの位置どおりに固定するよりもスムーズな動きを表現することができます。

PlayerController クラスの中では、PlayerMove 関数を変更して、ヨー (Yaw) の回転だけが変化するようにしました。ピッチ (Pitch) は変化しません。(これは、キャラクターの方向を 2D 平面に限定するためです)。最初は、マウスをスクロールさせながら Mouse Delta (差分) を利用してフレームごとに少しヨーを変更しようとしましたが、自然な感じが得られませんでした。PC にとっては不正確の極みでした。キャラクターは、私たちが操作した方向にきちんと向いている必要がありますよね? そこで、独自のコードを書いて、マウスの現在位置の canvas deprojection (キャンバス逆投影) に関する結果を得るようにしました。それから、その 3D ベクターをワールドに対してレイキャストすることによって、ユーザーが指しているワールドの部位を見つけ、キャラクターをその方向に向かせることにしました。 (3)

もちろん、カメラの回転による入力方向も変換して、入力が「カメラに相対する」直感的な動きになるようにしました。(TransformVectorByRotation 関数の vector>>rotator 演算子に相当するものだと言えます)。プレイヤーポーンは、ほとんどデフォルトのポーンと同じです。ただし、楽しいアニメーションツリーの作業を行って、複数のカスタムなフルボディアニメーションをブレンドしました。これによって、シーケンスで複数のアニメーションを再生するときでさえポップが現れなくなります。 (4) アニメーションツリー システムを使って作業していると、「Unreal」のツールスイートを使用することがいかに楽しいことであるか思い起こされました。キャラクターのブレンディングを視覚的にリアルタイムのフィードバックを伴って調整していると、ハードコード化よりも確実に楽しいのです。

しかし、この作業はほんの 2 ~ 3 時間で済むため、まだ本日の作業を終了するわけにはいきません。次に、魔法使いの武器である杖 (staff) に取り組まなければいけません。Weapon クラスをサブクラス化して、武器が携えられた際に充填できるようにし、リリースされた時にのみ発砲するように変更しました。(杖にはさまざまな充填攻撃がほしいものです)。 (5) また、Projectile クラスをサブクラス化してさまざまな強さの発射物をサポートしました。これに応じてすべての視覚効果が徐々ににスケーリングされます。 (6)

アーキタイプの参照を作成するほうが、直接的なクラスの参照よりもはるかに便利であると思いました。 (7) ゲームプレイのためにアーキタイプを指定するには Spawn 関数で行います (いわゆるアクタ テンプレートとして)。ゲームプレイのためにアーキタイプを作成すると、ゲームプレイ オブジェクトのライブラリ全体をエディタ内でリアルタイムに調整することができるようになります。値を修正しなければならなくなるたびに、デフォルトのプロパティを変更する必要がなくなります。また、これによって視覚的に値を設定することが簡単にできるようになるとともに、メディアのスワップアウトも容易になり、さらには、プロパティだけが異なる、同一のコアクラスからなる複数のバリエーションクラスを作成することも楽にできるようになります。アーキタイプの機能については後にその詳細に触れる予定ですが、イタレーションの重荷を軽減させてくれたと言って差し支えありません。もちろん、Remote Control もリアルタイムのイタレーションには本当に役立ちます。(実行ファイルを起動するときに -wxwindows の引数をつけることによってアクセスできます)。Remote Control の機能については、また別の投稿で述べるつもりです。

次に最初の敵である Goblin の AI Controller に取り組みました。「ステートロジック」を少しばかり書きました。これは、まずターゲットを選択します (ターゲット化の重み付けを返すインターフェースを実装する、すべてのアクタに基づきます)。 (8) さらに、この「ステートロジック」によって、パスファインドのタイミングとターゲットに直接進むタイミング、パスファインド / 移動の停止のタイミング、攻撃開始のタイミングが決定されます。 (9) AI スクリプトの詳細については後述するつもりです。また、敵の Goblin のために、かっこいい MeleeAttack (接近戦) のステートを実装しました。このためには、アニメーション通知を使用することによって、現在の「手のソケット」の位置と前回の位置との間における各フレームの Trace (ボックススイープ) を有効 / 無効にします。 (10) これによって Goblin がダメージを与えるためにスワイプした部分が、ハードコード化された値にではなく、アニメーションとそのタイミングに実際に基づくことになります。また、Goblin が「Trace された」アクタにダメージを与えるのは、一意のスワイプ 1 回ににつき 1 度だけにしています。これは、現在のスワイプ中に打撃を受けた全アクタのリストを維持し、それと照合することによって可能となります。 (11) これらがすべて実行されると、接近戦は素晴らしい出来映えになり、アニメーションが表現するとおりのものに見えます。

次に、どうしても基本となる「砲塔」を実装したくなりました。これは敵を攻撃するものです。このシンプルな動くことのないアクタのために AI Controller で時間を無駄にすることなく、Timer を使ってターゲットを選択します。(Controller だけではなく、ステートロジックは、どんなオブジェクトにも使用できることを忘れないでください)。 (12) また、LookAt Bone Controller をこの塔のアニメーションツリーに追加することによって、塔の最上部が選択されたターゲットに向くようにしました。 (13) アニメーションツリーがセットアップされると、他にやるべきことは、塔に対してどの方向に向くかを命じるコードを 1 行書くだけです。やった!

ゲームタイプが明確化されてきたので、さらに作業を進めて、Crystal Core の実装に移りました。この Crystal Core は、敵が破壊を試みようとするもので、敵の主要な目標となっています。 (14) ターゲットとなりうるアクタ (15) のために作成した「インターフェース」を使用して、この Core に特別に高い優先順位を与えました。そのため、敵は、プレイヤーや塔よりもこれに引きつけられることになります。「インターフェース」を使用すると、完全に異なるクラスのアクタが共通のメソッドの集合をもてるようになります。それによって、アクタは同一の方法で操作されたりチェックされたりするようになります。したがって、この Crystal Core クラスとプレイヤーポーンは、直接継承関係にはないにもかかわらず、共通のインターフェースによって提供される、同一のターゲット化の重み付け関数を共に実装していることになります。これによって敵の AI は、汎用的な方法でアクセスして、どのエンティティがより重要なターゲットであるかを判断することができることになります。すばらしい!

最後に、プロジェクトの中心アーティストである Morgan Roberts がテスト用レベルを整えて、魔法使いの隠れ家を実に素晴らしい感じに仕上げてくれました。私は Kismet を少しばかり使って、敵の Wave (大軍) が繰り返しスポーンして Core 攻撃のために進軍するように設定しました。 (16) さてこのような感じで、基本的にプレイ可能なプロトタイプが約 1 日でできてしまいました。

ゲームプレイはすでに手強いものになってきているので、今後数日間はバランスに関する作業に真剣に取り組むことにします。もちろん、多数追加される機能の実装や、すでにできあがったものを調整する必要もあります。「Unreal」という素晴らしいツールのおかげで、このようなものを実装するのは本当に楽しいことなのです。

次の投稿では、上記で簡潔に触れたテーマのうちその多くを取り上げて詳しく解説します。また、興味深いと思ってもらえるような (あるいは特に役に立つ) コードや機能をいくつか概説していきたいと思います。また、見てもらえるような画像ができたら、スクリーンショットをアップすることにします。それでは、みなさんがすぐに、このささやかなゲームをプレイすることを楽しみにしています。

さっき考えていたピザを食べよう...

- Jeremy

Blog 1: ファイル レファレンス

このブログで取り上げられている情報は、次にあげるファイルに含まれています。(それらのファイルは、Dungeon Defense のソースコードです)。カンマで分けられている行番号は、ファイル内で独立した行が複数あることを示しています。ハイフンで分けられている行番号は、ファイル内で行がその範囲にまたがっていることを示しています。

  1. Main.uc: 618, 638
  2. DunDefPlayerCamera.uc: 240 - 248
  3. DunDefPlayerController.uc: 1561 - 1652
  4. DunDefPawn.uc: 222
  5. DunDefWeapon_MagicStaff.uc: 111
  6. DunDefProjectile_MagicBolt.uc: 24
  7. DunDefInventoryManager.uc: 13
  8. DunDefEnemyController.uc: 222
  9. DunDefEnemyController.uc: 764
  10. DunDefGoblinController.uc: 52
  11. DunDefGoblinController.uc: 32
  12. DunDefTower_ProjectileType.uc: 99
  13. DunDefTower_ProjectileType.uc: 95
  14. DunDefCrystalCore.uc: 19
  15. DunDefTargetableInterface.uc: 15
  16. DunDef_SeqAct_BasicActorSpawner.uc: 11

Blog 2: 3 日目

Dungeon Defense を開発してから 2 日目と 3 日目は、照準機能と AI ナビゲーションの改善とともに、ワールド内に防御「塔」を直感的に配置できるシステムの基礎を追加することに注力しました。それぞれの実装について説明させてください!

照準について言えば、当初のマウスベースによるスキームは、単に、マウスで指したところをプレイヤーキャラクターがヨー角で見るというものでした。このスキームはうまく機能していたのですが、3D 照準については別です。キャラクターがヨー角でのみ回転するのは、(ほぼ) 上からの視点からでは、ピッチ角の入力を直感的に決定できる実際的な方法がないからです。敵がプレイヤーの下や上にいる場合が問題になります。敵を攻撃したくてもできないため、ストレスになります。そこで、大きな修正を 2 つ施しました。1 つは、マウスベースのスキームに関してです。もう 1つはゲームパッドのスキームについてです。これらは極めてうまく機能してくれました。マウスベースのスキームに関しては、まず、マウスのスクリーン座標の「逆投影」視線がぶつかる点に照準を合わせたキャラクターのピッチ角を計算しました。 (1)

ちょっとした脱線 : 「逆投影」とは 2D スクリーン空間から 3D ワールド空間に変換することを意味します (たとえば、スクリーンから線を投射するように)。一方、「投影」はワールド空間からスクリーン空間に変換することを意味します。両方とも、プレイヤーの HUD を通じてアクセスできる、対応する Canvas クラスの関数を使用して「Unreal」で実行することができます。 (2)

次にいずれの場合も、このピッチ角の値をキャラクターのアニメーションツリー内にある Bone Controller セットアップに渡します。こうすることによって、キャラクターは腰の部分で体を曲げて上下を見るようになります。 (3) このようにすると、PC ゲームでは自然な感じが最もよく出ます。

ただし、キャラクターの近くを指すとキャラクターが下を見がちな点は何とかしなければなりませんでした。(キャラクターが基本的に足下を見るのは、実際にそこを指しているからなのです)。そこで条件を加えることにしました。プレイヤーの近くを指した場合でも、狙ったところと高度の違いがさほど大きくなければ、単に前方を見るようにしたのです。 (4) これによって、カーソルをプレイヤーの近くに持ってきたときにプレイヤーが足下を見ながら走り回らなくても済むようになります。もちろん、Bone Controller にセットされているヨーとピッチに補間を適用しました。これによって、照準点が急激に変化しても、キャラクターの動作が極端に変化することがなくなりました。さらに、実際の照準点をポーン内に保存し (5) 、武器がそれを調べて、その地点に向けて発射物を明確に狙うようにしました。これは、Rotation (回転) を Rotator(TargetPoint-ProjectileSpawnPoint) に設定するとともに、Velocity (速度) を Speed*Normal(TargetPoint-ProjectileSpawnPoint) に設定することによって簡単に可能となります。 (6) この結果、ピンポイントのシューティングが可能になりました。PC ゲームの出来映えとしては妥当なところです。それでいて、私が望んでいたとおりにシンプルなままで、アーケードゲームのようなトップダウンの視点を保っています。

ゲームパッドコントロールのスキームに関しては、少しばかり異なった処置をしました。プレイヤーには素早く動く正確なポインティングデバイスがないため、ユーザーが指した正確な位置を見るなどということは論外です。それでも、なんらかの 3D 的照準が必要でした! そこで、自動照準関数を実装することにしたのです。この関数は、最大範囲内に存在する (もし存在すれば) 最善のターゲットを決定し、照準位置をそのターゲットの位置に設定するものです。 (7) 自動照準は依然として照準点システムを使用するため、PC のコントロールスキームのために私がすでに作成した「見る」メソッドに適合します。唯一の違いは、「どのようにして」照準点が選択されるかということです。

そこで、最善の自動照準ターゲットを選択するために、プレイヤーからの OverlappingActors 関数によるチェックを開始して、自動照準範囲にある「敵」タイプのアクタをすべて集めます。次に、どの潜在的なターゲットが自分に最も近く「かつ」自分の見ている方向に最も近いかを判断します。最も近くにいて、かつ見ている方向に最も近い (これは 「Normal(TargetLocation - EyeLocation) dot Vector(Rotation)」として計算されます) ターゲットが、それぞれにとって許容範囲にあり、かつ重み付けの範囲にある場合に理想のターゲットとなります。許容範囲と重み付けを調整し終わると、この自動照準選択メソッドはうまく機能しました。これで、ゲームパッドを使用して、(だいたい) 見ているものを垂直方向に自動的に狙うことができるようになりました。また、ターゲットに向けてヨー回転を少しばかりキャラクターの背骨 (Spine) に加えることによって、自動照準時に内積による許容量のために弾丸が斜めに飛んで行くようには見えなくなりました。 (8) これでゲームパッドコントロールがマウスと同等になりました。

これは余談ですが、私は DebugSphere を使用して自動照準ターゲットの地点を描画しました。これは、選択メソッドがどのくらいうまく機能しているかを見極めるのに役立ちます。実際、分析する際にはいつも DebugSphere、DebugLine、DebugBox を使用しています。プロトタイピングの段階では、これらを使用することを強くお勧めします。クラス内でコードにこれらを描画させたままにしておくこともできます。カスタムの bDebug (bool 型) を使用してオフに切り替えておけば、後で問題が発生したときやさらに調整が必要になったときにオンに戻すことができます。3D の操作に関連したコードがワールド内でどのように動作しているかをビジュアル化できるというのは、ゲームプレイ プログラマーにとって素晴らしいことなのです。

さて次に私は、AI パスファインド ルーチンを、「Unreal」の昔ながらの Waypoint-Pathnode ナビゲーションシステムから、新たなナビゲーション メッシュ システムに変更することにしました。すごいです! ものすごく簡単でした。しかし、結果は私の疲れきった目にも明らかでした。レベルの中にパイロン アクタを配置し、パスを作成すると、困難な作業はすべて自動的に実行されます。計算が完了するや否や (驚くほど速い) 、次のように、パスネットワークが環境の中に完全に実現されます。

ddblog2.jpg

レベルの構造が変化した場合にもパスノードの設定を再作成する必要はなく、基本的には、[rebuild paths] (パスをリビルドする) をクリックするだけで、すでに配置しているパイロンによって、すべての有効なパスを再計算するという大仕事が完了します。

ナビゲーション メッシュ システムを実際に使用するためのコードについては、これ以上ないほどシンプルです。(他のメッシュベースのナビゲーション技術を使用した経験に基づいています)。基本的には、以下に掲載したコードの数行だけが、Nav Mesh からもたらされたナビゲーション結果を AI Controller が歩くのに必要なものです。

function InitNavigationHandle()
{
   if( NavigationHandleClass != None && NavigationHandle == none )
      NavigationHandle = new(self) class'NavigationHandle';
}

event vector GeneratePathToActor( Actor Goal, optional float WithinDistance, optional bool bAllowPartialPath )
{
   local vector NextDest;

   //set our return value equal to our destination Actor’s Location.
   //In case it’s directly reachable or pathfinding fails, we’ll just return this.

   NextDest = Goal.Location;

   if ( NavigationHandle == None )
      InitNavigationHandle();

   //if the Actor isn’t directly reachable, then try to find the next navigation point towards it.
   //Otherwise we’ll just return its location to go there directly.

   if(!NavActorReachable(Goal))
   {
   class'NavMeshPath_Toward'.static.TowardGoal( NavigationHandle, Goal );
   class'NavMeshGoal_At'.static.AtActor( NavigationHandle, Goal, WithinDistance, true );
   if ( NavigationHandle.FindPath() )
      NavigationHandle.GetNextMoveLocation(NextDest, 60);
   }

   NavigationHandle.ClearConstraints();

   return NextDest;
}

次にステートコードの中で。

//WithinRange just checks a small distance from the TargetActor,
//otherwise just keep moving whereever GeneratePath tells us to go.

while(!WithinRange(TargetActor))
{
  MoveTo(GeneratePathToActor(TargetActor),none,30,false);
  Sleep(0);
}

このようなコードを AI Controller(9) に加えることによって、AI キャラクターがレベル全体で確実に進むことができるようになりました。しかも、動作が格段に効率的になりました。自分はほとんど何もしなくて済んだのです。Epic のおかげです。

パスファインドに利用し始めたばかりですが、他にもナビゲーションメッシュを利用できるものが多数あります。AI に他の情報を与えることもできます。この情報とは、カスタムな動き (例 : レッジ (棚状の障害物) を乗り越える)、あるいは動的な障害物の回避 (例 : 物理オブジェクトを回避する動き)、移動する動的なナビゲーションメッシュのセクション (エレベーターや列車など) への乗車などのために必要となる、周りの環境に関する情報です。将来、さらに多くの機能をぜひ試してみたいものです。ドラッグ & ドロップのナビゲーション機能に関する限り、非常に素晴らしいものと言えます。

新たにしっかりとしたパスファインド機能が備わったので、この日の最後のタスクに移ることができます。それは、塔を直感的に配置できる機構を設計 / 実装することです。これは特に重要です。タワーディフェンス系のミニゲームである DD (Dungeon Defense) は、ワールド内にこれらの塔をいかに自然に「楽しく」配置できるかということにかかっているためです。

まず、TowerPlacementHandler アクタ (TPH) を作成して、ワールド内に塔を配置する際に関わるレンダリング機能とロジカルな機能のすべてをカプセル化することにしました。Player Controller の PostBeginPlay() 関数を修正して、それが所有しているアクタとして自ら TPH を初期化できるようにしました。TPH は実査には物理的な実体をもつわけではありませんが、それに含まれているコンポーネントが塔配置モード時にはっきりと可視化します。

PlacingTower(10) (塔配置) という名前のステートを PlayerController に追加しました。(ステートスタックにプッシュされます)。これは、あらゆるゲームプレイ入力をロックする (正確に言うと、ほとんど空の PlayerMove() 関数によって無視する) と共に、関係のある入力イベントだけを加工するために TPH に渡します。また、これに対応する PlacingTower ステートを TPH(11) と PlayerCamera クラス (12) にも追加しました。PlayerController は、Placing-Tower (塔配置) モードに入ったときに、これらの 2 つの子アクタを自身の PlacingTower ステートの中にセットします。

Placing-Tower (塔配置) モードにある場合は、PlayerCamera がもっと極端な上からの視点に向かって補間するようにしようと思いました。そこで、Placing-Tower (塔配置) モードに入ったときのカメラの変換情報を保存し、それら前回の値から新たな「Placing-Tower (塔配置)」カメラの値に補間することにします (VLerp、RLerp)。 (13)

さて次は大物です。TowerPlacementHandler クラス本体です。TowerPlacementEntry という新たな構造体を作成しました。これには、ゲームで使用される塔のタイプに関する表現の詳細情報 (たとえば配置メッシュやコリジョン判定の範囲、塔が最終的に配置された場合に実際にスポーンされるアーキタイプなど) が含まれています。TPH にはこれら構造体配列があり、配置可能な塔をそれぞれ定義しています。 (14) 配置すべき塔の視覚的な表現のためには、TPH に SkeletalMeshComponent があります。 PlacingTower ステートが入力された場合 (また TPH アクタ自体が単に非表示解除されることによって PlacingTower ステートが非表示解除された場合)、対応する TowerPlacementEntry に基づいて、この SkeletalMeshComponent のためにメッシュが動的にセットされます。 (15)

この Tower-Placement メッシュコンポーネントをマウスに従わせるために、その変換をマウススクリーン座標の逆投影交差点に設定します。これは、プレイヤーのピッチ計算のために行ったのと同様のことです。実際の衝突点がない場合は、数学的な交点をプレイヤーアクタの平面上で使用します。 (16) また、Tower-Placement (塔配置) の位置を移動できる範囲を、プレイヤーアクタを中心としたある半径内に限定しています。 (17)

この範囲を視覚に訴える方法で表したかったため、マテリアルがアニメートするデカールが、プレイヤーの下のジオメトリ上に投影されるようにしました。DecalActorMoveable (スポーン可能バージョン) を TPH に追加しました。これは、子として デカールをスポーンすると共に、TPH が TowerPlacement (塔配置) ステートにあるか否かに基づいてデカールの視覚的なステートを管理し、さらにこのステートが入力されたときにプレイヤーに付随して DecalActor を配置します。この DecalActor は、有効な塔配置の範囲を示す円形のインジケータとして、マテリアルを使用します。 (18)

また、現在の TowerPlacement (塔配置) に関するコリジョン判定を実装しました。最終的には、新たな塔をその場所にスポーンさせるための自由なスペースを充分確保する必要がありました。そこで、一連の範囲 Trace (選択された特定の TowerPlacementEntry のサイズ値を使用します) を配置位置の周囲 (前後左右) に追加しました。その際、何かにぶつかる場合には、配置を許可しません。 (19)

次に、現在のマウス位置に塔を配置することが有効であるか否かを視覚的に示すものが必要になりました。つまり、有効な場合には塔を表すメッシュが緑になり、無効な場合には赤になるようにしたかったのです。これはマテリアルインスタンス定数 (MIC: Material Instance Constant) を使用して実現することにしました。MIC は、ゲームプレイ中にパラメータを使用してマテリアルを動的に変更するために使用するシステムです。(パラメータは通常、スカラーパラメータおよびベクターパラメータ、テクスチャパラメータの形式をとります)。

今回のケースでは、ベクターパラメータを塔のマテリアルに追加して色を変更します。そこで、エディタ内でベースマテリアルからマテリアルインスタンス定数を作成しました。これで、コード内で Mesh.CreateAndSetMaterialInstanceConstant() 関数を使用して、実際にMIC の「一意な」コピーをメッシュインスタンスに割り当てることができるようになりました。(さもなければ、MIC 内で値を変更すると、その特定の MIC を使用するすべてのメッシュに影響が及びます。これは、使い方によっては問題になる場合もあるし、ならない場合もあります)。 (20) これが完了したので、ベクターパラメータ (巧妙にも Color と名付けました) を、現在の配置位置が有効であるか否かに応じて、Green または Red に設定しました。 (21) これで望み通りの基本的なビジュアルフィードバックができました!

最後に、これをすべて TPH 内にあるステートロジックに結びつけます。これによって、Player Controller から渡されるマウスクリック / ボタン押下を処理して塔配置の確認を受け取ることができるようになります。これは簡単に行うことができます。Player Controller の PlacingTower ステート内部にある各入力「実行」関数をオーバーライドするとともに、イベントに対応する関数を TPH の PlacingTower ステートに送信します。 (22) ステートにおける関数のオーバーライドは、ステート固有の機能をカプセル化するには良い方法です。(これらの関数より汎用的なグローバルバージョンを作って機能しなくなるというようなことはありません)。プレイヤーが塔を配置する場所を確認し終わったら、回転の選択ができるようにしようと決めました。

そこで、PlacingTower の最上位にプッシュされるステートを作成しました。ステート名は PlacingTowerRotation (23) です。これには、回転を処理するための、オーバーライドされた更新 / 入力メソッドが含まれています。 これは単にマウスの投影位置に向かって塔を回転させるにすぎません。 (24) PlacingTowerRotation ステートで処理される確認は、当該の塔を実際にスポーンするとともに (25) 、シーケンス全体を完了させます。(その際、PlayerController に通知することによって、PlayerController がその PlacingTower ステートをポップさせ、カメラにも同じことをするように命令します。これによって、プレイヤーは通常の制御を取り戻します)。 (26)

このようにして、「UDK」を使ってゲームの開発をしながら、また楽しい一日が終わりました。楽しくも新しい照準スキームを実装するためにマウスとゲームパッドの入力に取り組んでいるときでも、あるいは、Epic の最新最強のパスファインド ソリューションを扱っているときでも、VFX / マテリアルのイタレーションをエディタで処理しているときでも、ユーザー入力主導型ステートの継承関係をセットアップしている場合でも、私は最も重要なものに集中していました。それはゲームプレイです。とても楽しい時を過ごすことができました。

Blog 2: ファイル レファレンス

このブログで取り上げられている情報は、次にあげるファイルに含まれています。(それらのファイルは、Dungeon Defense のソースコードです)。カンマで分けられている行番号は、ファイル内で独立した行が複数あることを示しています。ハイフンで分けられている行番号は、ファイル内で行がその範囲にまたがっていることを示しています。

  1. DunDefPlayerController.uc: 1575
  2. DunDefHUD.uc: 122
  3. DunDefPlayer.uc: 304, 329
  4. DunDefPlayerController.uc: 1611
  5. DunDefPlayer.uc: 304
  6. DunDefWeapon.uc: 134, 157
  7. DunDefPlayer.uc: 241
  8. DunDefPlayer.uc: 315
  9. DunDefEnemyController.uc: 938
  10. DunDefPlayerController.uc: 577
  11. DunDefTowerPlacementHandler.uc: 340
  12. DunDefPlayerCamera.uc: 109
  13. DunDefPlayerCamera.uc: 163-174
  14. DunDefTowerPlacementHandler.uc: 89-135
  15. DunDefTowerPlacementHandler.uc: 304
  16. DunDefTowerPlacementHandler.uc: 438, 474
  17. DunDefTowerPlacementHandler.uc: 468
  18. DunDefTowerPlacementHandler.uc: 219-238, 396-404
  19. DunDefTowerPlacementHandler.uc: 492-516
  20. DunDefTowerPlacementHandler.uc: 240-249
  21. DunDefTowerPlacementHandler.uc: 524-525
  22. DunDefPlayerController.uc: 706
  23. DunDefTowerPlacementHandler.uc: 691
  24. DunDefTowerPlacementHandler.uc: 786, 795
  25. DunDefTowerPlacementHandler.uc: 801
  26. DunDefPlayerController.uc: 602

Blog 3: 7 日目

皆さん、こんにちは。

Dungeon Defense 開発チームは、前回のブログエントリー以来かなりの範囲を処理してきました。そのため、どこから書いて良いのか決めるのが難しいくらいです。それでも、前回の投稿から数日間に渡って私たちがやり遂げたことを概観してみることにしましょう。(これらのトピックについてはそれぞれ後に詳しく解説するつもりです)。

  • 剛体の Mana Token (トークン) を追加しました。これは、敵が落とすもので、近くにいるプレイヤーが吸い込みます。塔を召喚するときや魔法を唱える場合に消費するリソースです。
  • 武器である「魔法の杖」をグレードアップできるシステムを追加しました。編集可能な構造体配列にある、一連のアーキタイプを使用します。(データ主導型システムです)。
  • 分割スクリーンと動的なローカルプレイヤーの参加をサポートするようにしました。
  • カスタム UI クラスを追加することによって、エディタ主導型のアニメーションシステムをサポートするようにしました。(Epic による既存の UI アニメーションの基盤の上に作成しました)。
  • 多数の機能的プレースホルダー UI シーンを追加しました。具体的には、メインメニュー、ポーズメニュー、ゲームオーバー UI、個々のプレイヤーの HUD、共通のグローバル情報 HUD、ロード画面です。
  • 非同期ロードをサポートするゲームロジックをセットアップしました (Seamless Travel (シームレスな移動))。これによって、レベルのロードがバックグラウンドで行われている間に、遷移画面をアニメート化することが可能になります。
  • 新たなキャラクターアニメーション ノードを追加しました。(BlendBySpeed のバリアントです。どの物理ステートが movement (動き) として見なされるかを指定するオプションと、スピード乗数の上限を伴います)。また、プレイヤーキャラクターのアニメーションツリーが上半身のブレンディングをサポートするようになりました。
  • AI の改善点 : AI がターゲットを直接見通すことができると判断したときに、パスファインドを中止させるようにしました。また、どれが理想のターゲットなのかを AI に定期的に再評価させるようにしました。さらに、フェイルセーフを追加して AI がスタックした (動けなくなった) か否かを検知させ、ナビゲーションシステムへの復帰を試みさせるようにしました。
  • 以下のような、多数の Kismet アクションを追加することによって、(特に) フルの「Tower Defense ゲームプレイサイクル」をサポートしました。
  1. 見事な、潜在的 (すなわち長期に及ぶ) 「Wave Spawner (大軍スポーナー)」アクション。これは、敵の集団を表す任意の構造体配列から長期に渡って来襲する敵をスポーンするアクションです。プレイヤーが特定の大軍を殲滅した時のために、適切な出力リンクを伴います。
  2. 敵の数と Wave (大軍) が押し寄せる間隔を動的にスケーリングするためのアクション。これによって、ゲームを手順に沿って徐々に難しくすることができるようになります。
  3. UI を開いてカスタムの情報を渡すことができる各種アクション。
  4. 「敗北条件」(Crystal Core の破壊) を検知するイベント。Crystal Core が実際に破壊される少し前に検知することによって、敗北時の早い段階でカットシーンを起動することができます。

さて、これらのトピックのいくつかについて、より詳しく見ていきましょう。まずは、剛体 Mana Token からです。

これはかなり単純です。Epic によって提供されている KActorSpawnable クラスを継承します。私は、セットアップ済みのこのクラスを利用して、静的メッシュコンポーネント (凸衝突が設定されています) に基づき、アクタに剛体物理を適用しました。 (1)

子クラスのデフォルトのプロパティをオーバーライドして、bWakeOnLevelStart=true としました (ただちにドロップするようになります)。また、bBlockActors を false に設定することによって、プレイヤーがスタックすることなくオブジェクトを通過することができるようにしました。(アーキタイプにおいて) この ManaToken には、小さな宝石のような静的メッシュをつけます。次に、DunDefEnemy が、Died() 関数においてさまざまな数の ManaToken を (アーキタイプから) スポーンするようにします。 (2) また、スケーリングした VRand() 関数 (ランダムな方向ベクター) によるインパルスをドロップした Token に適用し、敵から外に向かって飛んで行くようにしました。プレイヤーの範囲内にある Mana Token を調べて、あった場合は回収します。(つまり、破壊して Mana 値を Player Controller のトータルに加算します)。 (3) 最後に、各 Token を回収する際に実際に触れなくてもいいようにするために、定期的な OverlappingActors テストをプレイヤーの中に追加しました。(各 Token の中ではありません!)。これによって、近くにある Token すべてを見つけることができるようになると共に、プレイヤーに対して力が適用されて Token が吸い込まれるフラグを立てることができます。また、Token の速度 (velocity) の向きがプレイヤーの方向とは異なる場合に、逆方向の力をわずかに加えるようにしました。この力は基本的に「異方性の摩擦」を利用したもので、プレイヤーの方向により速く向かうようにさせるものです。 (4) ともかく、Token が飛び回り始めたとき、満足のいく吸い込み効果を得ることができました。

ddblog3-1.jpg

さて次に、ゲームプレイ中に起きる武器のグレードアップに関するサポートです。そのために、私は Summoning Tower (塔の召喚) ステートを PlayerController で拡張しました。(このステートは、基本的に入力をロックし、プレイヤーキャラクターに召喚アニメーションを再生させるものです)。 (5) この子ステートの名前を UpgradingWeapon としました。いくつかの対応する関数をオーバーライドして、異なるアニメーションとビジュアルエフェクトを再生するようにしました。 (6) このように、元となるステートの機能すべてを利用しつつ、必要なオリジナルの新機能だけを実装することができるのです。ステートの継承関係はゲームプレイプログラミングにとって、とてつもなく有益なコンセプトです。言語的な観点から見れば、UnrealScript に独特のものと言えます! さてこれで、Upgrade (グレードアップ) ボタンを押すだけで、プレーヤーがユニークなアニメーションを再生することができるステートにさせることができましたが、ここで武器に何らかの処理をしなければなりません。

Weapon Upgrade Entries (武器グレードアップ エントリ) という名の構造体配列を追加しました。これは、各グレードアップ レベルに関する情報を格納するためのものです。この情報に含まれているのは、Mana コスト、説明、グレードアップに必要な時間、(最も重要な情報として、) グレードアップが完了したときにスポーンしてプレイヤーに渡される武器のための Weapon Archetype Reference (武器アーキタイプの参照) です。 (7) なぜ構造体 (単に値しか含まれません) を使用してクラスを使用しなかったのでしょうか? それは、構造体がエディタのプロパティ エディタ内で動的に作成できるからです。これによって、Weapon Upgrade Entries 構造体配列の値をエディタ内で設定して、システム全体をデータ主導型にしておくことができるのです。

次に、サポートされている各グレードアップのレベル (最大レベル 5) に対応するエントリを格納する列挙型変数を追加しました。また、プレイヤーがグレードアップするたびに増える列挙型変数の値 (現在の列挙型変数の値 + 1) を決めました。これは、構造体配列の次の Weapon Upgrade Entry を得るための添字として使われます。PlayerController 内部で Upgrading Weapon の状態 (ステート) のまま待機します (グレードアップのためのループアニメーションが再生された状態にあります)。これはグレードアップのための構造体のエントリが指示する時間だけ続きます。その時間が切れたときに、新たな武器のためのアーキタイプがスポーンされます (古い武器を破壊するわけです)。 (8) これらは全部うまく機能しました。PlayerController のアーキタイプがもつ Weapon Upgrade Entries 構造体配列に値すべてが格納されているため、武器グレードアップに必要なコストと時間を微調整するイタレーションは、リモートコントロールを通じてリアルタイムにエディタ内で処理することができることになります。これこそ効率というべきものですね!

ddblog3-2.jpg

また、分割スクリーンをサポートしようと考えました。1 人でも楽しくなってきたのだから、4 人のプレイヤーならば 4 倍楽しくなるはずです! (まあ、そんな感じでしょうか)。

分割スクリーンによるマルチプレイヤーをサポートするのは、非常に簡単です。これもまた、Epic が提供してくれる強力なフレームワークのおかげです。実際には次のようにすればいいだけでした。すなわち、まだプレイヤーが関連づけられていないコントローラーに関する Press Start (押してスタート) ボタンの入力を処理するとともに、その新たなコントローラー ID を使用して CreatePlayer 関数を呼び出しただけなのです。Input のサブクラス内にプレイヤーをもたないゲームパッドの Press Start 入力については、InputKey 関数で処理しました。プレイヤーがゲームパッドの Start を押すと、このキーの名前が InputKey 関数に渡され、それに対応する ControllerID が使われて CreatePlayer が呼び出されます。 (9) InsertInteraction() 関数を使って、この新たな Input クラスを ViewPortClient クラスの Interaction (インタラクション) リストに追加しました。それだけでお終いです。(10) Player #2 が Start を押すと、第 2 の PlayerController とそれに関連づけられているプレイヤー - ポーンが立ち上がり、さらにこれに応じてビューポートが自動的に分割されます。(分割スクリーンを望まない場合は、ViewportClient クラスで UpdateActiveSplitscreenType()(11) 関数をオーバーライドします。その場合、第 1 のプレイヤーのカメラ視線で全体の描画が行われることになります)。さてこれで、1 人のプレイヤーしか経験できなかったことが、複数のローカルプレイヤーも楽しめるようになりました。オンラインによるマルチプレイヤーの場合は、アクタ複製システムを使用するため、これよりも手間がかかります。それでも、Epic が提供する既存のフレームワークによって、それほど大変な手間にはなりません。これについては次回掲載時に解説することにします。

ddblog3-3.jpg

次に、ゲームで使用するための、基本となる機能的な UI に取り組んでみました。これによって、ゲームが完全にプレイ可能なシステムとして機能することになります (1 つのレベルにとどまらず、メインメニューから勝利の確定までの全プロセスにおいて)。UI アニメーションシステム (非常に強力なシステムです) について検討してみましたが、デフォルトのプロパティを通してしか編集することができません。そこで、UnrealScript の機能を利用することによって、UI アニメーションクラスの値を構造体にラップするとともに、拡張した UIScene クラス内で編集できるようにしました。 (12) カスタムの UIScene クラスが起動すると、これらの構造体の値が動的に作成される UI アニメーション オブジェクトにコピーされるようにします。 (13) このようにして、Epic によって作られた既存の UI アニメーション システムを利用しながらも、エディタ内でアニメーションの値を編集したり試したりすることができるようになったのです。

この新たな機能が準備できたので、単純なプレースホルダー UI を多数作成しました。これらの UI には、プレイヤー HUD UI (14) (カスタムの HUD クラスによって開かれます) のように、各プレイヤーのビューポートで描画されるためのものもあれば、1 人のプレイヤーによって所有されるのではなくグローバルでフルスクリーン用のものもあります。GameInfo クラスに関数をいくつか書いて、ゲームの持続的なステートに直接基づくグローバルな UI を表示してみました。(たとえば、建造局面 (build phase) における残り時間や、戦闘局面 (combat phase) での残っている敵の数などです)。 (15) UI のためにささやかな (プレースホルダー) オープンおよびクローズのためのアニメーションを作成しました。(エディタ内で調整します)。

ddblog3-4.jpg

これがうまく行ったので、ロード用の UI をアニメート化することにしました。 (16) これによって、メインメニュー (実際はメインメニュー UI を開くレベル) からゲームプレイレベルまで、見事な遷移が可能となります。「退屈する瞬間がまったくない」というような願望に基づいているわけです。これは、Epic の Seamless Travel (シームレスな移動) を使用することによって可能となります。これは、レベルをバックグラウンドでロードする際に、他のレベルを一時的な「遷移」マップとして使用するものです。私のゲームの場合、遷移マップは Loading Screen (ロード画面) の UI Scene (シーン) を開くものです。これは遷移マップが閉じられるまで表示されます。そのとき、目的のレベルのロードは完全にバックグラウンドで完了しています。 WorldInfo.SeamlessTravel (17) を呼び出すだけで、.INI ファイルで指定された遷移マップに入るとともに、最終目的のレベルがバックグラウンドでロードさることになります。シンプルで強力です。

ddblog3-5.jpg

もちろん、いわゆる「レベルストリーミング」の機能も備わっています。これは、ゲームプレイの進行中にレベルのある部分 (たとえば、ある建物の最初の部屋に入った時の内部) をストリームしたり、古い部分 (たとえば建物の内部に入ったときの外部のワールド) をアンロードしたりする機能のことです。ワールドが大きいゲームには特に役立ちますが、これはまったく別のプロセスです。Kismet とワールドエディタを使って処理することになります。Epic による完全なドキュメントが、 レベルストリーミングの手順 に掲載されています。

次に気がついたことは、キャラクターの動く速度を動的に調整することができると、キャラクターが動くアニメーションの再生レートの処理に非常に有益だということです。このためには、アニメーションツリーノードがすでに Epic によって用意されていますが、これに少しばかり機能を追加したくなりました。つまり、プレイヤーが特定の物理ステート (すなわち地面を歩いているステート) にある場合にのみ速度のスケーリングを行うようにするとともに、再生レートのスケールに上限を設定したかったのです。こうすれば、何らかの理由によって (たとえば、爆発から大きなはずみを受けた場合など)、プレイヤーが極めて速く動くときに、動きのアニメーションが結果的に不自然に見えることはありません。ありがたいことに、この作業は簡単でした。新たなアニメーションノードクラスは Epic の AnimNodeScalePlayRate から継承させて、Tick 関数を追加しただけです。Tick 関数では、オーナーである骨格メッシュのアクタが現在動いている速度をチェックします。(私の望みであったクランピング (値の制限) と物理チェックを実行しています)。 (18) この新たな Tick 関数をサポートするために TickableAnimNode インターフェースを作成するとともに (19) 、ポーンクラスに関するノードを OnBecomeRelevant() 関数に登録しました (また、OnCeaseRelevant() の登録を解除しました)。これによって、ポーンはノードをティックすることを知るようになります。エンジンの基本クラスを拡張して自分のクラスを作成し、UnrealScript を使って新たな機能を追加すれば、フレームワークのパワーを最大限に活用できます。これは、Kismet の機能を追加するときに、はっきりすることでもあります。そして、これが次に私がおこなったことなのです!

(また、CustomAnimation ノードを追加しました。これは、キャラクターの上半身だけで再生されるようにフィルタリングがかけられたノードです。これの親としては、Spine(背骨) ボーンよりも上方をフィルタリングするように設定された AnimNodeBlendPerBone クラスを使用しています。したがって私のキャラクターは、脚を独立して動かしながら反応できるアニメーションを再生することができるのです)。 (20)

さて、ローカルのマルチプレイヤー、機能的 UI、基本的なリソースと武器のグレードアップシステムがすべて終わったので、これらすべてをタワーディフェンス ゲームのサイクル (スタートから終了まで) の元にまとめることにします。これには少しばかりレベル スクリプティングをうまく扱う必要があります。(ハードコーディングすることもできるのですが、かなり不自然なものになり、他のゲームタイプやレベルへの拡張性も犠牲になってしまいます!)。そこで、Kismet を使用して この Build and Combat (建設と戦闘) サイクルを制御することにします。このサイクルは、基本的に次のような構成になっています。すなわち、プレイヤーに塔を建設する時間を与える (UI を通じて時間を通知する)、敵の Wave (大軍) をスポーンする (UI を通じて敵の数を通知する)、このサイクルを繰り返しながら敵の数と襲来インターバルを手順に沿ってスケーリングし、抗い難くなるまで徐々に難しくしていく。素晴らしい..^^

まず、潜在的な Kismet アクションを使用して敵の Wave (大軍) をスポーンさせます。このアクションは、すぐに終了 / 出力するのではなく、内部的に長期的に更新し、内部ロジックによる指示があったときにのみ終了します。独自の Enemy Wave Spawner (敵大軍スポーナー) アクションを作成しました。(SeqAct_Latent を拡張しているため Update() 関数をもちます。 (21) )。これには、構造体配列が含まれており、それぞれの構造体は、このアクションが開始されてから一定時間後に現れる敵の Wave (大軍) を定義しています。 (22) この Enemy Wave Spawner Kismet アクションは、敵の Wave (大軍) を全滅させたときにのみ終了し、最終的出力をアクティベートします。 (23)

次が特に興味深い部分です。Wave Entries 構造体配列をこのアクションのプロパティ内で直接編集することは可能でしたが、一方で、これらの Wave Entries を複数のスポーナー間で回す必要があり、また、手順に沿って値をスケーリングし、さらに、UI による情報 (「倒すべき敵の数」) として編集する必要もありました。 (24) そこで、SeqVar_EnemyWaveEntries という新たな Kismet Variable(変数) クラスを作成することにしたのです。このクラスは、構造体を単に内包しているものです。 (25) この Kismet Variable オブジェクトが、Variable 入力として Wave Spawner Kismet アクションに渡され、自らの使用のために構造体をコピーするのです。 (26)

Kismet Variable オブジェクトを使用して Wave Entries 構造体を使用することで (Wave Spawner アクション内で直接編集可能な値を使用するのではなく)、Wave Entries 構造体を Kismet 内にビジュアルに回すことができるようになりました。これによって、Kismet 内の Wave Entry 変数を、自分で書いた他のアクションである ScaleEnemyWave にリンクすることが可能になります。ScaleEnemyWave は Wave (大軍) のエントリと float 型を入力として受け取ります。これらは 敵の数と敵襲来のインターバルのためのもので、Wave に乗じる倍率です。 (27) Combat (戦闘) サイクル後に Multiply Float Kismet アクションを使用してこれらの float 型の倍率を変更することによって、ラウンドごとにゲームを徐々に難しくすることができます。まもなくこのシステムを使ってさらに多くのことを実行するつもりです。たとえば、Wave (大軍) がランダムなアーキタイプを持てるようにしたり (これによってどのようなグループの敵に遭遇するか、はっきりとは分からなくなります)、スケール値に RandomFloat 変数を使用してスポーンされる敵の数とペースが、常に少しずつ変化するようにしたりすることなどを計画しています。

ddblog3-6.jpg

要するに、Kismet のおかげでレベルが Play-In-Editor (エディタ内再生) によるイタレーションにマッチするため、よりユニークなシーケンス (区切りとなる回数の Wave (大軍) 襲来時に付加的イベントを追加するなど) を作成することが可能となるのです。 (付加的イベントとは、たとえば Wave (大軍) 襲来 5 回ごとに凄い敵と戦うことになるようなことです。ゲームプレイ のオブジェクトがアーキタイプであるため、このようなイベントを実現するのは簡単です。) これから数日間、Kismet を使って Build-Combat-Wave (建設 - 戦闘 - 大軍) のサイクルを調整することになります。これは、設計者主導の、とても楽しい体験となりそうです。次回まで、作り続けましょう!

Blog 3: ファイル レファレンス

このブログで取り上げられている情報は、次にあげるファイルに含まれています。(それらのファイルは、Dungeon Defense のソースコードです)。カンマで分けられている行番号は、ファイル内で独立した行が複数あることを示しています。ハイフンで分けられている行番号は、ファイル内で行がその範囲にまたがっていることを示しています。

  1. DunDefManaToken.uc: 8
  2. DunDefEnemy.uc: 208
  3. DunDefPlayer.uc: 351
  4. DunDefManaToken.uc: 62
  5. DunDefPlayerController.uc: 812
  6. DunDefPlayerController.uc: 1266
  7. DunDefPlayerController.uc: 69
  8. DunDefPlayerController.uc: 1316-1320, 1280
  9. DunDefViewportInput.uc: 15
  10. DunDefViewportClient.uc: 474
  11. DunDefViewportClient.uc: 226
  12. DunDefUIScene.uc: 11
  13. DunDefUIScne.uc: 36
  14. DunDefHUD.uc: 27
  15. Main.uc: 223, 333, 482, 132
  16. Main.uc: 482
  17. Main.uc: 488
  18. DunDef_AnimNodeScaleRateBySpeed.uc: 17
  19. DunDefPawn.uc: 285
  20. DunDefPlayer.uc: 163
  21. DunDef_SeqAct_EnemyWaveSpawner.uc: 162
  22. DunDef_SeqAct_EnemyWaveSpawner.uc: 14
  23. DunDef_SeqAct_EnemyWaveSpawner.uc: 198, 231
  24. DunDef_SeqAct_OpenKillCountUI.uc: 31
  25. DunDef_SeqVar_EnemyWaveEntries.uc: 10
  26. DunDef_SeqAct_EnemyWaveSpawner.uc: 176
  27. DunDef_SeqAct_ScaleEnemyWave.uc: 53

Blog 4: 10 日目

勇気ある Unreal ファンのみなさん、ご機嫌いかがでしょうか。

前回のブログから数日経ちましたが、その間に我がチームは小規模ながら、また休日だったにもかかわらず、長足の進歩を遂げています。まず概観の説明、その後に各トピックの詳細に入っていきます。

*今回初めてアートワーク (骨格メッシュ) に取り組みました。クールなキャラクターデザインのために! ちょっと気取ったファンタジー常套句ですね! さて、後はアニメーションのために調節して、悲運の UT ロボットを取り替えます。これによってゲーム独自のファッション感覚に磨きがかかります。環境への更なる作業に加えて、武器 Mage Staff (魔法使いの杖) のベースとなるモデルを作りました。仮のビジュアルエフェクトの段階ですが、うまく機能しています。

  • 我慢できずに、メインメニュー上で気の利いた Render Target が使えるように実装してしまいました。つまり、1 ~ 4 プレイヤー用に、各プレイヤーキャラクターのアニメート化されたイメージをメインメニューで表示できるようになりました。(結局、メインキャラクターの色を換えたバージョンになるのですが)。これによって、誰がゲームにサインインしているかが分かります。接続しているゲームパッドがあればメニュー表示中に Start を押すことによって、それに続くゲームプレイにプレイヤーがサインインできます。また、このサインインに反応して render to texture (テクスチャへのレンダリング) のキャラクターが「アクティブな」アニメーションを再生します。(キャラクターは選択されていない場合にグレイアウト (灰色になって) 「待機中」の状態になっています)。カッコイイ!
  • メインメニューをさらに調整して、小さな Canvas パーティクルシステムを作成しました。これは、カーソルの位置からパーティクルを発射するものです。このシステムは、将来他の UI エフェクトにも使用することができます。
  • 私は狡猾にも、Matinee を使用してシネマティックス状態にあるときにプレイヤーが動いたり発射したりできないようにしてしまいました。ゲームプレイの導入部とゲームオーバー部のシネマティックスを実装する際に、Player Controller 上に適当な入力ブロックステートを設定したのです。また、プレイヤーが Start/Escape を押したときにシネマティックスをスキップするためのカスタムソリューションを実装しました。
  • プレイヤー用 HUD にはかなり精力を傾け、だいたいのことは実装してしまいました。たとえば、マテリアルベースのヘルス値 / 進行状況バー (カスタム UI コントロール)、ステート反応型の魔法のアイコン、アニメート化された Mana Token インジケータなどです。また、HUD のオーバーレイ表示 (動的 Canvas 描画) も実装しました。塔 / Core 上にヘルス値バーが流れます。さらに、攻撃されている Core を指し示すウェイポイントが回転します。これらすべてが 2 ~ 4 の分割スクリーン上で正しく表示されます。素晴らしい。
  • 武器のためにインパクト デカールを実装しました。これには、Epic の非常に強力な Material Instance Time Varying (マテリアルインスタンス時間変化) システム (非常に長い名前です) も使用しています。
  • 遠隔攻撃をする敵が Archer (弓の射手) 軍団として機能するのための基本的な機能を実装しました。ステートを継承することによって楽に実装できました。ただし、望みどおりの動作をさせるために AI を調整しました。(これには、照準予報 (aim-prediction) や故意による誤差 (deliberate inaccuracy)、発射物方向に関する「補正係数」(fudge-factor) と私が好んで呼んでいるものなどが含まれます)。

さてまずは、メインメニュー上の Render Target について説明します。ちょっとしたサインイン用の UI を作ってみることにしました。この UI では、誰がゲームに参加しているかを確認することができることにします。また、全員がメインメニューでサインインすることができるため、ゲームプレイが開始された瞬間に準備が整った状態で迎えることができるものとします。(もちろん、ゲームプレイ中に新たなプレイヤーを動的に追加することもできるようにします。その場合は、Start ボタンを押すだけです)。また、だれがゲームに参加しているかは、かっこいい 3D キャラクターによってビジュアルに表示されるようにします。そこで、4 つの骨格メッシュをメニューレベルの環境に追加することから始めました。これらは虚ろな空間に離れて配置され、4 つの骨格メッシュだけがレンダリングされ、バックグラウンドを構成するものはありません。各メッシュの前に SceneCapture2DActor を配置しました。(これは、カメラのようなアクタで、その視界からシーンをテクスチャにレンダリングするものです)。さらに、これらのアクタにユニークな Render Target テクスチャを割り当てました。

ddblog4-1.jpg

次に、これらのテクスチャの 1 つを使用するマテリアルを作成しました。さらに、このマテリアルのマテリアルインスタンス定数を作成しました。これによって、4 つの一意なベースマテリアルを作成しなくても、簡単に、他の Render Target のためのテクスチャパラメータをスワップアウトできるようになります。マテリアル インスタンス定数は、文字通りマテリアルのインスタンスであって、それぞれ一意の「パラメータ」(通常は、スカラーまたはベクター、テクスチャ パラメータなど) をもつことによって、マテリアル全体のコピーがそれぞれに含まれなくとも、インスタンス単位ベースで値をスワップアウトすることができます。これによって、ゲームの稼働中でも、このようなパラメータを動的に変更して、動的に反応するマテリアルエフェクトを実行できるようになります。また、メモリの節約になると同時にマテリアルのアセットを簡単に管理できるようになります。(値を少し変えるためだけに、マテリアル全体をコピーしなくても済むようになります)。

ddblog4-2.jpg

マテリアル自体の中では、Render Target テクスチャを Emissive (エミッシブ) 出力に渡しました。ただし、カメラに使用するように設定してある緑のバックグラウンドカラーによって、Opacity Mask を囲む処置もとりました。これによって、背景が見えず、キャラクターだけが出来上がるイメージの中で見えるようになります。つまり、マテリアルを使用してのみ、キャラクターのメッシュを見ることができるということです(ピクセル等倍表示によって。また Render Target テクスチャ全体の正方形のイメージではなく)。さらに、マテリアル スカラー パラメータを追加して、出来上がるイメージの輝度を制御できるようにしました。これによって、選択されていないキャラクターを薄暗くすることができます。次に、UI Image コントロールを、選択した UI に追加して、それぞれの UI Image コントロールを一意な MIC (マテリアル インスタンス定数) に渡します (プレイヤー 1 は プレイヤー 4 を使って)。

これらのキャラクターメッシュが Epic の SkeletalMeshActorMAT を継承するカスタムクラスを使用するようにさせていることに注意してください。(SkeletalMeshActorMAT は、オンデマンドでアニメーションを再生する必要がありながらも、ポーン全体の再生が必要ない場合に適する動的な骨格メッシュ アクタ クラスです)。私がカスタムクラスを使う理由は、ブレンドしたアニメーションをコードで簡単に再生することができるからです。 (1) これらのことを Kismet と Matinee を使用して行うことはできたでしょうが、プレイヤーが直接サインインするイベントを処理することが難しくなるのです。

実際私は、プレイヤーキャラクターのサインインイメージのために UI Image コントロールを直接使用するのではなく、UI Image を拡張したカスタムのコントロールを作成しました。これによって、必要な関数をカスタムのコントロールに追加し、プレイヤーのサインインに反応して、対応するキャラクターメッシュと MIC を操作することができるようになりました。(このようなことを他のどこかで実行する必要がなくなりました)。 (2) 特定の機能を追加する必要がある場合は、Epic の豊富な基本クラスを継承するカスタムの UI コントロールを作成することが、大変有益なアプローチとなります。これらのカスタム UI を作るときに、私は実際何度もそうしました。^^

それはさておき、最後のステップは、Press Start (押してスタート) ボタンイベントをコードでキャプチャして、それに応じて新たなプレイヤーを作成することです。カスタム関数をカスタムの Player-Select UI Scene クラスの OnRawInputKey デリゲートに割り当てることによって、容易に行うことができました。(カスタムの UnrealScript 機能をもたせるには、カスタムの UI Scene クラスを使うことを忘れないでください!)。この関数の中では、InputKeyName パラメータがゲームで使用する Start ボタンの 1 つであるか否かが判断されます。もしそうであれば、そのコントローラー ID を使って新たなプレイヤーを作成します (ViewPortClient の CreatePlayer 関数を用います)。ただし、この場合もちろんのこと、そのコントローラー ID に関連づけられているプレイヤーが存在していないことが前提となります。 (3) それを受けて、カスタムの UI Scene は、カスタムの Player-Select Image コントロールに更新を命令します。 (4) そこで、コントロールはそれぞれマテリアルを更新し、指定されたインデックスに対応するローカルのプレイヤーが見つかった場合には、対応する 3D キャラクター上でアニメーションを再生します。 (5) 何と、参加するローカルプレイヤーに対応したオンスクリーンのキャラクターがアニメートされました!

ddblog4-3.jpg

以上が完了したので、次にメインメニューを少しばかり洒落たものにしたくてたまらなくなりました。それには、カーソル (まもなくカスタムのテクスチャに置き換えるつもりですが...) から魔法のパーティクルが放たれるようにします。そのためには、ゲームの ViewPortClient クラスにある PostRender() 関数をオーバーライドし、ゲームの MapInfo を調べて現在のワールドに menu のレベルのフラッグが立っているか否かを判断します。menu レベルであった場合は、Canvas.DrawMaterialTile を使用してパーティクルの構造体配列によって定義されているパーティクルを手動で描画します。 (6) それぞれの構造体にはパーティクルに関する情報が含まれています。具体的には、位置、サイズ、速度、加速度、ライフスパンです。 (7) パーティクルの更新は ViewPortClient クラスの Tick() 関数で行います。速度を使用して位置を増分させ、加速度を適用し、ライフスパンを減少させ、パーティクル終了時にリセットします。 (8)

後に、Canvas パーティクル システム フレームワークを拡張して、事前定義した「デフォルト プロパティ」のアニメーション値をサポートすることを計画しています。これは、入力イベントなどに反応する UI パーティクルを作って、より楽しくメニューでクリックできるようにするねらいがあります。素晴らしいことに、このようなことを実現するカスタムのソリューションは、Epic の柔軟なフレームワークによってサポートされています。「Unreal Engine 3」に強力なツールがあるだけでなく、ちょっとしたパーティクルシステムのメソッドを自作しようとするときに、UnrealScript の柔軟性を利用することができるのです!

ここでゲームプレイ関連に戻ります。レベルの開始をもっとエキサイティングなものにするとともに、防衛しなければならない環境の概観をプレイヤーに表示することにします。このようなシネマティックスを作成する場合、Matinee は扱いやすいツールです。また、フレームワークのおかげで、簡単に着手することができます。「Unreal」の Player Controller クラスは、Director トラックを含む Matinee であれば、そのビュー (視点) をデフォルトで使用します。(単なるスクリプト化されたゲームプレイ シーケンスではなく、cinematic (映画の) マチネーとして考えられます)。

ただし、入力を正しくブロックして、プレイヤーがシネマティックスの間にうろうろしないようにしなければなりません。(あるいは、シーケンスに関係のない場合にはキャラクターを非表示にすべきでしょう)。これは、PlayerController の NotifyDirectorControl イベントをオーバーライドするだけで簡単にできます。このイベントにはパラメータが含まれており、プレイヤーがシネマティックス シーケンスに入ったのか、あるいはそこから出たのかが示されます。Director の制御に入った場合は、ほとんどの入力関数を無視する (オプションで Controller のポーンアクタを非表示にすることもできる) InCinematic というステートが PlayerController にプッシュされます。Director の制御から出る場合は、そのステートがポップされます。さてこれでOKです。もうカットシーン中に動き回ることはありません。 (9)

ただし、プレイヤーがシネマティックスをスキップできるようにもしたかったのです。(カットシーンがスキップできないゲームは少なくなく、ゲーマーの悩みの種ですよね!)。これを実現するには、StartFire 実行関数 (および、このゲームで Escape キーを押下することに対応する入力関数) をオーバーライドします。武器を撃つかわりに (もちろん、シネマティックスの間にそのようなことをしても意味はありませんが)、そのレベルの Kismet にあるすべての SeqAct_Interp (matinees) についてイタレートして、どれが現在 Player Controller を制御している Director トラックをもっているのかを調べ、その Matinee の PlayRate(再生レート) を 10000 にセットすることによって、1 フレームで終了させます。(その後、PlayRate を元の値に戻します)。 (10) あっという間に終了しました。^^

ゲームのシネマティックスがある程度良くなってきたので、そろそろプレイヤーの HUD を改良する時期だと判断しました。それには HUD を移動します。すなわち、HUD クラスの PostRender にある DrawText コールから、ゲームプレイのイベントに反応してアニメート化されたグラフィックスを使用する実際の UI Scene に移すことにします。この新たなスーパー HUD に何よりも必要なものは、ヘルスのパーセント値と魔法がかかっている時間を表示するプログレスバー形式のグラフィックです。「パーセント」のスカラーパラメータに対応する (注) プログレスレイヤーをマスクする自家製マテリアルを使って、自作することにしました。(注 : この意味は、表示すべきパーセントをマテリアルインスタンスに通知すると、全体のフレームのうちその分のプログレスバーのイメージだけを表示するということです)。 この HUD はマテリアルであるため、UI Scene 内の UI Control でも使用できる上 (プレイヤーの HUD として) (11) 、それだけでなく DrawMaterialTile を使用する動的な Canbas オーバーレイとしても使用することができます (12) (塔のアクタの最上部にヘルスインジケーターとして浮かべることができます)。すばらしい。

唯一気をつけなければならないことは、このような用途でマテリアル インスタンス定数 (MIC) を使用する場合は、描画する各プログレスバーのために「一意の」マテリアル インスタンス定数を作成しなければならないということです。(オリジナルの MIC を各 MIC の「親」とします)。そうしない場合は、このゲームの「パーセント」値のような MIC パラメータを設定すると、プログレスバーのすべてに影響を与えてしまうことになります。(なぜなら、すべてのプログレスバーが同一の MIC をもっているからです)。そこで、このゲームでは単に、各 UI HealthBar インスタンス (13) (すなわち、「ダメージ付与可能な」インターフェースを実装するアクタ (14)) に、自身の新たな MaterialInstanceConstant (マテリアルインスタンス定数) を初期化させると共に、オリジナルの MIC をその親として設定しました。このようにして、ヘルス / プログレスバーシステムに柔軟性をもたせることによって、UI Scene でも 浮遊型 HUD でも使用できるようになりました。同時にすばらしいマテリアルのアニメーションもサポートされているため、まさに「魔法のように」に見えます。(つまりは、スクロールするブレンドされたレイヤーが複数あるということなのですが)。

次に、魔法のアイコンを作成します。これはプレイヤーが現在行っていることに対応して視覚的なステートを更新します。アイコンが表示するのは次のようなことになります。魔法を使用する余裕があるか、魔法は現在使用可能か、魔法は唱えられているところか、魔法はかけることが可能かなどです。また、魔法をかける際に対応する入力ボタンを、これらの魔法アイコンが動的に表示するようにします。そのためには、UI Image を継承した Spell Icon を作成し、そこに 2 つのパラメータを加えることによって、2 つの追加された UI Label への参照を含むようにします。1 つは、魔法が消費する Mana の量を示すためのもので、もう 1 つは、魔法を使用するときに押すテキストボタン (あるいはゲームパッドのボタンイメージアイコン) を表示するためのものです。

Spell Iconコントロールは、どの Player のステートが変化したときでも更新します。また、対応する魔法の使用可能性を Player Controller に問い合わせて (15) 、その結果に基づいてビジュアル (カラー / オパシティ) を更新することによって一意のステートをそれぞれ表示します。さらに、必要な場合は対応するラベルの中に数字を表示します (16) 。これらを簡単にできた要因は 2 つあります。 (1) UI コントロールが、Scene 内にある他の UI コントロールへの参照を含むということ。 (17)  (2) UI コントロールが、それの親である UI Scene を分かっており、親である UI Scene がそれの Player オーナーを分かっているということ。これによって、表示される結果が UI HUD を所有しているプレイヤーに固有なものになり、その際、情報を得るための参照を余計に追加する必要は一切ありません。

また、すべてのグラフィックスを分割スクリーンにきれいにそろえて配置するには、「Percentage Owner (パーセンテージ オーナー)」の位置計算を利用するとともに、元のアスペクト比を保つ必要があるイメージ (たとえばボタンアイコンです!) のために「Scale Height (スケール 高さ)」のアスペクト比サイズ調整オプションを利用すればよいことが分かりました。この分割スクリーンの検証は実に簡単です。直接 UI エディタ内で全スクリーン UI ビューモードを切り替える機能を利用します。もの凄く強力な機能です。最後に、数日前にエディタ向けにした UI アニメーション システムをさらに活用します。すなわち、コントロールそれぞれがステートの変化に反応するアニメーション シーケンスを作成します。つまり、ステートの変化 (たとえば無効になった場合や、再度有効になった場合) を反映して跳ね、時間経過に伴って (即座にではなく) 色が変化するようにします。これによって、なかなか良いフィードバック感覚が少しばかり加わりました。この作成プロセスは楽しいものでした。プロセス全体が、UI エディタ / PIE (Play in Editor) 内で行うデータ主導のイタレーションだったからです。

ddblog4-4.jpg

最後に、「Crystal Core」が攻撃されている場合に、プレイヤーに知らせる必要があります。(それを守るのが第一の任務なのですから!)。また、それに対応させて Crystal Core を指し示す浮遊型 HUD ウェイポイント インジケータを表示する必要があります。そのために、HUD Overlay インターフェースを Crystal Core 上に実装しました。(PostRender の中でプレイヤー HUD は、このインターフェースを実装している動的なアクタをすべてイタレートし、その上にある DrawOverlay 関数を呼び出します)。新たに実装された DrawOverlay 関数内で、Crystal Core は最近攻撃を受けたか否かを調べます。もし攻撃を受けていたら、Crystal Core の DrawOverlay 関数が、ウェイポイントアイコンをともなった DrawRotatedMaterialTile() 関数の呼び出しを行い、Rotation (回転) が、(Canvas の中心から) Crystal Core の投影されたスクリーン位置への方向として計算されます。さらに、Rotated Material Tile (回転したマテリアル タイル) に渡される位置については、その回転の方向におけるスクリーンの中心からのオフセットを計算します。このようにして、ウェイポイントが中心を巡って円を描くように (羅針盤と同じように) 動くようになりました。 (18) 単なるテクスチャではなくマテリアルを使用したため、(マテリアルの中で) ウェイポイントアイコンを跳ねさせたり、光らせたりすることによって注目をひくことができるのです!

次に、魔法の杖の発射物によるインパクトをもっと派手なものにします。インパクトポイントでスポーンするパーティクルエミッタ (ライト コンポーネントを伴って) は、すでに優秀なものが用意されていますが、これに何を付加できるでしょうか? デカールです、デカール! 単に、旧式の静的なデカールではいけません。Epic のもの凄く強力で、もの凄い名前の Material Instance Time Varying (マテリアルインスタンス時間変化) システムを使用することにします。このデカールならば芸術的にアニメートするはずです。

デカールは発射物のインパクト上で簡単にスポーンさせることができます。発射物の Explode 関数内で、WorldInfo.MyDecalManager.SpawnDecal() 関数を単に呼ぶだけです。その際引数として、発射物が衝突する HitLocation (衝撃位置) と negative-HitNormal (負の衝撃法線)、さらに、デカールのサイズとライフスパンに対応する値を渡します。もちろん、SpawnDecal() 関数には、発射物アーキタイプのマテリアルインスタンスを渡します。この汎用的な参照は、静的なデカールマテリアルか動的な MITV (Material Instance Time Varying) のどちかになります。MITV の場合は (GenericMaterialReference.IsA(‘MaterialInstanceTimeVarying’) を使って調べます)、新たな MaterialInstanceTimeVarying をそこから作成します。これは、スポーンさせるデカールそれぞれについて、一意にアニメートさせるために必要となる処置です。MITV の有効期間は GetMaxDurationFromAllParameters() にセットします (これによって、マテリアルのアニメーションが命じるまで継続することになります) 。これを SpawnDecal() 関数に渡すと、アニメート化されたデカールが発生するのです! (19)

ddblog4-5.jpg

MITV によって、デカールをアーティスト主導で簡単にフェードすることができます。(数秒間に渡ってオパシティパラメータを 1 から 0 に補間するためのキーフレームをいくつか追加するだけで済みます)。ただし、用途はこれだけではありません。たとえば、弾丸による穴が黒くなるまで赤熱する場合や、デカールを使用しない場合でも、マテリアルが複雑なループをなして脈動する場合、さらには、マテリアルがゲームプレイイベントに反応して動的にアニメートする場合などが考えられます。発射物のインパクトに反応してデカール上にスポーンさせる単純なコードを書き上げると、後は、アーティスト主導でシステムをイメージどおりにすべく、かなり多くの作業が行われることになります。これから数日間は、Dungeon Defense の VFX アーティストがこの作業を楽しむだろうと思います。

最後に、より核心的なゲームプレイ コンテンツ (このゲームの場合は乱戦) の実装に戻ります。遠隔攻撃を行う敵を加えることによって乱戦する部隊を補完します。遠隔攻撃を行う敵のポーン自体はほとんど空のクラスで、敵ポーンの基本クラスを継承します。ただし、その AI Controller には少しだけユニークな機能を追加します。発射物を撃つために立ち止まった地点から遠距離攻撃できる機能 (これは当たり前ですが) に加えて、汎用的な Attacking ステートを基本 Enemy AI Controller (この場合) から拡張することによって、攻撃期間中の正確な時間に遠隔発射物をスポーンさせるのです。(特に、発射アニメーションが再生されているときにポーンから渡されるアニメーションイベント時に)。 (20) この汎用的な Attacking ステートがより抽象的なロジックを扱える点に私は満足しています。たとえば、敵が攻撃中にダメージを受けた場合にステートをポップさせるロジックや、特定の RangedAttacking ステートが特定の発射物発射行為を行う際に、Last Attacked Time (前回攻撃時) 値を設定することによってインターバルのチェックを行う (その間、敵の乱戦部隊の MeleeAttacking ステートは、以前のブログで述べたように乱戦の Trace を行う) ロジックなどです。

ステートの継承は、プレイヤーが魔法を唱えるシステムで広範囲に渡って使用しましたが、ここでもより一般的な機能を抽象的なステートに追加することができます。さらに、個々のケースにより固有なステートの子バージョンを実装することも可能になります。以前に述べたように、このシステムは非常に強力であり、ほとんどそのすべてが UnrealScript の言語による設計専用となっています。このため、生産性は飛躍的に向上し、コード設計も強化されます。それはともかくとして、発射物をスポーンさせるために、少しばかり楽しい計算をしてみました。これは、プレイヤーの現在の速度を考慮に入れて、敵がどこを狙って発射すれば良いかを予測するものです (21) 。(もちろん Epic の SuggestTossVelocity 関数が同様の機能をもっていることは分かっているのですが、自分で Kinematics 101 をブラッシュアップしたかったのです!)

ただし、敵の狙いが完全に正確であっても困ります。そんなの楽しくないですよね? そこで、少しだけ故意の誤差を発射角度に加えることにしました。(発射ターゲットの位置をちょっとしたランダム Rotator (ローテータ) によって変換します)。 (22) 最後に、発射物の発射方向を、ポーンの現在の回転を中心とする 15 度角に限定しました。こうすることによって敵が横から発射物を撃つという卑劣な発射が一切なくなります。 (23) ただし、この 15 度角の自由度は違う役目ももっています。例の「発射を誤らせる係数」によって敵は完全にプレイヤーを狙うことができないようになっていますが、この 15 度角によって、適切な狙いがつけられているかのような脅威を保つことができるのです。うまくいきました。シーッ。誰も気がつきませんから。^^

さて皆さん、ここ数日間はこのような感じでした。強力な「Unreal」の技術のおかげで、開発は急速に (ほんとうですよ!とんでもなく「急速に」ですよ) 進んでいます。まもなく皆さんにすてきなデモ版を発表することができると思います。今後も投稿し続けます。それでは次回まで。休まず開発しよう!

Blog 4: ファイル レファレンス

このブログで取り上げられている情報は、次にあげるファイルに含まれています。(それらのファイルは、Dungeon Defense のソースコードです)。カンマで分けられている行番号は、ファイル内で独立した行が複数あることを示しています。ハイフンで分けられている行番号は、ファイル内で行がその範囲にまたがっていることを示しています。

  1. DunDefPlayerSelectUICharacter.uc: 24
  2. UIImage_PlayerSelect.uc: 38, 48
  3. UI_CharacterSelect.uc: 97
  4. UI_CharacterSelect.uc: 65
  5. UIImage_PlayerSelect.uc: 38, 48
  6. DunDefViewportClient.uc: 108
  7. DunDefViewportClient.uc: 14
  8. DunDefViewportClient.uc: 122
  9. DunDefPlayerController.uc: 1713, 1726
  10. DunDefPlayerController.uc: 1832, 1809
  11. UIImage_HealthBar.uc: 55
  12. DunDefDamageableTarget.uc: 137
  13. UIImage_HealthBar.uc: 16
  14. DunDefDamageableTarget.uc: 154
  15. UIImage_SpellIcon.uc: 81
  16. UIImage_SpellIcon.uc: 90
  17. UIImage_SpellIcon.uc: 11-14
  18. DunDefCrystalCore.uc: 41-69
  19. DunDefProjectile.uc: 78-103
  20. DunDefDarkElfController.uc: 85-107, DunDefDarkElf.uc: 56
  21. DunDefDarkElfController.uc: 31, 71
  22. DunDefDarkElfController.uc: 62, 63, 71
  23. DunDefDarkElfController.uc: 74

Blog 5: 13 日目

皆さんこんにちは。

ここ 2、3 日ほど、開発で大忙しでした! マイルストーン (区切り) となる 2 週間目のビルドを準備するとともに、Frontend ツールを使用してクックを行い、パッケージ化して、我々のエーステスター軍 (友人と家族のことです^^) にリリースしました。これらの機構がしっかりと整っているため、ゆったりとゲームを楽しみ、難しい調整についてはメモをとることができました。美しくも楽しいミニゲームになってきました。私にとってさらに楽しみなことには、もう 2 週間すれば皆さん自身がこのゲームをプレイできるということです。

実装面については、次に、開発の主な事項に関する概観を掲載しました。ほとんどが先の「マイルストーン」の準備に関係しています。

  • Kismet の機能を追加しました。パラメータ補間を伴った任意のポストプロセスを切り替えることができる潜在的なアクションです。(いつでも必要なときにエフェクトをスムーズにフェードイン / アウトすることができるようになります)。また、ゲームに存在するプレイヤーの数によって、あらゆる Kismet の float 型変数をスケーリングするアクションを追加しました。(プレイヤーの数に応じた乗数の配列を使用します)。マルチプレイヤーのバランスを取ることは難しいため、重要なアクションです。より多くのプレイヤーが存在している場合に、より多くの敵がより短い間隔で Wave (大軍) が押し寄せて来るようにすることができます。ただし、必ずしも、固定化された線形のスケールではありません。
  • 新たな塔のタイプを追加しました。これは Blockade (障害物) といいます。敵はこれを破壊あるいは迂回しようとします。(基本的な動的パスファインドを使用します)。また、Attacker Registration (攻撃者登録) システムを作成しました。これは、同時に攻撃できる敵の最大数を、そのターゲットになりうるオブジェクトが指定できるシステムです。(上限を超えた分の敵は先に進み、ふさわしい他のターゲットを得ようとします)。そのため、Blockade に敵が遭遇した場合、そのうち少数の敵がそれを倒そうとし、残りの敵はそれを迂回して進もうとします。
  • ゲームでオプションとして選択できる Friendly Fire (味方への誤射) チェック機能を追加しました。これは UT (「Unreal Tournament」) にすでにありますが、このゲームでは UT のクラスを使用していないため、自分で書くことにしました。さらに、プレイヤーのスタート位置に関する追加ロジックを加えました。これによって、ゲームは、新たに参加する各プレイヤーのために、有効なスタート位置を順繰りに使用するようになります。そのため、 4 人のプレイヤーが同時に参加しても、それぞれの一意のスタート位置に必ずつくようになります。
  • UI スキンを編集して、独自の「もの凄い」ボタンとフォントを加えました。また、音楽と SFX をゲームシーケンスとメニューのすべてに追加しました。SFX については、AudioComponent をこのゲームの標準である Effect Actor に追加しました。これによってスポーンされるビジュアル エフェクトのすべてが、ただちにオーディオをサポートするようになります。(すべてがアーキタイプの場合は、簡単にオーディオをゲームのイベントほとんどに追加することができます。残りの少数のサウンドエフェクトは通常、アニメーション シーケンス ブラウザを使ってアニメーションに結びつけられます)。
  • ゲームをクックしてパッケージ化しました。(ケーキを焼く (bake) かのように)。

さて、これで全体像がつかめましたね。それでは、各内容についてもっと詳しく見て行くことにしましょう。

Kismet の機能

マルチプレイヤーについて最初に気がついたことは、ゲームが非常に簡単だということです。いえ、簡単すぎるということです。これは、プレイヤーが 4 人もいれば、敵の Wave を捌くのがずっと容易になるためです。そこで、さまざまな規模のゲームにおいて適度に一定な難易度を維持するために、現在のプレイヤー数に基づいて、敵の数と Wave 襲来のインターバルをスケーリング (インターバルの場合は短縮) することにしました。ただし、単に線形のスケールを望んだのではありません。カスタムのスケールを作成して、Wave 単位、プレイヤー数単位で調整できるようにしたのです。これによって、理想的なスケールの「マジックナンバー」がやがて見つかるはずです。そこで、シーケンスアクションを書いて「プレイヤー数に応じて float 型の値をスケーリング」してみました。このアクションには、スケーリングの際に乗じられる float 型の値が格納されたユーザー定義による動的な配列が含まれています。(配列のインデックスは、「プレイヤー数 -1)」に等しいです)。 (1) またこのアクションは、実際にスケーリングされる float 型シーケンス変数を入力として受け取ります。このアクションはアクティベートされると、GetNumPlayers() の値 (現在のワールドの GameInfo から得るプレイヤー数) を使用して、それに対応するスケーリング float 型配列のインデックスをルックアップします。(インデックスの大きさは「配列の長さ -1」に限定されます)。入力された float 型変数の値をそのスケール値で乗じ、Kismet の出力リンクをアクティベートします。 (2) 敵の数と Wave がスポーンするインターバルを制御する、マスターとしての Kismet 変数に使用してみると、魔法のようにうまくいきました。これで、どんなプレイヤー数でもゲームのバランスを取れるようになりました^^

また、フォーラムにおいて、ボリュームに基づいて任意のポストプロセス マテリアルを改変するにはどうしたら良いのかという疑問に接しました。PostProcessVolumes は現在、事前作成されている Epic のポストプロセス エフェクトしかサポートしていません。しかし、ちょっとした Kismet によって任意のユーザー マテリアル エフェクトを制御することが可能です。(Kismet によらない場合は、独自のカスタム ボリュームクラスを作成して、Touched/Untouched されたときに PlayerController がその上にあるイベントを呼び出すようにします)。私は Kismet によるルートを選択しました。任意のゲームの基準に基づいてカスタムの PostProcess イベントをトグル (切り替える) する必要があったからです。このゲームの場合、シネマティックスが再生されているときは常に、自作のフィルム粒子のマテリアル エフェクトをアクティベートしたかったのです。これを実現する方法は2 ~ 3 あります。最も簡単なアプローチは、単に PostProcessEffect 上にある bShowInGame プロパティを使用してエフェクトの on と off を直接切り替えるという方法です。 PostProcessEffect オブジェクトを見つけるために、現在の World Info 内にあるすべての LocalPlayerControllers をイタレートします。(または渡された場合には Instigator を使用します)。そして、LocalPlayer の PlayerPostProcess チェーンを使用して、名前で探している PostProcessEffect を見つけます。見つけた後は、bShowInGame を望ましい値に切り替えます。 (3)

ただし、これを実現した後で、時間経過とともにエフェクト値を up/down (増減) させる補間をサポートすることにしました。これによって、有効 / 無効は、突然 on/off するのではなく、スムーズに遷移することになります。これは少し大変でしたが、完全に実現することが可能でした。まず、アクションを SeqAct_Latent にします (単に SequenceAction ではなく)。これによって、Update() 関数を使用して経時的にポストプロセス値を調整することができるようになります。 (4) 次に、現在補間されている PostProcessEffect を 2 つの配列のうちの 1 つに格納します。すなわち、「表示」のプロセスにあるエフェクトは FadeUp (フェードアップ) 配列に入れ、「非表示」のプロセスにあるエフェクトは FadeDown (フェードダウン) 配列に入れます。 Update() 関数の中では、これら配列のそれぞれをイタレートします。そして、ターゲットのスカラー値に達するまで、指定されたマテリアル スカラー パラメータに対して増減を施します。ターゲットとする値に達した後は、この PostProcessEffect エントリーを (実際は補間の値を含む構造体も) Up / Down 配列から削除します。 (5) どちらの配列にも配列エントリがなくなったら、Update() 関数で false を返し、潜在的なアクションを完了します。 (6) 結果としてうまく動作し、望みどおりスムーズな FadeUp/FadeDown が得られました。ただし、特別に処理しなければならないケースが 2 つばかりありました。

  1. PostProcessEffect を FadeUp 配列または FadeDown 配列に加えようとしたときに、そのエフェクトがすでにその配列のどちらかに存在している場合は、新たに追加する前に古い配列からそのエフェクトを削除する必要があります。さもなければ、同時に 2 つの補間が処理されることになり、混乱が生じます。このようなケースは、Kismet 上で急速に FadeUp / FadeDown 配列の入力をトリガーした場合に起こります。(たとえば、Touched / Untouched を通じて当該のアクションにリンクされているトリガー ボリュームの端のあたりでダンスをする場合などです)。 (7)
  2. 適切な分割スクリーンのサポートについては、当該アクションは確かに Instigator を受け取り、その PlayerController.LocalPlayer のエフェクトだけを改変します。(オプションとして全員のエフェクトを改変することもできます)。ただし、マテリアルエフェクトが一意のパラメータ値をもつには、一意の MIC (マテリアルインスタンス定数) が必要となります。また、この MIC は、現在のところポストプロセス チェーン エディタ自体の中で直接指定することはできません。そこで私は、マテリアル エフェクトのマテリアルが一意であるか否かを調べる関数を書きました。(そのためには、エフェクトにある現在のマテリアルが、MIC ではないか、あるいは、親が MIC ではないかのどちらかであるということを確かめます。いずれのケースも (このゲームの場合)、まだ一意になっていないことを意味します)。もし一意でないならば、新たな MIC を作成し、元のマテリアルがその親となるようにセットし、作成した新たな MIC をマテリアルエフェクトに適用します。 (8) このようにして、分割スクリーンの一意なポストプロセス変更を適切に行うことができます。よかった!

「動的な Blockade (障害物)」オブジェクト

ゲームプレイに戻ります。新たな塔のタイプを追加することによってゲームプレイに深みを与えることにしました。敵を迂回させることによって (あるいは攻撃させることによって) その侵攻を遅らせることができる「Blockade」を追加することにしました。したがって、障害物を動的に迂回する機能をサポートする必要があります。私が取ったソリューションは必ずしも強力とは言えませんが、このゲームの目的には十分役立っています。まず、Blockade オブジェクトを作成します。これは、ターゲット設定可能なインターフェースを実装したダメージを与えることができる (敵が攻撃可能な) 自作オブジェクトの 1 つです。この Blockade アクタの特殊な点は、Bump (衝突) イベントがオーバーライドされているということです。Bump された場合、Blockade はそのアクタが Enemy (敵) のタイプかどうかを判定します。もし Enemy タイプであれば、そのアクタの EnemyController 上にある MoveAroundBlockade 関数を呼び出し、それ自身とパラメータ (コリジョンおよびインパクトの法線) を渡します。 (9) 以上が Blockade の実行することです。本当のマジックは、この MoveAroundBlockade の通知に反応する EnemyController の中にあります。

ただし、先に注釈を加えておきます。Bump イベントをこの目的で使用すると、アクティブなロジックを一切必要としないという利点があります。ただし、敵が実際にオブジェクトにタッチしなければトリガーされないという欠点もあります。このゲームに関して言えば、これでも大丈夫です。ただし、このビジュアル上の欠点 (すなわち敵が実際に Blockade にタッチしなければ迂回の決定ができないという欠点) を解消するのであれば、Trace チェックを敵 Controller の移動に関するロジックで使用することによって、Blockade タイプのアクタが前進する方向にあるか否かをチェックすることができます。これで見栄えが良くなるでしょう。(敵は Blockade に衝突する前に迂回するようになるのですから)。ただし、若干遅くなるという欠点があります。

さて話を戻して、私が作成した EnemyController の MoveAroundBlockade 関数についてです。まず、敵の状況をチェックするロジックを使用します。すなわち、敵が現在当該の Blockade アクタをターゲットにしていないか、あるいは、すでに迂回するつもりで却下しているか、そのどちらかに該当しているかを調べます。(どちらの状況でも、迂回させるのは意味がありません)。 (10) さらに、Blockade アクタが実際に敵と敵がターゲットとする目的地 (パスファインドを使用している場合は次の移動地点) の間にあるかどうかをチェックします。もしそうでないのであれば、やはり却下します。 (11) この措置は、敵が、かすめるような横からのコリジョンによって接触した Blockade を迂回しようとすることを防ぐためのものです。考慮しなければならないのは、ダイレクトな移動パス上にある Blockade だけですから。

MoveAroundBlockade 関数が以上のチェックを通過した場合は、敵が到達すべきオブジェクト回避地点を選択します。すなわち、コリジョンの法線の方向を基準にしてその左側か右側かを決め、Blockade のコリジョンの距離範囲から出ます。なお、左側か右側かを決めるのは、敵の現在における「オブジェクト回避方向」です。(通常は右から始まります)。 (12) 「オブジェクト回避方向」は、敵が移動するのに有効な地点を発見できなかった場合に切り替わります。有効であるか否かを検証するには、その地点の真下が地面であるかどうかを確認します。(これには Trace を使用します) (13) また、Epic の FindSpot() 関数が true を返すかどうかを確認することによっても可能です。(FindSpot() 関数は、目的とする地点に (あるいはその地点の近くに) 有効な非ジオメトリ交差地点を見つけようとするアクタ関数です)。 (14)

有効なオブジェクト回避方向が見つかった場合は、当該 Controller を MovingAroundBlockade ステートにします。このステートは、Controller をダイレクトに目標地点まで移動させます。有効な地点がなかった場合は、オブジェクト回避方向を切り替えて再度見つけようとします。 (15) (その方向も失敗した場合は、単に却下して...きっと他の敵が Blockade をすぐに破壊してくれることでしょう!) (16)

MovingAroundBlockade ステートは MoveToDirectNonPathPos を使用して、オブジェクト回避地点に到達します。(17) (パスファインドが必要となることは想定されていません。理由は、オブジェクト回避のための距離が非常に小さく、かつ、辺りにコリジョン ジオメトリが一切ないことが確認されているとともに、真下が地面であることなども確認されているからです)。4 秒のキャンセル タイマーをセットします。これは、何らかの理由でオブジェクト回避地点に到達できない場合に備えられています。 (18) 敵は、その地点に到達すると、Seeking (探索) ステートに復帰します。このステートは、本来のターゲットへのパスファインドに敵を戻すものです。もちろん、その敵が新たな Blockade にぶつかることも考えられます。(たとえば、互いに隣接するように Blockade を配置した場合)。その場合、この新たな障害物のために再度 MovingAroundBlockade ステートに入ることとなります。おそらくは、敵が先ほどと同じ方向に進み続けることによって、これらの Blockade 群から逃れることができるでしょう。

このアプローチは、極めて基本的なものでありながら、大概きちんと機能します。Blockade の配置がかなり複雑な場合でも、敵は、同一方向に進路をとりながら、次から次へと効率的に Blockade を迂回していくことができます。通過不可能な障壁があった場合は、進む方向を反対方向に切り替えて前進を試みます。この機能が失敗するのは、主に次のような場合です。すなわち、Blockade のネットワークが非常に複雑で長いため、敵の本来のターゲットが、Blockade 凹状のネットワーク内のどこかに位置することになり、状況によっては敵がその位置に到達できない場合です。表現をかえると、この機能は、本当のパスファインドではなく、パス構造という考え方をまったく取らない単純なオブジェクト回避動作ということになります。

当面はこの機能で充分です。(特に Blockade が破壊可能であり、敵が迂回と同程度に攻撃をしかける可能性があるからです)。ただし、ナビゲーションメッシュによる動的なアウトラインナビゲーションを使用できるように改善することもできます。これが今後数週間で私が検討しようとしていることなのです。

Attacker Registration (攻撃者登録)

実際問題として、Blockade のそばで攻撃できる敵の数を制限するメソッドがなければ、Blockade 迂回システムは役に立ちません。なぜなら、敵は迂回を試みようとはせずに、常に Blockade を攻撃しようとするからです。少数の敵が Blockade を攻撃し、残りの敵は迂回するという、この必須動作を実現するために、Attacker Registration (攻撃登録) を実装する必要があります。これは、指定された Blockade (あるいはあらゆるターゲット化可能なアクタ) を同時にターゲットにできる敵の数を制限するものです。これは、自作の Targetable インターフェースに新たに 2 つの関数 (RegisterAttackerと UnregisterAttacker) を追加するだけで簡単に実現することができました。 (19)

もちろん、これを実現するには、このインターフェースを使用する全クラスで、この新たな関数を実装する必要があります。幸いなことに、Registration を必要とするオブジェクト (すなわち Blockade、場合によっては他の配置可能な塔も) は、すべてダメージを与えることができるオブジェクトクラスのヒエラルキーに属しています。したがって、1 つのクラスでこれらの新たな関数を実装すればよいだけです。(ポーンも Targetable インターフェースを使用しますが、現在のところ Attacker Registration を必要としていません。したがって、ポーンクラスに関しては、これら新たなインターフェース関数のスタブ版だけを追加します)。

DamageableActor クラスでは、RegisterAttacker 関数が Attacker (攻撃者) を Attackers 配列に追加します (すでに配列に追加されてない場合に)。また、UnregisterAttacker が Attacker を削除します。 (20) さらに、Attackers 配列の長さが、Attackers の最大数 (私が追加した変数で、デフォルトプロパティで指定します) 以上になった場合は、Targeting Desirability (ターゲット化の望ましさ) 関数が -1 の Desirability を返すようにします。( -1 はターゲット化できないことを意味します)。 (21)

最後に、EnemyController の中では次のような処理を行います。SetTarget 関数内において、新たな Target 上にある RegisterAttacker 関数の呼び出しと、古い Target 上にある UnregisterAttacker 関数の呼び出しを追加します。(この SetTarget 関数は、AI が新たな Target を選択した場合は必ず呼び出されます) (22) 。さらに、 当該の Controller が破壊された場合は、Target を None (なし) にセットします。これによって、敵が倒されたときに UnregisterAttacker が呼び出されます。 (23) これで完了です!

さてこれで、アクタを同時にターゲットにできる敵の最大数を、アクタが指定できるようになりました。それ以上の敵はこのターゲットを無視して先に進みます。私はアタッカーの最大数を 2 に設定したのですが、Blockade の場合に特によく機能しました。Blockade の周りにいる残りの敵は、迂回を試みますが、これこそ私が見たかったものなのです。Blockade が、敵の侵攻を遅らせ、重要な地点を守りながらも攻略不可能ではないという効果的なテクニックを実現することができたのです。

Friendly Fire (味方への誤射) & Start Point Selection (スタート地点の選択)

マルチプレイヤーのテストをさらに行ってみると、Friendly Fire (味方への誤射) が問題であることに気がつきました。何せ、敵を攻撃しているつもりでも、お互いを徹底的に痛めつけていたのですから。(背後から「友人」をやっつけるのもちょっぴり楽しいのですが...。だから、これをオプションとして残しました)。UT (「Unreal Tournament 」) のクラスには Friendly Fire (味方への誤射) をチェックする機能が備わっています。UT では、 犠牲者と攻撃者の関係に基づいて調整するように GameInfo クラスに対して要請することによって、この機能が実行されます。そうすると、GameInfo は両者のチームを調べて、同一のチームだった場合にはそれに応じてダメージを減らします。しかし、私は自分でメソッドを作成してみることにしました。また、ダメージ調整には GameInfoを使用しません。Targetable インターフェースに IgnoreFriendlyFireDamage という名前の関数を追加しました。これは、Instigator を入力として受け取り、チームを調べ、同一チームにいるかどうかによって true / false を返します。現在、この Friendly Fire は GameInfo によって有効になっています。 (24)

さらに、自作の基本クラスにおいて TakeDamage の宣言をいくつか修正することによって、IgnoreFriendlyFireDamage の結果をチェックして true であれば却下する (ダメージを適用しない) ようにしました。 (25) この方法の利点は、IgnoreFriendlyFireDamage 関数をオーバーライドすることによって、特定のクラスがゲーム全体の設定値に関わりなく常に Friendly Fire を無効にすることができるという点です。たとえば、敵は絶対にお互いにダメージを与えません。これはずるいですよね。

次に気がついたことは、4 人のプレイヤーがスポーンすると、(有効な) 同じスポーン位置を選択してしまうということです。ゲーム開始時に 4 人が一か所に重なって見えてしまいます。そこで、スポーン位置を順繰りに使用できるようにしようと考えました。こうすることによって、スポーン位置の数が足りていれば、各プレイヤーが一意の場所を確保することができるようになります。このためには、GameInfo クラスで ChoosePlayerStart() 関数をオーバーライドして、現在チェックされているスポーン位置が Used Spawn Point (使用済みスポーン位置) 配列にあるか否かを調べるようにします。(もしあった場合は、それを無視して次のスポーン位置を調べます)。あるスポーン位置が選択されると、それを Used Spawn Point (使用済みスポーン位置) 配列に追加します。もしスポーン位置が見つからない場合は、Used Spawn Point 配列をクリアして再度チェックします。 (26) 見事に、スポーン位置の循環利用はうまくいきました。

もちろん、これを実現する方法は他にもあります。新たに Spawn Point クラス (実際は PlayerStart クラス) を作成して、使用済みであるか否かに基づく bool 値をもつようにします。PlayerStart がまったく見つからない場合は、この bool 値をすべてクリアします。あるいは、カスタムのスポーン地点選択システムを書くことによって備えつけの機能を一切使用しないというやり方も取れます。Epic のクラスフレームワークのすばらしい点は、所定のタスクを「最も簡単に」実行する方法を提供する一方で、そのタスクを扱うカスタムの方法も無数に提供しているという点です。最適な方法の選択は、皆さんに任されているのです。

UI のスキニング

そろそろ、ささやかなるマイルストーン(区切りの) ビルドをリリースしても良い頃ですが、UI (特にボタン) にはまだ、月面クレーターのデフォルトテクスチャによる DefaultSkin (デフォルトスキン) を使っています。そこで、ゲームに基本的なテーマ風デザインを加えようと考えました。独自の UI スキンを作成する時が来たのです。まず、Epic の DefaultUISkin リソースを私自身が作成したパッケージ (名前は DunDefSkin.DefaultSkin) にコピーします。次に DefaultUI.ini ファイルの UISkinName キーを変更して、私のパッケージを指すようにします。さらに、DunDefSkin を、DefaultEngine.ini ファイル内の StartupPackages リストに追加します。これは、アプリケーションが起動したときにロードされるようにするために重要な処置となります。これでゲーム用のカスタム UI スキンを指定することができたので、UnrealEd の UI スキンエディタを起動して例の月面を自作のボタンテクスチャと交換します。(Default Image Style (デフォルト イメージ スタイル) ->Button Background Style (ボタン バックグラウンド スタイル) の Texture (テクスチャ) 値を、すべてのステートに関して交換します)。また、Clicked イベントのための SoundCue の参照を追加しました。これによって、全ボタンが、クリックされたときにオーディオによる心地よいフィードバックを返すようになりました。さらに、カスタムの True Type Font をインポートして、Default Text Style で使用するフォントに設定しました。これで UI の外観が多少はテーマ風になったので、ビルドの準備をする前に次はサウンドに取り組むことにします!

サウンドと音楽

どのようなゲームでも、さまざまなゲームプレイ イベントが起きますが、そのとき適切なフィードバックとなるサウンドは欠かせません。音楽も同様です。プレイヤーにどのようなことをさせるにしても、その気分を高めるものが音楽です。ありがたいことに、「Unreal」のパイプラインを使用すれば、サウンドも音楽も容易に統合することができます。サウンドをゲームに取り込む方法は数多くあります。今回のささやかな開発においては、そのうちいくつか (全部ではないですが) を採用してみました。まず、私の WAV ファイルをインポートして、そのためのキューをエディタに自動作成させました。(とても便利です)。その際、3D 空間で再生するサウンドすべてが Attenuation (減衰) ノードを使用して作成されるようにします。これによって 3D 配置と減衰の機能が備わります。次に、キーとなるゲームプレイ イベントのための、各種アーキタイプにおいてスポーンされるビジュアルエフェクト (エミッタ) アクタに AudioComponent を追加しました。 (27) これによって、ビジュアル エフェクトのそれぞれが、そのアーキタイプで指定された、対応するサウンドをもつようになります。ビジュアル エフェクト アーキタイプそれぞれについて、望ましいキューをその AudioComponent プロパティの中にセットし、3D サウンドをもつすべてのビジュアル エフェクトを発生させます。ビジュアル エフェクトが使用されない少数のケースにおいては、サウンドキューがアニメーションで再生されるように設定するか (AnimNotify_Sound アニメシーケンス通知を使用します)、単にコードを使用して再生しました (Actor.PlaySound() を使用します。優れたフェードイン / フェードアウトのオプションも含まれています)。また、PlaySound Kismet UI アクションを作成しました。 (28) これは、全体用のスキンのサウンドリストに定義されていないカスタムのサウンドを UI が再生しなければならない場合に、WorldInfo.PlaySound() を呼び出すものです。30 分かからないで、すべてのキーとなるイベントのための 3D サウンドが、ゲーム全体に渡って再生されるようになり、確実にゲームプレイのフィードバック感覚が得られるようになりました。

次は音楽です。響き渡る、圧倒的で劇的な音楽といったところでしょうか! どのような音楽であっても、「Unreal」を使用すれば、簡単に、WAV ファイルをインポートして (音楽トラックが長い場合は、圧縮クオリティを下げて高い率の圧縮をかけます)、Kismet の Play Music Track (音楽トラックを再生) アクション (フェード値もあります) で再生できます。コードを使用する場合は、WorldInfo.UpdateMusicTrack() を呼び出すと簡単です。(29) その際、MusicTrackStruct パラメータがこの新たな音楽の値をもつようにします。私は両方のアプローチをさまざまな用途で採用しました。さてこれで、このゲームには、レベル内であらゆるキーとなるイベントに対応した、フェードする素晴らしい音楽が備わりました。最後に、感情に訴えかけるような操作を若干加え、いよいよリリースのためのパッケージ化の準備が整いました。

クッキングとパッケージ化

リリースのシェフになるのは難しくありません。UnrealFrontEnd を開き、ゲームで使用するすべてのマップを教えます。(このゲームの場合は、エントリレベルおよびメニューレベル、Seamless Travel (シームレスな移動) に使用される遷移レベルも指定します)。また、主となる UDKGame スクリプトパッケージを、DefaultEngine.ini ファイルで指定された StartUpPackages のリストに追加します。この理由は、ゲームの初期化のために UT スクリプトではなく、UDKGame スクリプトパッケージに私が依存していたためです。(なお、スクリプトパッケージは複数もつことができ、それぞれ特定のレベルによって参照されることが可能です。ただし、パッケージに何が含まれていても、カスタムの GameInfo が StartupPackages に入っている必要があります)。さらに、[Cook] ボタンをクリックすると、Frontend+UDK によって最適化されたリリースコンテンツファイルのすべてが生成されます。最後に [Package] ボタンをクリックすると、ゲーム名を尋ねてきます。これはインストーラーのユーザーインターフェースに使用されるものです。 [OK] ボタンをクリックすると、インストーラーがビルドされます。(ゲームに必要となる、あらゆる再配布可能なファイルが梱包されます)。それが完了すると、インストーラー EXE ファイルがメインディレクトリに置かれます。この宝物を、友人、家族、β版テスター、発行元に渡すと、準備完了です。まったく簡単です。

このようにして、「Unreal」を使用して猛スピードで開発するといった生産的な 数日間がまた終わりました。ゲームは本当に形になってきました。私たちは、コンテンツの追加と調整の局面の真っただ中に入っています。もうみんなにお披露目しても良いころだと思います。どうか今後もこの記事を読みに来てください。まもなく皆さんからフィードバックをいただくことができそうです!

-Jeremy

Blog 5: ファイル レファレンス

このブログで取り上げられている情報は、次にあげるファイルに含まれています。(それらのファイルは、Dungeon Defense のソースコードです)。カンマで分けられている行番号は、ファイル内で独立した行が複数あることを示しています。ハイフンで分けられている行番号は、ファイル内で行がその範囲にまたがっていることを示しています。

  1. DunDef_SeqAct_ScaleFloatForPlayerCount.uc: 10
  2. DunDef_SeqAct_ScaleFloatForPlayerCount.uc: 22-26
  3. DunDef_SeqAct_TogglePostProcessEffects: 70-79
  4. DunDef_SeqAct_TogglePostProcessEffects: 240
  5. DunDef_SeqAct_TogglePostProcessEffects: 171-233
  6. DunDef_SeqAct_TogglePostProcessEffects: 236, 248
  7. DunDef_SeqAct_TogglePostProcessEffects: 139-155
  8. DunDef_SeqAct_TogglePostProcessEffects: 137, 117
  9. DunDefTower_Blockade.uc: 21
  10. DunDefEnemyController.uc: 456-460
  11. DunDefEnemyController.uc: 492-509
  12. DunDefEnemyController.uc: 522-530
  13. DunDefEnemyController.uc: 536
  14. DunDefEnemyController.uc: 563
  15. DunDefEnemyController.uc: 538, 545
  16. DunDefEnemyController.uc: 549
  17. DunDefEnemyController.uc: 656
  18. DunDefEnemyController.uc: 648, 602
  19. DunDefTargetableInterface.uc: 26, 29
  20. DunDefDamageableTarget.uc: 65, 73
  21. DunDefDamageableTarget.uc: 61, 93
  22. DunDefEnemyController.uc: 259, 272
  23. DunDefEnemyController.uc: 217, 283
  24. DunDefTargetableInterface.uc: 21, DunDefPawn.uc: 67
  25. DunDefPawn.uc: 148
  26. Main.uc: 274, 286
  27. DunDefEmitterSpawnable.uc: 154
  28. DunDef_UIAction_PlaySound.uc: 13
  29. Main.uc: 156

Blog 6: 17 日目

UDK 開発者の仲間のみなさん、こんにちは!

Dungeon Defense は最終段階にさしかかっています。ここ数日間、私は AI の動作を洗練させました。さらにその後は、リプレイバリューを高めてくれるメタゲームシステムのいくつかに集中して取り組みました。また、最近完成した多数のアートワークを統合しました。開発プロセスで一番わくわくする瞬間というのは、すばらしいアートによってゲームプレイに息吹が与えられ、それが「Unreal」の技術によって見事にレンダリングされる瞬間です。

ddblog6-1.jpg

さて、コードの側ではどのようなことが行われたか、まずざっと見ていきましょう。その後で各事項について詳細な説明を試みたいと思います。

  • 敵の AI を改良しました。敵がナビゲーションネットワークからはじき出された場合 (たとえば、ダメージなどのはずみを受けた場合など) や、何らかの理由でターゲットにたどり着けない場合に備えて、「スタック (身動きとれない状態)」チェックを定期的に行うようにしました。これらの場合には、敵 AI は、新たなターゲットを探し、ナビゲーションパスに動的に戻ろうとします。また、Aggro システムを追加しました。(MMO ユーザーにはなじみ深いものです)。これは、プレイヤーが敵にダメージを与えた場合、敵がそのプレイヤーをターゲットにしてより攻撃的になるというシステムです。その際、ダメージの時期や規模といった条件とともに、その他すべてのターゲットファクタにおける重み付けが考慮されます。これによって、敵がより生き生きとするだけではなく、戦術上の深みも加わります。(たとえば、強力な敵から重要な拠点を守るために、ダメージを与えて引きつけ、その拠点から敵を遠ざけるという戦術が考えられます)。
  • 基本的なグローバル UI 通知システムを追加しました。これは、ミッション目標やその他のゲーム情報がフルスクリーンで表示されるものです (たとえ分割スクリーンでゲームが行われていても)。複数の通知が重ねられていき、時間経過とともに徐々にスクリーンからフェードアウトします。
  • スコアシステムを追加しました。このシステムでは、プレイヤーが敵を倒した場合や、Wave をクリアした場合に得点を得ることができるようになります。スコアが加算されるたびに、Award Name (授与名) が添付されます。これは、スコアを得た理由 (ボーナスや特別得点授与など) を短いテキストで表示するものです。
  • また、スコアシステムとハイスコアシステムを結びつけました。これによって、保存されたトップテン (上位 10 傑) のスコアがメインメニューとゲームオーバー画面で表示されるようになります。ゲーム中にトップテンのスコアを達成すると、ゲームの終了時にハイスコアにエントリするために名前を入力するように促されます。複数のプレイヤーの場合にも動作します!
  • 基本的なオプション用 UI を追加しました。これによって、オプションを保存し、ゲームプレイ中に変更することもできるようになります。
  • ゲームプレイの調節やバランス調整、仕上げのための微調整を多岐にわたって行ってきましたが、みなさんに楽しんでいただければと思います。 ^^

それでは、まず AI から詳しく見ていきましょう。ナビゲーションメッシュシステムは非常に便利ですが、あらゆるエッジケースを自動的に解決することはできません。キャラクターがナビゲーションエリアからはじき出されパスファインドに失敗した場合、どうしたら良いでしょうか? これはとても簡単に解決することができます。NavigationHandle.FindPath() 関数が false を返し、なおかつターゲットに直接到達することができない場合は、AI Controller が、視線上にダイレクトに位置している近くのターゲット (またはナビゲーションノード) に切り替えるようにさせます。さらに NavigationHandle.FindPath() 関数が 有効なパスを返すようになるまで、そのターゲットに向かって直接歩かせます (パスファインドを使用しない MoveToDirectNonPathPos() 関数を使います)。このような単純なソリューションによってこの問題が解決されたので、敵はおよそどこに弾き飛ばされても、プレイ可能なエリアまで戻って来ることができるようになりました! (1)

さらに、タイマーチェックを追加しました。これは、敵が毎秒一定の距離を進んでいるか否かを判定するものです。もしそうでない場合は、パスファインドする前に敵を左右どちらかに直接移動させます。 (2) これによって、敵が全員同一方向を目指すために身動きとれなくなる状況を解決することができ、効率的にお互いの周りを動き回れるようになります。

パスファインド問題が解決されたので、次は、敵がオブジェクトに対してもっと反応するようにします。すなわち、単に距離や静的な適性度に基づいてターゲットの選択を行わせるのではなく、敵に攻撃をしかけたオブジェクトに対してもっと反応するようにしたのです。簡単に言えば、シンプルな Aggro システムを導入するということです。これは、最近敵にダメージを与えたプレイヤーほどターゲットとされる重みが高くなるというシステムです。(いえ、私はプレイしませんよ WoW )

これは難しいものではありませんでした。ちょっとした配列の管理だけが必要となります。まず、Recent Attackers (最近のアタッカー) の動的配列を Enemy Controller に追加します。(この Controller の NotifyTakeHit (被ダメージ通知) イベントを通じて Attaker がその配列に追加されます)。 (3) ただし、Attacker への直接の参照を保存するだけではなく、言わば「AggroEntry」構造体配列を作ります。この構造体配列は Attacker に関する付加的な情報を保存します。つまり、Attacker が最後に敵にダメージを与えた時期や、Attacker の現在の Aggro Factor (係数) などが保存されることになります。 (4) Aggro Factor とは、Attacker が敵に与えた最近のダメージの総計を分子にし、敵のヘルス値を分母にしてパーセント計算したものです。敵がターゲットステートにある場合、毎フレームごとに Aggro Entry 配列をイタレートし、時間経過にともなって各敵の Aggro Factor を減じていきます。(0 になった場合は、リストからエントリを外します)。 (5)

一方、ターゲットの選択の際には、潜在的なターゲットが Aggroのエントリを有しているか否かを調べます。エントリがある場合には、この潜在的なターゲットに関する適性度を、当該エントリの Aggro Factor が現在どのくらい大きいかということに基づいて引き上げます。 (6) また、新たなエントリの Aggro Factor が逓減し始める前に、一定の間隔 (10 秒間) を空けるようにしました。これは、敵がターゲット間を行ったり来たりしないようにさせるための措置です。これでお終いです。敵は、最近攻撃をしかけてきたものに向かうようになりました。しかも、距離とターゲット適性度による全体に適用されるターゲットの重みはそのまま計算に入ります。誰にとってもゲームプレイの深みが増しました!

さて、次に考えたことは、重要な目標事項を分割画面それぞれの上に表示するのではなく、全体画面で通知すべきだということです。そこで、FullScreen レンダリングモードにセットされている Global UI を追加します (プレイヤービューポート単位ではなく)。これらのうちの 1 つの UI だけが、ゲームプレイ開始時に GameInfo.PostBeginPlay() で開かれます。これで全画面 UI が保証されたので、次に、複数の通知を順番待ちさせ、シームレスに互いの中にフェードさせるとともに、Kismet を通じて追加できるようにします。これを実現するには、UI Labels の配列を Global UI Scene に追加し、Global UI Scene の変数内にある編集可能な配列にそれらの参照をセットします (シーンエディタを通じて)。 (7) さらに、ShowMajorNotification という名前のカスタム UI Scene クラスに関数を作成します。この関数は、(使用された最後のラベルのインデックスを保存し、毎回インデックスをインクリメントしながら) その配列内の次の Label ウィジェット上にテキストをセットする (とともに「ポップイン」アニメーションを再生する) ものです。 (8) これで、ラベルのために作成したメッセージと同数のメッセージ (このゲームの場合 3 個) を同時に表示することができるようになりました。これはうまく機能しました。 (GameInfo におけるその UI Scene への保存済み参照を使用して Kismet からアクセスできる) Global UI Scene の ShowMajorNotification 関数を単に呼び出す Kismet シーケンスアクションを追加すると (9) 、全プレイヤーが全画面モードで重要なイベントの通知を受けることができるようになりました。 (10)

ddblog6-2.jpg

実際のところは、同じシステムを各プレイヤーの HUD UI 内にも実装しました (11) 。したがって、Instigator (PlayerController をもつ Pawn) が ShowNotification Kismet アクションに渡されるか否かに応じて、プレイヤー単位で UI メッセージを表示するか、あるいは全員にグローバルに UI メッセージを表示するか選択することもできます。 (12) Sweet :) すばらしい ^^

ddblog6-3.jpg

次に私は、シンプルな Score システムを実装することにしました。これは、リプレイへの意欲を起こさせ、自慢の種を提供するためのものです。これはかなり簡単でした。Score 値を Controller クラスに保存し、敵を倒したときはいつでもその値に加算していきます。(敵の Died 関数内で Killer の参照を使用して誰が倒したかを決定します) (13) 。さらに、ゲーム内で比較的少数の他イベントが起きた場合にも加算します。(たとえば敵の Wave を殲滅したときなど)。 (14) ただし、ゲーム内で表示される Score は少し見栄えが良くなくてはいけません。そこで、カスタムの UI Lavel クラスを Score インジケータのために実装します。これは、プレイヤーが実際に倒した本当の得点まで (時間をかけて) 数字が上昇していくものです。(旧式のキャッシュレジスターみたいにです)。 (15) さらに、数字が上昇している間は、UI Label 上で、少し派手目に見えるような色と位置にアニメーション (16) を再生します。これによって少しばかり注意を引いて見栄えを良くします。

また、Score が加算される場合に Bonus Names (ボーナス名) を添付するようにしました。(たとえば、COMBO KILL x2 のように)。これを実現するには、前述のように重要な通知を実装したときとかなり似た方法をとります。新たな UI Label のセットを作成し、テキストのキュー (queue) を表示し、アニメーションを使用して時間経過と共にそれらテキストをスクロールまたはフェードアウトします。 (17) これら UI Label への参照を格納する配列をカスタムの Score Label クラスに置き (18) 、得点が加算された場合はいつでも、Score Label クラスが、 (もしあれば) 次の Bonus Name のテキストを次の UI Lavel にセットするようにします。 (19) 結局、これはうまく機能して、得点を獲得した場合に見事にビジュアル的なフィードバックを示すようになりました。UI Scene と UI Control で重要なことは、何か型破りでユニークなことをしようとするならば、Epic の Control クラスの 1 つをサブクラス化して、必要なあらゆるカスタムロジックを作るということです!

さて、独自の Score ができたので、次はトップテンの値を保存する方法が必要です。これは、さまざまな UI でプレイヤーにトップテンの値を魅力的に示すとともに、新たなハイスコアを出したときにカスタムの名前を使ってプレイヤーがエントリに追加できるようにするものです。このためには、Epic の便利な SaveConfig() 関数を使用します。これは、セキュリティについての心配がない場合に、基本的なゲームデータを保存するのに役立ちます。Epic の公式の発表によれば、次回リリースの「UDK」では DLL (ダイナミック リンク ライブラリ) バインディング機能が搭載されるとのことです。つまり、私たち UDK 開発者が、C++ で作成したいかなる保存スキームも利用することができるということになります (ネイティブデータの処理可能性が無限に広がります)。ただし、今回のように基本的なものには荷が重すぎます。

そこで、Data_HighScores クラスを作成することにします。(Actor ではなく Object からの派生クラスです)。また、このクラスで HighScoreEntry 構造体を定義します。これは、High Score 情報 (スコア、プレイヤー名、到達 Wave 数) を含みます。また、HighScoreEntry の config 配列を作成します (20) 。config というキーワードを使用して変数宣言したので、この配列の値はクラス宣言で指定される INI ファイルで定義されることになります。そこで、DefaultHighScores.ini ファイル内で、デフォルトの 10 個のエントリを High Score の配列のために定義します。(これは当て推量で適当な値を設定しました。自分でもっとゲームをプレイして、トップスコアがどのようなものになるか分かるようならなければいけませんね!)

これらのエントリは、Data_HighScores クラスがオブジェクトとしてインスタンス化された場合いつでも、自動的に「Unreal」によってロードされます。インスタンス化は、カスタムの GameViewportClient クラスにある初期化関数で行っています。Data_HighScores クラスの config 変数 (この場合、High Score のエントリ) を修正するとともに、このクラスの SaveConfig() を呼び出すと、このデータが INI ファイルに返送されて保存されます。 (21)

このような単純な情報を保存するだけではなく、チェックポイントデータや、他にも必要となりそうな一般的なデータを保存することができるということを覚えておくと良いでしょう。(ただし、エンドユーザーに見られても構わないのであれば)。PerObjectConfig キーワードを使用することによって、オブジェクト インスタンス単位でデータを保存することができます。これは、ユーザーが複数の保存データを作成したり、より多くの保存可能なオブジェクトを動的に追加できる場合に役立ちます。後に、GetPerObjectConfigSections を使用してイタレートすることによって、どの保存データのエントリがロードできるか検索することができることになります。

ともかく、High Score のロード / 保存ができたので、プレイヤーのスコアをいつリストに加えるかを決めて、UI を通じて High Score を表示しなければなりません。

プレイヤーがゲームオーバーになって (Dungeon Defense では最終的にはいつもこうなるはず^^) リストに追加する場合、プレイヤーのスコアが、High Score 配列に入っているエントリのうちのどれか 1 つよりも高いか否かを調べ ます。 (22) 高い場合は、編集ボックスを含む UI を開き (High Score を達成したプレイヤー 1 人につき 1 回)、新たなエントリのために使用する名前を入力するように要求します。UI 上で [OK] ボタンがクリックされると、適切なインデックスに位置するこのプレイヤーの新たな HighScoreEntry 構造体 (プレイヤーが入力したプレイヤー名をもつ) を High Score 配列に挿入します。ちょうど 10 個のエントリになるように配列のサイズを小さくし、最後に SaveConfig() を呼び出してこの新たな値を INI ファイルに書き出します。 (23)

ddblog6-4.jpg

High Score について最後にやらなければならないことは、UI の上に表示することです。これを実現するには、カスタムの UI Panel クラス (HighScoresPanel) を作成し、それに、(各 High Score Entry のための) 10 個ある UI Label への参照の配列を与えます。 HighScoresPanel のためにカスタムの OnCreate 関数を呼び出します。この関数の中では、インデックスが対応する High Score Entry データへの UI Label が参照されている場合にそれぞれの文字列の値をセットします。 (24) さてこれで、常に High Score を表示する再利用可能なコントロールができました。これは、あらゆる任意の UI Scene に入れることができます。よくやりました。High Score をゲームオーバー UI とメインメニューの両方で表示する計画だったのですから。

ddblog6-5.jpg

最後に、High Score と同じデータ保存用 config メソッドを使用して、非常に基本的なオプション UI を作成します。これは、特定のゲームの設定値をユーザーが変更するためのものです。これらの設定値は、GameInfo クラスに含まれている bool 値です (25) 。これを、チェックボックスをつけてオプション UI に表示します。オプション UI クラスの SceneActivated() イベントでは、チェックボックスでチェックされた値を、 GameInfo クラスの対応する変数に設定します。ユーザーが [OK] ボタンをクリックすると、チェックボックスの値を GameInfo クラスの変数にコピーするとともに、GameInfo クラス上にある SaveConfig() 関数を呼び出します。(Cancel をクリックするとコピー / 保存を実行しないで単に UI を閉じます)。 (26) シンプルで効果的です。

ddblog6-6.jpg

さて、AI と UI を仕上げ、より魅力的なゲームにするためのちょっとした作業が完了したので、いよいよ開発の最終局面に近づいてきました。リリースの前にさらにコンテンツを実装する計画です。これには、プレイヤーが敵の速度を低下させるトラップが含まれます。しかし、もうほとんど完了に近いです。皆さんがこの小ゲームをプレイする日が待ち遠しいです。ゲームが仕上がる数日後まで投稿を続けます!

Blog 6: ファイル レファレンス

このブログで取り上げられている情報は、次にあげるファイルに含まれています。(それらのファイルは、Dungeon Defense のソースコードです)。カンマで分けられている行番号は、ファイル内で独立した行が複数あることを示しています。ハイフンで分けられている行番号は、ファイル内で行がその範囲にまたがっていることを示しています。

  1. DunDefEnemyController.uc: 972
  2. DunDefEnemyController.uc: 715
  3. DunDefEnemyController.uc: 915
  4. DunDefEnemyController.uc: 11
  5. DunDefEnemyController.uc: 109
  6. DunDefEnemyController.uc: 201, 167
  7. UI_GlobalHUD.uc: 11
  8. UI_GlobalHUD.uc: 27
  9. DunDef_SeqAct_ShowNotification.uc: 41
  10. Main.uc: 233
  11. UI_PlayerHUD.uc: 51
  12. DunDef_SeqAct_ShowNotification.uc: 36
  13. DunDefPlayerController.uc: 309
  14. Main.uc: 345
  15. UILabel_ScoreIndicator.uc: 53, 112
  16. UILabel_ScoreIndicator.uc: 59, 115
  17. UILabel_ScoreIndicator.uc: 103
  18. UILabel_ScoreIndicator.uc: 32
  19. UILabel_ScoreIndicator.uc: 65, 103
  20. Data_HighScores.uc: 11-19
  21. Data_HighScores.uc: 35-64
  22. UI_GameOver.uc: 75, Main.uc: 186-209
  23. UI_AddingHighScore.uc: 18-30, Data_HighScores.uc: 35-64
  24. UIPanel_HighScores.uc: 16-30
  25. UI_Options.uc: 31-43
  26. UI_Options.uc: 164-165, 55

Blog 7: 23 日目

皆さん、こんにちは!

前回ブログ以来、ここ 4 ~ 5 日ほどもの凄く忙しかったです。それで非常に多くのことを成し遂げることができました。私たちは現在、このささやかなデモゲームの仕上げに入っているところです。あらゆる要素を磨き上げています。皆さんももうすぐご自分で確かめることができます。そのようなことで、ゲームのバランスとコンテンツの統合に関して、かなり多くの変更を決定しました。ゲームのいろいろな面が、すばらしいメディア (表現媒体) を伴って生き生きとしてくるのを見ると、いつも心が躍ります。もちろん、機能も数多く追加しました。それでは、一緒に主なものを見ていくことにしましょう。

  • Kismet を使用して、lava pit (溶岩の穴) を変更しました。以前は、大ダメージを与える物理ボリュームでしたが、それに代えて、プレイヤーを安全にテレポートするものにしました。適用されるダメージは少なくなりました。ただし、敵は即死します。このためには、トリガーとなるボリュームにタッチしたものすべてのクラスタイプを調べる Kismet の条件が必要になります。
  • 適切なカメラの追跡を追加しました (補間付き)。これによって、カメラが壁を突き抜けなくなります。またカメラの回転方法を変更しました (マウス制御スキームにおいて)。単にマウスをスクリーンの端に動かすだけで良くなりました。また、カメラの FoV (視野角) をビューポートのアスペクト比を使って動的にスケーリングしました。これによって、ワイドスクリーンの解像度または水平方向の分割スクリーンを使用する場合にプレイヤーが遠くまで見ることができるようになります。
  • 浮遊するパーティクルエフェクトがマウスカーソルの下に発生するようにしました。これによって、撃とうとしている場所が示されます。また、マウスカーソルが敵の上にあるときは、Particle Color パラメータを使用して色を変化させます。オーナープレイヤーだけがこのパーティクルエフェクトを自身のビューの中で見ることができます。bOnlyOwnerSee アクタ / コンポーネント オプションを使用しています。
  • 「ガストラップ」を追加しました。これは、ガスが消えるまで敵を咳き込ませて侵攻を遅らせるものです。その間、塔が敵を徹底的に攻撃します。
  • 塔を売ることができる機能を追加しました。塔配置システムからステートを継承しています。もちろん、それに要した費用と塔の現在におけるヘルス値の何割かしか戻ってきません。(減価償却、経済学の常識ですね)。
  • 連続して参加するプレイヤーそれぞれが、一意にキャラクターマテリアルがカラー化されることによって、お互いを簡単に見分けることができるようになりました。
  • メインメニューに多数のオプションを追加しました。(ガンマ、SFX、音楽ボリュームのスライダー、解像度セレクター、フルスクリーン / ポストプロセス切り替え)。
  • ゲームの外観をすっかり変えました! 基本的には漫画的な要素を強くしました。そのためには、ジオメトリ アウトライン化ポストプロセス マテリアルとコントラストの調整を採用しました。私は楽しげな感じを Dungeon Defense で表現したかったのですが、それをはっきりと表すようにしました。

では、どのように動作するか見ていきましょう。まず、lava (溶岩) に落ちたプレイヤーを再スポーンさせる (lava に touch した敵は kill する) Kismet について検討してみます。次は、この Kismet が機能する図式です。以下でいくつか説明を加えます。

ddblog7-1.jpg

ご覧のとおり、2 つの Touched イベントがあります (2 つの lava ボリュームのための)。このイベントでは、Touched の Instigator がであるか否かをチェックします。敵のクラスであれば、多大なダメージを与えます。そうではなく、Instigator がプレイヤークラスであるならば、オブジェクトリストからランダムにスポーン位置を選択して、プレイヤーをそのスポーン位置にテレポートさせるとともに、軽微なダメージを与え、その新たな位置にテレポートの視覚的エフェクトをスポーンさせます。これでお終いです。

ただし、これだけは憶えておいていただきたいのですが、Touched イベント上で bPlayerOnly (bool 値 プレイヤーのみ) を無効にしています。そのため、敵もこのイベントをトリガーすることができます。また、 MaxTriggerCount (最大トリガー数) と RefireDelay (再作動の遅延) が 0 に設定されているため、必要なだけ素早く何度でもトリガーできます。私が書いた Is Of Class (クラス判定) 条件は、編集可能な変数である name ClassName (クラス名を指定) をもち、 TheObject.IsA(ClassName) (このオブジェクトは (ClassName) である) の結果を利用して True / False の出力をアクティベートします。 (1) これで完了です! (なお、もちろんのこと、他のボリュームについても機能します。私が PhysicsVolume を使用したのは、単に lava のレベルでこのボリュームをすでに配置していたからに過ぎません)。

次に、カメラが壁を通り抜けないようにしましょう。そこで、トレースを行い、ワールドのジオメトリとのコリジョンを調べるとともに、その トレース結果に対して補間をかけて、カメラがジオメトリをスライドしていくときにスムーズに動くようにします。この機能は、Camera クラスの UpdateViewTarget() 関数 (ここでは、カメラの位置が計算されます) で、私が書いた CheckForCollision 関数を呼び出すことによって実現します。 (2) CheckForCollision 関数では、カメラの理想的な位置からビューターゲットの (ポーンの) 位置まで トレースします。調べるのはワールドのジオメトリだけです。 (3) このトレースにヒットする (ぶつかる) ものがあった場合は、元の (理想的な) カメラ位置を起点とする、ヒットした位置のオフセットを取り、それに対する補間を開始します。毎フレームこれを行い、補間が常に更新されるようにします。引き続いて行われるトレースから得られるターゲットのオフセットも同様に更新されます。これによって、カメラが壁を突き抜けることがなくなります。また、VLerp 速度に基づいて、カメラとコリジョン地点間のスムーズな遷移が可能になります。また、ヒット法線の結果を衝突したカメラの位置に少し加えることによって、衝突したサーフェスよりもわずかに前方でムーブアウトし、Z 軸方向にややオフセットするようにしました。これによって、カメラは常にプレイヤーキャラクターのわずか上方に位置するようになります。(つまり、このゲームはトップダウンの視点となります)。 (5)

次に気がついたことは、ワイドスクリーンでプレイする場合や、2 人用ゲームでビューポートが水平に分割される場合に、遠くが見えづらくなり、そのため私の見事なゲームスキルが下がってしまうということです。そこで、プレイヤーの現在におけるビューポートのアスペクト比に基づいて (標準のアスペクト比である 4 : 3 と比較します)、ターゲットの FOV (視野角) を動的に調整することにしました。これも Camera クラスで処理します。このクラスでは、UpdateViewTarget で FOV を DefaultFOV に直接設定するのではなく (6) 、AdjustFOV 関数を置きます。この関数は、PCOwner の (すなわち、PlayerController の) HUD の解像度およびアスペクト比を取得するとともに、その HUD のアスペクト比を 4 : 3 と比較して出力 FOV をスケーリングするものです。 (7) ただし、線形のスケーリングは行いません。極端化してしまうからです。アスペクト比の係数を 0.4 乗まで引き上げました。これによって、画面が横長になっても、FOV が漸進的に増加するため、あまり金魚鉢のようにはなりません。念のために、スカラーの最小値 / 最大値を 0.75 / 1.5 に固定しています。これで、超ワイドスクリーンや水平分割画面であっても、快適なエクスペリエンスがゲームで得られるようになりました。

最後に、これまでの制御スキームがあまりエレガントではないと思えてきました。マウスの右ボタンを押下して視点を回転させるやり方のことです。これを止めて、より標準的な方法をとることにします。すなわち、画面の端までマウスを動かすことによって、その方向に視点を回転させるというやり方です。これを実現するには、PlayerController クラスの PlayerMove 関数で、現在のマウスの位置が画面の右側 / 左側の 3 % 以内に位置しているか否か判定する機能を追加します。(HUD の X 軸に関する解像度の 3 % とマウスの位置を比較します)。 (8)

マウスが実際に画面左端 / 右端にあり、Mouse Delta (差分) の符号が画面の端の方向と一致している場合は、現在の Mouse Delta X を Rotate Camera (カメラを回転する) 関数に使います。これによって、マウスを画面右端において左方向にスクロールしても、カメラは左に回転しません。この方法は、マウスの右ボタンを使用して視点を回転させるよりもかなり自然であると分かりました。また、これによって右ボタンを他の用途に充てることができます。これにて一件落着!

次は、プレイヤーのためにより便利な表示です。プレイヤーがどこを狙っているのか、敵を狙っているのかどうかをもっと分かりやすく表示するようにします。このためには、パーティクルエフェクトをワールド内で使用することにします (純粋な UI や Canvas エフェクトは使用しません)。これによって、3D 空間でのスケーリングが可能になります。 ParticleSystemComponent (パーティクル システム コンポーネント) を Player クラスに追加し (9) 、Player のアーキタイプにある渦巻き状パーティクルのテンプレートを与えます。このコンポーネントの bOnlyOwnerSee (オーナーだけが見える) を、当該のプレイヤーのビューにおいてのみ見えるように設定します。また、このコンポーネントの Scene Depth Priority Group (シーンの深度優先グループ) を Foreground (前景) に設定することによって、他のジオメトリによって見えなくなることがなく、あたかも UI 要素らしくなるようにします。 AbsoluteTranslation (絶対的平行移動) を true に設定することによって、Translation (平行移動) の指定が、アクタ空間ではなくワールド空間で行われるようにします。最後に、このコンポーネントへの参照を、PlayerController に渡します。このクラスは、ポインタの参照先である位置にこれを配置します。 (10)

PlayerController クラスは、このコンポーネントの Translation を、以前のブログで記したスクリーン レイテスト (raytest) された位置にセットします。これで完了です。クールなパーティクルエフェクト インジケータが、私の指しているところを教えてくれます。 (11) ここで更に、敵を指したときにこのインジケータの色が変わるようにしたいと思います。これについては、スクリーンレイテストが、PlayerMove にある Enemy クラスをヒットするかどうかテストし、確認済みです。そこで、パーティクル システムがカラーを変更するようにするため、Color Parameter (カラーパラメータ) を ParticleSystem のサブエミッタに追加します。この Color Parameter モジュールのモードを Colorizer (カラー化機能) に設定します。さらに、コードから ParticleSystemComponent.SetColorParameter(Colorizer, NewColor) を呼び出して、インゲームのカラーを変更します。敵の上にカーソルを置いた場合には、このカラー値が赤になるように変更しました。敵の上ではない場合は、白 (パーティクルの固有のカラーを乗じます) にしました。 (12) なお、このパーティクルの Color Parameter モジュールは、エミッタから続々とスポーンされる「新しい」パーティクルだけに影響します。すでにあるパーティクルには影響しません。エフェクトの範囲にある全パーティクルを直接カラー化するには、MIC (マテリアルインスタンス定数) を使用して、エフェクトのマテリアルに関する完全カラー化を動的に変更する必要があります。このゲームではその機能は必要ありません。短いライフタイムの新しいパーティクルを次々とシステムがエミットするためです。

ddblog7-2.jpg

次は、マルチプレイヤーの識別についてです。それぞれのプレイヤーが異なる外見 (異なる色) をしていたほうが良いと思いました。このためには、基本となる Player のマテリアルを基にして 4 つのバリエーションを作成し、それぞれにおいてカラーチャンネルをスワップすることによって、ディフューズ テクスチャのバリエーションを作成します。さらに、マテリアルの配列をプレイヤーのアーキタイプに追加するとともに (13) 、PlayerController の PostBeginPlay 関数において、現在他に存在する LocalPlayerControllers の数を調べます (LocalPlayerControllers イタレータを用います)。その数に基づいて (ただし、-1 します)、プレイヤーの「番号」を決定します (14) 。さらに、PlayerController.Possess 関数 (ここで Controller がポーンを引き継ぎます) において、プレイヤーの「番号」をインデックスとして使用することによって、アーキタイプで指定された配列からすばやく望みのマテリアルを得ます。最後に、Mesh.SetMaterial() を呼び出すことによって、今選択したマテリアルをキャラクターのメッシュに適用します。(このゲームの場合は、メッシュが 1 つのマテリアルしか使用しないため、 要素 0 に適用します)。 (15) これで完成です。ゲーム内の各プレイヤーがユニークに見えるようになりました。

リリースに近づいてきましたが、さらに選択項目を UI に追加しようと思います。解像度や他の設定項目を INI ファイルにまで行かなくても変更できるようにします。特に、一般的な解像度の切り替え、フルスクリーン表示 / ウインドウ表示の切り替え、ポストプロセス エフェクトの無効化 (悲しいことながらもこれを好まない人がいるかもしれませんので。あるいは、単にビデオカードの性能が低い場合を想定して) を選択できるようにするとともに、ガンマおよび音楽のボリューム、サウンドエフェクトのボリュームを調整できるスライダーを作成します。それぞれどのように実装したか、以下に簡単にまとめてみましょう。

  • 解像度の選択とフルスクリーン / ウインドウの切り替え : これについては Checkboxes の配列を使用します。この配列には、ゲームでサポートしている解像度 (1024x768、1280x720 など) の文字列データが含まれます。これらのチェックボックスに ButtonClicked デリゲートを設定します。これは、他の解像度のチェックボックスにチェックが入っていないかどうかを確認するためのものです。もし他にもチェックが入っているならば、それを解除します。(これによって、一度に 1 つの解像度だけが選択されることになります)。また、通常の「チェック解除」の動作をできないようにします。クリックした時にチェックボックスの値を true にすることによって、有効にすることはできても無効にすることができないようにします。 (16) フルスクリーン表示 / ウインドウ表示の切り替えは、単に、デフォルトの on/off の動作をもつチェックボックスで行います。最後に、プレイヤーが [OK] ボタンをクリックすると、現在選択されている解像度のチェックボックスがもつ文字列の値とフルスクリーン切り替えの値を使って、SetRes [resolution][fullscreen] コンソールコマンドを呼び出します。SetRes コンソールコマンドは、実際の解像度を変更するとともに、INI ファイルの中に最新の値を保存します。 (17)
  • ポストプロセスの切り替え : これについても、ポストプロセスを切り替えるチェックボックスを追加します。チェックボックスの値は、ViewportClient クラスの config bool 型変数に格納します (他にグローバルなポストプロセスの config 値がないように思われるため)。このポストプロセス bool 値が false の場合は、 ViewportClient 初期化関数の中で、コンソールコマンドの show postprocess (ポストプロセスを表示) を実行することによって、ポストプロセスを off に切り替えます。 (18) これは、ポストプロセスの bool 値がオプションメニューを通じて切り替えられた場合も、そのたびごとに実行され、ViewportClient クラスにある SaveConfig() 関数が呼び出されて、このポストプロセスの bool 値がユーザーの INI ファイルに保存されます。 (19)
  • ガンマ スライダー : Slider をオプションメニューに追加し、その最小 / 最大値を適切なガンマ値に設定し、さらに、コンソールコマンドの Gamma [SliderValue] をオプションメニューにいるあいだ毎フレーム実行します。(このためにスクリプトのコールバックを追加するのは気が進まなかったのです)。 (20) また、現在のガンマ値を config 変数として ViewportClient クラスに保存します (そうしなければ保存されません)。さらに、この値をアクティブなガンマ値として ViewportClient 初期化関数にセットします (コンソールコマンドを使用します)。 (21)
  • 音楽と SFX のスライダー : これについては、ゲームの全 SoundCues 上で SoundClass を指定するとともに、AudioDevice 上に SoundMode をセットすることによって、各 SoundClass のボリュームをコントロールできるようにする必要があります。Epic の SoundModesAndClasses.upk を自分のゲームの起動パッケージに追加し (そこに備わっている Sound クラスをすべて利用できるようになります)、SoundCue クラスのデフォルトのプロパティを編集して、デフォルトの SoundClass が SFX となるようにします。音楽キューが 「Music」 SoundClass のものであることをエディタで明示的に設定するとともに、ViewportClient の初期化においては「Default」 SoundMode を AudioDevice 上にセットします。エディタで、2 つのエフェクトを「Default」SoundMode の Effects 配列に追加します。1 つは「SFX」 SoundClass のためのもので、もう 1 つは「Music」 SoundClass のためのものです。これによって、それぞれのボリュームを個別に制御できるようになります。次に、現在の SoundMode がもつ Effects 配列の VolumeAdjuster 値を変更する Set Volume 関数を書きます。(配列のインデックスは、先にセットアップした「SFX」および「Music」SoundClass に対応します)。 (22) 最後に、SFXボリューム と Music ボリュームの config float 型変数を ViewportClient クラスに追加し、オプションメニューでこれらの変数に、それぞれの UI スライダーの値をセットします。 (23) オプションメニューにいる間は、SetVolume 関数を呼び出すことによって、スライダー主導の値が AudioDevice の中に継続的に更新されて入ります。 (24) さてこれで何と、リアルタイムに SFX と音楽を個別に調整することができるようになりました!

ddblog7-3.jpg

ゴールはもうすぐです。ところが、私はここで同僚のアーティストである Morgan Roberts に対して爆弾発言をしてしまいました。「ゲームをトゥーン (漫画) 風にしたい!」と。セルシェーディングというわけではありません (これは私が目指しているものからかけ離れています)。ジオメトリをアウトライン化し、カラーコントラストを下げることによって、もっと柔らかみを出したかったのです。これは、2 つのマテリアルエフェクトをポストプロセスのチェーンに追加すれば簡単に実現できます。

まず、ジオメトリのアウトライン化を実現するために、現在のピクセルのスクリーン位置を中心にして 8 つの深度をサンプリングし、それらの平均を取ります。この平均化された「近接した」深度と現在のピクセルの深度を比較し、大きな差がある場合 (閾値よりも大きい場合) は、実際のスクリーンカラーではなく黒のカラーを返します。こうすることによって、黒いラインが縁に沿って描画されることになります。閾値を微調整することによって、すばらしい外観を手早く得ることができます。

次に、ゲーム全体のコントラスト比を調整して明るい漫画風にします。特に、低輝度のカラーを明るくしながらも、ハイエンドカラーは抑えないことにします。このためには、スクリーン ピクセル カラー値を 0.5 の値でドット化するとともに、その結果を利用して 1.5 (低輝度を明るくする) から 1.0 (高輝度にあまり影響を与えない) まで線形補間します。さらに、このスカラー値を元のシーンカラーで乗じるとともに、サチュレートすることによって一層カラフルに見えるようにします。次はその結果です。ちょっとしたポストプロセスによって大きな違いが出ます!

ddblog7-4.jpg

ddblog7-5.jpg

アート面の変更をほんの少し加えるだけでも、カラフルなファンタジー世界が開け、ある種無骨なリアリズムが漫画風の娯楽に様変わりしました。これこそが「Unreal」のポストプロセス システムのパワーです。マテリアルインスタンス定数を使用してリアルタイムにパラメータを調整することができるようになるのです。ビジュアル関係の調整が完了しました。Dungeon Defense をコミュニティにリリースするまで、いよいよ最終段階のテスティングと調整を残すだけとなりました。最終段階の結果についてはすぐに発表します!

Blog 7: ファイル レファレンス

このブログで取り上げられている情報は、次にあげるファイルに含まれています。(それらのファイルは、Dungeon Defense のソースコードです)。カンマで分けられている行番号は、ファイル内で独立した行が複数あることを示しています。ハイフンで分けられている行番号は、ファイル内で行がその範囲にまたがっていることを示しています。

  1. DunDef_SeqCond_IsOfClass.uc: 14
  2. DunDefPlayerCamera.uc: 243
  3. DunDefPlayerCamera.uc: 278
  4. DunDefPlayerCamera.uc: 288, 295
  5. DunDefPlayerCamera.uc: 286
  6. DunDefPlayerCamera.uc: 145, 205
  7. DunDefPlayerCamera.uc: 354
  8. DunDefPlayerController.uc: 1550-1557, DunDefHUD.uc: 56, 62
  9. DunDefPlayer.uc: 494-504
  10. DunDefPlayerController.uc: 226
  11. DunDefPlayerController.uc: 1586
  12. DunDefPlayerController.uc: 1594-1596, 1606-1609
  13. DunDefPlayer.uc: 88
  14. DunDefPlayerController.uc: 270
  15. DunDefPlayerController.uc: 224, DunDefPlayer.uc: 114
  16. UI_OptionsMenu.uc: 140-152
  17. UI_OptionsMenu.uc: 71
  18. DunDefViewportClient.uc: 297
  19. UI_OptionsMenu.uc: 68, DunDefViewportClient.uc: 359
  20. UI_OptionsMenu.uc: 94
  21. DunDefViewportClient.uc: 299, 354
  22. DunDefViewportClient.uc: 318
  23. DunDefViewportClient.uc: 51-52, UI_OptionsMenu.uc: 39-40
  24. UI_OptionsMenu.uc: 95

Blog 8: 26 日目

皆さんこんにちは。最後のエントリを読んでくださってありがとうございます! (これまでのところ) ゲーム開発に集中した強烈な 4 週間でしたが、全体的にはすばらしい経験をしてきました。画像を使って説明しましょう。

* 1 週目の終わりは次のような感じでした。

ddblog8-1.jpg

* 4 週目の終わりには次のようになりました。

ddblog8-2.jpg

期待どおり、「Unreal」はプロセス全体を通じて見事に機能してくれました。そのおかげで、私たちチームは短期間で実に多くのことを成し遂げることができました。これを読みながら、皆さんが Dungeon Defense を楽しんでいただければと思います。(いえ、読むのとまったく同時ということではなくて。皆さんがマルチタスク機能を搭載しているのでしたら別ですが!) 私を含めてチームは、このささやかなゲームに関する皆さんの感想をとても聞きたがっています。また、このゲームをどのように作成したのかということに関するどんな質問にも答えたいと思っています。そこでぜひ、UDK のフォーラムに参加してみてください。私もそこにいて、思いついた解決策などを披露しています。

これからしばらくは、このブログシリーズの締めくくりとして、ゲームへの全体的なアプローチについて、また「Unreal」を使用したプロトタイプの開発について論じてみたいと思います。以下は個人的な意見ですので、あらゆる状況に適用できるものではありません。また、遍く認められた意見でもありません。ただ、皆さんがゲームを作成する上で、いくらかは役に立つのではないかと考えています。

題して「Unreal ゲーム開発と Jeremy の 8 つのクレージーなルール (おやおや)」です。(ハヌカー (ユダヤ教のお祭り) にちょうど間に合いました)。

1. 早い段階でゲームの構造をしっかりと組み立て、後はイタレーション、イタレーション、イタレーション

私は、これまで数多くのプロジェクトに携わってきましたが、そこから学んだことの 1 つには、家を建てる前にはしっかりとした土台が必要になるということがあります。言い換えれば、アート面の製作活動やレベル開発に入る前にすでに、ゲームプレイの構造が面白くかつ楽しくなるような機能をもっていなければならないということです。このような言い方をすると、あまりにも当たり前のことに聞こえるかもしれませんが、実際には、いきなりコンテンツ開発にどっぷり浸かり、それが非常に楽しくて、結局は、土台を最初に作るのを忘れてしまうことがあるのです。頭痛の種を減らすには、コストがかかるアセットを作成する前に (すなわち、少なくても設計処理の前に)、どのようなゲームを作成するのかということを正確に把握するだけではなく、その時点までに当該ゲームがプレイに値するものになっていなければなりません。(これは必ずしもバグがなく、ビジュアル的な説得力があるということを意味しません)。

さらに言うならば、ゲームプレイの構造を早い段階にきっちり整えると、それだけイタレーションの時間を多く取ることができます。繰り返し修正作業を行うことによってゲームが洗練します。イタレーションは、ゲーム開発サイクルの多くの局面で必要になります。それでも、なるべく多くを、開発前の / 構想的なプロトタイピングの段階で解消できれば、それに越したことはありません。

もちろん、すでに述べたように、「Unreal」には迅速なイタレーションをサポートする優れたツールがそろっています。たとえば、Remote Control (リアルタイムで値を変更できます)、Archetypes (値をハードコード化せずに、基本的にデータ主導で扱えます)、Play In Editor (同一のアプリケーションインスタンスにおいてアクティブに修正を加えているレベル内でプレイできます) などです。これらツールのそれぞれを同時に利用するならば (たとえば、Play In Editor 内で Remote Control を開くことができます)、イタレーションを非常に速く行うことができます。ゲームプレイにはとても役に立ちます。

2. プレースホルダー アセットを使用してゲームプレイをプロトタイピングする

これまで、こんな経験はありませんか? 3D アーティストにキャラクターを用意してもらって、テクニカルアーティストにそれを整えてもらい、アニメーターに多数のアニメーションを作成してもらったあげく、これらメディアのどれ一つとしてゲームが必要としているものには適さなかったという経験が。そんな経験がないのでしたら、それは運がいいですね。私はそのような失敗をしたことがあります。言うまでもないことですが、アーティストたちは面白くないです。当然ですよね。プログラマーと設計者は、最終的なアートワークを生産パイプラインに投入する前に、そのアートがゲームプレイの目的のためにどのように作成されるべきであるかを知るとともに、担当アーティストに明確にそのことを伝えるべきです。このために私が見つけた最善の方法は、「プレースホルダー」アセットを使うということです。これは、 (たとえば) 人間型キャラクターや武器などのシンプルな汎用的なバージョンのことであって、最終的なアセットを表現するために使用することができます。理想的には、プレースホルダーの大きさ、形、(骨格メッシュの場合は) ボーン構造は、最終的なアセットとだいたい同じになるべきでしょう。ただし、仕組みの複雑さによってはそうする必要はありません。

プレースホルダーアセットを使用すると、「早い段階でゲームの構造をしっかりと組み立てる」(ルール #1 参照) ことができるばかりではなく、担当アーティストが、最終的なメディア (表現媒体) に取りかかる前に、目標とされている動作がゲーム内で機能している様子を見ることができるようになります。プレースホルダーによる実装は、考えられうる限り最も効率的な伝達手段です。さらに、アーティストは自らプレースホルダーアセットを最終的なアセットにダイレクトに交換できます。つまり、抽象的な空間においてのみイタレーションするのではなく、実際のゲームプレイのコンテンツ内においてビジュアルな環境でイタレーションできるということになります。ありがたいことに、「Unreal」を使用することによって一時的なアセットを最終的なアセットに簡単に換えることができます。いくつかの参照 (メッシュや AnimSet など) を Archetype または DefaultProperties において変更するだけで済みます。(コードで直接参照をつける必要はありません!) これでもう大丈夫です。斯くて戒律 #2 は終わりぬ。

3. 「Unreal」の流儀に従う

「Unreal」を使用してゲームプレイの成果を出すには、通常、実に多くの方法があります。しかし、理想的な方法ということになると、かなり少なくなりがちです。そのような理想的な「Unreal の流儀」を実行するには、UnrealScript のインターフェースによって提供される機能をフルに活用しなければなりません。この機能は、C++ や Java などのような基本的なプログラミング言語のそれを遥かに凌駕します。順不同に例をいくつかあげてみます。

[*]一定時間の間、あることを遅れさせたり実行させたりする場合は、潜在性の (latent) ステート関数 (Sleep や MoveTo) または Timer を使用してください。ステートメントが Tick にある場合、Time ベースの処理を多数、実行すべきではありません。 [*]プレイヤーのまわりにいる特定のアクタに関する情報を得る場合は、AllActors を実行して、ワールド内に存在する各アクタからの距離をそれぞれチェックすべきではありません。OverlappingActors を使用して、プレイヤーの半径を与えるべきです。 [*]プレイヤーキャラクター全身に防具のアタッチをセットアップする場合は、それぞれのためにアクタを作成するべきではありません。新たなメッシュコンポーネント (カスタムの Component クラスでも構いません) を動的に作成して、ポーンにアタッチしましょう! [*]1 つのテクスチャによってのみ変化するマテリアルを多数作成する場合は、それぞれのためにユニークな基本マテリアルを作成すべきではありません。親マテリアルを共有するマテリアルインスタンス定数を使用して、ディフューズ テクスチャ パラメータを換えるべきです。 [*]それから、決して、絶対に、間違っても、構造体は参照で渡すものと決めてかからないでください。デフォルトでは、関数のパラメータで out キーワードを使用しない限り、構造体はディープコピーされます。不必要に構造体をディープコピーすると、重くなりメモリも浪費され、なおかつ、構造体変数が一意ではない参照であるとロジックで想定した場合は、バグ発生の温床となっていまいます。Epic はこのことを説明するために UDN のトピックをまるごと 1 つ充てています。^^

UnrealScript とエンジンのフレームワークは、ゲームの実装が極めて確固としたエクスペリエンスとなるように作られています。これは、純粋な C++ による開発から得られた実感です (他の方と同じように私もそのような開発に従事していました)。「Unreal」を使用して開発していると、考えていた以上に独自の機能を発見することでしょう。「Unreal」で、あることを実装しようとしてもがいているときは、エンジンにすでに備わっている機能をあなたが単に知らないだけの場合があります。「ひょっとしたらあるかもしれない」と思ったら、だいたいあるものです。Epic のコード読んで、サンプルを見て、興味をひく UDN の記事を集めると (Dungeon Defense を調べても良いかもしれませんね)、完全ではないにしても、使用パターンが分かるようになるでしょう。それによって、この超パワフルなフレームワークを自由自在に使いこなすことができるでしょう。そして正しい方向に進み...

*4. コードベースを調べる!

UnrealScript エンジン フレームワークのコードベースは非常に豊富です。また、とても良くドキュメト化されています。ただ、どこから理解していけば良いのか時として途方にくれることがあるでしょう。最初から最後まで読むというのはあまり賢明なやり方とは言えないでしょう。レッドブルの点滴を受け続ければ別でしょうけども。(ただし、手始めに Actor.uc と Object.uc に関して学んでみることはお勧めできるかと思います)。

究極的に、恐ろしい巨大なコードベースを便利な百科事典にする最善の方法とは何でしょうか? ファイルの中をすべて検索する (Find All in Files) ことです。これは、Visual Studio (フリーバージョンは Visual Studio Express) のようなコード用 IDE やその他の編集プログラムに備わっている検索機能です。たとえば、「Cursor」とか「Force」といったキーワード (あるいは通常検索するようなものであれば何でも) を Epic のコードファイルのいくつか、またはすべてから検索することによって、それに関連して Epic がすでに提供している機能について多くのことが学べます。その機能によって、あらゆる種類の一般的なゲームで必要となることを扱うことができます。経験則 : 自作しようとする前に、Epic のコードベースを探し、すでに自分が必要とするものが作られていないか調べてみよ! 探しているものがすでに作られている場合がかなりあって、びっくりするかもしれません。いえ、Gears of War をプレイしていたら、驚かないでしょうね。

5. nFringe を使用する 、以上。

nFringe は素晴らしい。(よし、これは Pixel Mine 社の方に渡さなければ)。nFringe のインテリセンスとコード解析はだいたいいつも適切で、構文チェックも頑固なバグを減らすのに大いに役立ちます (単なる間の抜けたバグとは違って)。nFringe を使用することによって、コードの生産性は飛躍的に向上します。(私の場合だけかもしれませんが)。また、Epic のコードベース (とあなた自身のコードベース) を検索するスピードが非常に速くなります。インテリセンスとメンバーリストによって、簡単にクラスからクラスへと移動することができるようになり、変数や関数、ステートを素早く調べることができるようになります。

もしあなたが 「Unreal」を使用して初めてプログラミングするのであれば、nFringe によって非常に幸先の良いスタートが切れるはずです。このことは、いくら強調しても強調し足りないくらいです。ただし、現在問題が 1 つだけあります。「Unreal」には強力なデバッガがついていますが、nFringe にはロックがかかっているため、アクセスするには商用版の nFringe のライセンスを Pixel Mine 社から購入しなければならないのです。(このバージョンが、現在プロの開発者に唯一利用できるものです)。Pixel Mine 社さん、お願いですから、このとんでもなくもの凄いツールを一般大衆が使えるようにしてください。そうすれば、大衆はあなたの前にひれ伏すことでしょう! と言っても過言ではないですかね。でも、そうですね、nFringe をすぐにダウンロードしましょう。(もし、もっていなければ Visual Studio Express も)。そして、これまで書いたことがないようなコードを書いてみてください。

*6. あらゆるデバッギングの方法を意のままに使えるようにする

nFringe を使用してもしなくても、「Unreal」のフレームワークでゲームプレイをデバッグする方法は数多くあります。最高の結果を出すには、さまざまなテクニックをすべて利用する必要があります。数ある方法の中でも、次の方法は私が好んでいる方法です。

[*]描画をデバッグする (球、ライン、ボックスなど): これは、3D 空間で起きていることを視覚化するためのものです。3D 変換の計算結果を確認する場合や、単に大きさを知る場合に便利です。 [*]ログステートメント : 嗚呼、ログよ。 : スパムみたいな場合もありますが、とても有益です。特に、nFringe のデバッガが機能しない場合に! さて、ログを利用すれば、ほとんどあらゆるデータタイプを出力ウインドウに即座に表示することができます。(コンソールコマンドの showlog を使用して呼び出します)。また、@ の記号を使って複数の文字列を連結させ、1 行にできるだけ多くの情報を得ることができます。ただし、Tick の関数内に置きっぱなしにしないように注意してください。ログがスパム化するために、ゲームが重くなってしまいます。したがって、アクタクラスで bDebug のトグル (切り替え) が設定されている場合にのみログをアクティブにするのが最善です。(bDebug は、必要に応じてランタイム時に切り替えることができる編集可能な変数です)。 [*]Unlit (アンリット) モード、ワイヤーフレーム モード : 現在取り組んでいるものがグラフィカルなもので、かつ、レベルの光源処理に混乱が生じている場合、F2 を押すことによって Unlit モードに切り替えることができます。壁を透視しなければならない場合は (ずるいぞ!)、 F1 を押してワイヤーフレーム モードに切り替えられます。見えない敵が何をやっているのかを知るには、驚くほど便利です(のぞいたな!) [*]Show Collision (コリジョンの表示) コンソールコマンドによって、ワールド内のすべてのコリジョンを視覚化します。これは、コリジョンに関係すると考えられる問題に直面した場合に役立つでしょう。玄関に入れないですって? それはひょっとすると、ゲームのバグではなくて、同僚のレベルデザイナーが大きな不可視の障害物メッシュを入り口に置いて隠したのかもしれません。あなたに意地悪するために。Show Collision によってすべてがあらわになります! (より実際的に言えば、ポーンのコリジョンサイズを見るには大変役立ちます)。 [*]Remote Control 上で Time Dilation (時間の遅れ) という設定値を使用して、文字通りゲーム内の時間を遅らせます。(現実の時間を遅らせるわけではないですよ。そうだったらすごいですが、「Unreal」はまだそこまで強力ではありません。。。)。微細なゲームプレイ部分において、ビジュアル エフェクトとアニメーションがどのようになっているのかを正確に調べることによって、不自然な点を探す場合に大変役に立ちます。タイミング関係の問題を解決するために有用です。 [*]Remote Control はまた、ゲームプレイ中にこれまでスポーンされた動的なアクタをすべて表示することができます。それには、アクタリスト上で時計のアイコンをクリックします。破壊されたはずのアクタがまだ生きていないか、ダブルチェックするのに役立ちます。(たとえば、発射物が衝突した後でも、まだそのあたりに残っていないか、など)。 [*]すべての Kismet アクションには Output Comment To Screen (コメントをスクリーンに出力する) オプションがあります。このオプションが有効になっている場合、コメントがインゲームのコンソール画面に表示されます。どのようなアクションがいつトリガーされているのかを調べるのに役立ちます。あるいは、(プロフェッショナルな Kismet 権威者になるには) コンソールコマンド Kismet アクション (「話す」ための) を、任意の Kismet 変数を表示するために私が作成した Concatenate String (文字列連結) アクションと組み合わせて使用します。^^

数あるデバッギング アプローチの中でも上記のものを使用し、さらに、(願わくは) いつか、だれでも利用できる nFringe デバッガを使うようになれば、あなたは大きな目をしたバグつぶしの魔神になっていることでしょう。すばらしいことです。

*7. Kismet に明るくなろう。ただし...

Kismet よ、我々が如何に汝を愛していることか。汝は、レベルデザイナーにゲームを実装する力を与え、ゲームデザイナーに素早くプロトタイピングする能力を与える。汝は与え...そして奪う。レベルのインタラクションを実装する場合、さらにはレベル志向の特定のゲームプレイ構造をプロトタイピングする場合であっても、Kismet は、途方もなく素晴らしく、天下無双のツールとなります。ただし、弱点もあります。まず、とりわけオブジェクト指向というわけではなく、クラスの継承関係を作ることができないということです。また、UnrealScript ほど速くはありません。さらに、UnrealScript には備わっているデバッグ機能をすべて備えているわけではありません。

したがって、たいていのゲームにおいて最終バージョンを作成する場合、Kismet を通じてあらゆることを行おうとするのは、現実的なやり方とは言えません。もちろん、お望みであれば、Kismet を使って可能な限りゲームプレイをプロトタイピングしても良いでしょう。(実際、多くのことができるのですから)。ただし、憶えておかなければならないことがあります。最終的な製品のためには大半を書き直さなければならないということです。Kismet を使用して悪戦苦闘しているのであれば、私見では、UnrealScript を使用することを検討すべきだと思います。(あるいは、機能をさらに拡張する新たな Kismet アクションを書くか)。

私の全般的なルール : 継続的なレベルの設計において起きることであれば、Kismet を使用すべきである。動的にスポーンされ、動的なオブジェクトの動作に関係するものについては (レベル自体ではなく)、UnrealScript を使用するべきである。このルールは、大好きなKismet (どんなことでもしてあげたいほどです) を使用した長い経験から得られたものです。だからこれから言うことで私を悪く思わないでください。ゲーム全体を動的にスポーンする Prefab を使用して書くことは理論的に可能です。しかし、私見では、ある時点を超えると障碍になるのです。でも親愛なる読者の皆さん、ご心配には及びません。私の Kismet に対する愛情は決して褪せることはありません。Dungeon Defense について言えば、ハイレベルのゲームプレイロジック全体が Kismet で制御されているのですから。

*8. 最終的には、楽しめるということ

皆さん、私たちは独立系の開発者です。(Cliffy B、君を除いて。君は凄いセレブになったから! これを読んでくれているかな)。すなわち、楽しめるかどうかということに意識を集中させているのです。(何と言うべきか、つまりその、何億円もの予算に意識を集中しているのではなく)。もちろん、それは我々の多くにとって目標でしょう (みんな、自分の立場は分かっているものです)。そしてもっとパワーを求めてもいるのです。「Unreal」はあなたのその目標とするものへ導いてくれます。

ただし、決して忘れてほしくないことがあります。世の中に対して自分のゲームが素晴らしく、皆がプレイする価値があるということを証明しようとあなたが奮闘しているとき、どのくらいグラフィックスがきれいであるとか (役立ちはしますが)、プレイ時間がどのくらいであるとか (あぁ、人月喰いの Oblivion)、メインキャラクターの乳房にいくつのポリゴンがあるかなどは問題ではないのです。重要なことは、プレイヤーが、インタラクティブなエクスペリエンスを楽しみ、それによってすばらしいフィードバックが得られ、満足のいく見返りを受けられるかということなのです。

皮肉なことに、現場で開発してる設計者がこのことについていつも正しい判断を日々下せるとは限らない、ということがあります。そこで、友人や家族、同僚、ペットの犬たちに節目節目でプレイしてもらって、ゲームの具合についての評価を語って / 吠えてもらうことにしましょう。その評価はいつも心地よいものとは限らないでしょう。建設的な意見であっても良薬は口に苦しになる場合もあるでしょう。しかし、これがあなたとゲームのためになるはずです。「Unreal」は実際、世界で最も強力なゲーム作成テクノロジーです。ただし、それをどのように使うか、バランスのとれた楽しいゲームにするか、それとも、う?ん、Monster Madness みたいなゲームにするか、それはあなた次第なのです!

それでは皆さん。がんばって、これからも夢を現実に変えていきましょう^^

-Jeremy Stieglitz