ActionScriptでのNavMeshのパスの検索-CrossBridge Port Recast Navigation





この記事では、 FlasCCコンパイラーを使用してC ++コードをActionScriptに移植した経験について説明し、 それを使用してパスを見つける問題を解決するかなり大量の有用なコードを移植する方法を示します。 最後に、デモとコードを含むリポジトリへのリンクがあります。 それまでの間、それがどのように始まったかについてのいくつかの言葉。



私はindiprojectに取り組んでいます。 これは、トップダウンシューティングゲームです(クリムゾンランドに似ています)。 AS3でプロジェクトを書いています。 ゲーム空間は連続的で(セルではなく)、Nape物理エンジンに基づいています。 通行不能なセクション(壁)は、凸多角形のセットで記述されます。 このようなマップ上でエージェント(モンスター)をナビゲートする問題が発生します。



この問題を解決するためのアイデアは何ですか?



1.スペースをセルに分割し、市松模様のフィールド上のパスを見つけるタスクに減らすことができます。 このため、Flashには既製のソリューションがすでに存在します。 しかし、エージェントを常にあるセルから隣接するセルに強制的に移動させるという点で、このオプションは好きではありませんでした。 このソリューションでは、エージェントのサイズも考慮されていませんが、エージェントのサイズは異なる場合があります。



2.透磁率グラフを作成できます。 エージェントが直線で目標に到達できない場合、アルゴリズムはエージェント、グラフノードのペアに最も近い目標を見つけ、グラフに沿ってそれらの間にルートを構築します。 私はこのアプローチを一時的なものとしてプロジェクトに実装しました。 次のように配置されました。レベルエディターでは、接続を指定せずにノード(ウェイポイント)が設定されました。 レベルのロード中に、システムはあらゆる種類のノードペア間の直接的な通過性を判断し、ノード間の接続を設定しました。 このソリューションはしばらく私に適していましたが、すぐにいくつかの欠陥が面倒になりました。

-エージェントのサイズはまだ考慮されていません。 開通性に基づいて、さまざまなサイズのエージェントがゲーム内で状況を構築できれば素晴らしいことです。

-ノードはエディターで手動で設定する必要があります。 一般に、このトピックに常に戻る必要があるのは不快です。 また、ルートの品質は、ノードの配置状態に依存します。

-ルートが最適ではありません。 多くの場合、エージェントが最初に「ターゲットから」最も近いノードに移動し、そこからのみ続くときに状況が発生し始めました。 直接トラフィックには他のノードはありませんでした。

-エージェントはお互いの存在を考慮しません。 モンスターの群れが動くとき、彼らの動きの直線性は非常に顕著であり、現実的に見えません。



3.別のアプローチは、ナビゲーションメッシュです。 アイデアは次のとおりです。マップの通行可能エリアは、互いに隣接する一連の凸ポリゴンで覆われています。 各ポリゴン内で、エージェントは自由に移動できます。 これは私の場合、非常に良い解決策です。Flashに存在するツールを探し始めました。 一般的には何もありませんでした。 しかし、C ++にはあります。 Recast Navigationと呼ばれる一連のツールは非常に深刻なものであり、必要なものはすべて揃っています。 公平に言えば、GithubにはAlchemyを介して移植されたRecastのデモがありますが、これは再利用できないデモにすぎません。



そして、私はそれが必要であることに気づきました。 これをAS3に転送する方法を考え始めました。 少し側に移動して、Nape物理エンジンを使用する前に、Box2Dを使用しようとしました。Flashでは、AS3に書き換えられ、Alchemyを使用して移植されます。 AS3にリライトされたバリアントには、バグ修正であれ新しい機能であれ、変更​​ごとに対応するコードをエンジンのFlashバージョンに手動で転送する必要があるという欠点がありました。 ある時点で、彼らは単に彼を捨てました。 しかし、Alchemyを介した移植は基本的に好きでした。 元のエンジンのAPIを繰り返す一連の関数に加えて、著者は、C ++の対応するクラスと構造にAS3ラッパークラスのセットを追加しました。 したがって、FlashバージョンのAPIは、ラッパークラスを使用して単純に動作するポインターの転送を監視する必要性から開発者を隠しました。



CrossBridge (例:Alchemy)を通してすべてを移植することにしました。 CrossBridgeは、独自のgccコンパイラーを備えたCygwinベースの環境です。 コンパイラはFlasCCと呼ばれ、出力はActionScriptバイトコードです。 一般的な進捗は次のとおりです。C++の各メソッドはAS3のメソッドに関連付けられており、AS 3はC ++関数のプロトタイプをできるだけ正確に繰り返します。 そのような関数のセットは、swcでFlasCCを使用してコンパイルされます。 次に、このswcはプロジェクトに接続されます。プロジェクトでは、最終API(ドキュメント付き)を使用して別のswcが作成されます。



元のコードと同様に、このポートを開くことにしました。 すべてのコードはこちら: github.com/Rokannon/Crossbridge-Recast-Navigation

オリジナルのC ++コードを含むリポジトリがサブモジュールとして追加されました。



ここで起こったことの小さなデモを見ることができます:

work.rokannon.com/navmesh_demo

右側のパネルで、レベルジオメトリを記述するソースメッシュを選択する必要があります。 次に、パネルを下にスクロールし、[ビルド]をクリックして、NavMeshがビルドされるまで少し待ちます。 左側のパネルには、ツールボックスの機能を示す2つのツールがあります。パスの検索と多数のエージェントのナビゲーションです。



すべての機能がまだ転送されているわけではありませんが、非常に多くの機能が転送されています。 したがって、コードはすでに使用できます。 リポジトリをサブスクライブして、調整を続けてください。



これで公式の部分は終わりました。 ご清聴ありがとうございました。 誰か質問があれば、尋ねてください。



そして、実装の複雑さに興味がある人のために、C ++コードをAS3に移植するときに使用したトリックを描きました。



最も簡単な例



#include <AS3\AS3.h> // ,     AS3. float testFunc(x:float, y:float) { return x + y; } void _testFunc() __attribute__((used, annotate("as3sig:public function internal_testFunc(x:Number, y:Number):Number"), annotate("as3package:ctest"))); void _testFunc() { float x; AS3_GetScalarFromVar(x, x); //    -,  - AS3. float y; AS3_GetScalarFromVar(y, y); AS3_Return(x + y); }
      
      







CrossBridgeを実行すると、このコードをコンパイルできます。

 /cygdrive/c/Crossbridge_1.0.1/sdk/usr/bin/g++ "main.cpp" -emit-swc=ctest -o "out.swc"
      
      





その結果、結果のswcの関数はinternal_testFunc(x:Number、y:Number):Numberになります。これは、C ++のtestFuncと同じ働きをします。 すべての機能は、このパターンに従って移植されます。 接頭辞internal_が特別に選択されます。 最終的なswcからこのメソッドを完全に非表示にすることはできません。



構造



このクラスは、構造体のラッパークラスのベースとして使用されます。

 package ctest { use namespace c_internal; public class CBase { c_internal var ptr:int; public function alloc():Boolean { return false; } public function free():void { CModule.free(ptr); } } }
      
      





ここに注意を払うのは理にかなっていますか? ラッパークラスには、ラップする構造体またはクラスのアドレスが格納されます。 AS3にはポインターはありません。 代わりに、intのみが使用されます。 ptr変数は、最終的なAPIを除いてどこからでもアクセスできる必要があるため、特別なカスタム名前空間c_internalによって非表示になります。



C ++でこの構造を検討してください。

 struct MyStruct { int x, int y };
      
      





ラッパークラスとしてのそのような構造は、次のものに対応します。

 package ctest { use namespace c_internal; public class MyStruct extends CBase { c_internal static var SIZE:int = 0; c_internal static const OFFSET_X:int = offset(4); c_internal static const OFFSET_Y:int = offset(4); private static function offset(size:int):int { return (SIZE += size) - size; } public function get x():int { return CModule.read32(ptr + OFFSET_X); } public function set x(value:int):void { CModule.write32(ptr + OFFSET_X, value); } public function get y():int { return CModule.read32(ptr + OFFSET_Y); } public function set y(value:int):void { CModule.write32(ptr + OFFSET_Y, value); } public override function alloc():Boolean { ptr = CModule.alloc(SIZE); return ptr != 0; } } }
      
      





ここでは、小さなトリックにより、すべてのフィールドのインデントを便利に計算でき、構造のサイズを取得するためのボーナスとして使用できます。 しかし、 メモリアライメントに関連する落とし穴が1つあります 。大まかに言って、データが1ワード(4バイト)に完全に収まらない場合、インデントは4バイトの倍数でなければなりません。 たとえば、構造:

 struct ExampleStruct { char x, char y, int z }
      
      





XY 0 0 ZZZZ



ようにバイト単位で配置されます。 また、 sizeof(ExampleStruct)



は8バイトであり、6バイトではありません。



引数にポインターがある関数



 #include <AS3\AS3.h> void addNum(MyStruct* s, int n) { s->x += n; s->y += n; } void _addNum() __attribute__((used, annotate("as3sig:public function internal_addNum(s_ptr:int, n:int):void"), annotate("as3package:ctest"))); void _testFunc() { MyStruct* s; AS3_GetScalarFromVar(s, s_ptr); int n; AS3_GetScalarFromVar(n, n); addNum(s, n); }
      
      





開発者からポインターをロールする必要を隠すために、最終的なAPIには次のような関数があります。

 package ctest { use namespace c_internal; public function addNum(s:MyStruct, n:int):void { internal_addNum(s.ptr, n); } }
      
      







ポインターを返す関数



 #include <AS3\AS3.h> MyStruct* sum(MyStruct* s1, MyStruct* s2) { s1->x += s2->x; s1->y += s2->y; return s1; } void _sum() __attribute__((used, annotate("as3sig:public function internal_sum(s1_ptr:int, s2_ptr:int):int"), annotate("as3package:ctest"))); void _testFunc() { MyStruct* s1; AS3_GetScalarFromVar(s1, s1_ptr); MyStruct* s2; AS3_GetScalarFromVar(s2, s2_ptr); AS3_Return(sum(s1, s2)); }
      
      





ラッパーは、アドレス以外のラップされる構造/クラスに関するデータを保存しないため、APIメソッドへのオプションの引数として追加すると便利です。

 package ctest { use namespace c_internal; public function sum(s1:MyStruct, s2:MyStruct, resultMyStruct:MyStruct = null):MyStruct { if (resultMyStruct == null) { resultMyStruct = new MyStruct(); } resultMyStruct.ptr = internal(s1.ptr, s2.ptr); return resultMyStruct; } }
      
      





同様の手法を使用して、配列の要素にアクセスします。



All Articles