AI制作の手引き その4「詰め・Varの応用、他」 by ADI 16:43 2011/11/07 17:51 2011/10/29 速度計算のところを修正 4:13 2011/06/08 Velの性質4−3追加 15:09 2011/05/28 Velの性質1−2追加、2修正 15:09 2011/05/28 Velの性質3−1追加 13:35 2011/05/16 TempVarとGetHitVar(Hitcount)バグについて 16:01 2011/03/05 52番ステート ・・・ 19:41 2009/10/30〜 |
AIを一通り作ったら、調整を行います。 が、後は個々人のセンスです。 調整は主に見つかった問題を解決することですので、 調整方法の一例をご紹介します。あと補足知識も。 ※ただ、至らない云々をとやかく言えるのは、制作者本人たちだけです。 文句があるなら自分がやる。基本です。 |
.弱点補助 >対めくり補助 今更。作っていて気づいた方もいるかもしれませんが、 例に書いた記述の通りだと「めくり」に対して一切反応しないはずです。 それは「Attack.Dist(Guard.Dist)」が「キャラ中央から前」にのみ伸び、 後ろ側にいると「Attack.Dist(Guard.Dist)」の範囲に入らないため、 InGuardDistの条件が満たさせずめくりに反応しないのです。 一部の特殊な技でも同様の状態が発生しガード困難な場合もありますが、 手操作ではガード方向(向きの後ろ)へレバーを倒しておけばガードできます。 一番簡単な対策方法としては、「相手後ろ向き+攻撃動作中」を加えた TriggerAll = InGuardDist || P2Dist X < 20 && EnemyNear,P2Dist X <= 0 && EnemyNear,MoveType = A ;密着〜後ろ側+相手後ろ向き攻撃中 というInGuardDist条件。手操作ならガードできるはずなので性能改変には当たりませんが、 この場合どんなに離れていてもガード反応をしてしまいますし、 飛び道具や分身などのめくりには対応できません。 複雑な方法としてはヘルパーを使うもの。 詳しくは[ヘルパーに関するメモ]をご参照してください。 またどちらとも確率を加えないと確実にガードしてしまいますので、 TriggerAll = Random < 50 || P2Dist X > 20 || BackEdgeDist < 10;低確率||切替寸前でない||後ろ壁 などでガードの確率を制御してあげる方がいいかとおもいます。 |
.抑制調整 >動作確率調整 明確に強いわけではない相手にあまりダメージも与えられず 負けてしまう場合などに考慮すべきことです。 動作には隙がつきもので、例え回避動作であっても よほど高性能なもので無い限り、何かしらの隙が出来ています。 動作をしていないために攻撃できないのではなく、 動作をしてしまっているために攻撃できていない可能性もあるのです。 特に同じような状態で食らっていることを確認した場合、 同じ状態を減らすため、その動作を調整が望ましいでしょう。 後述する記憶システム以外にも、最初から確率を下げたり、 条件を見直すことでそうした隙が生まれる可能性を減らせます。 |
.下方調整 >レベル設定 例えば、最良の動作を追及した結果強いけどつまらなかったり、 見栄えのよいコンボはあるけど威力がコストに見合わないなどの時、 強さの上限は維持しつつ、下限を広げることによって、 強さも見れるし、面白いことも見れるということを実現するのが、「レベル設定」です。 詳しいことはAILv論の方をご覧ください。 応用の一例として、 Lv数値も使ってコンボの確率を割り振りたい場合は、 Trigger* = Random < 800 && Var(*AILv*) >= 10 ;10以上 Trigger* = Random < 500 && Var(*AILv*) = [7,9];7〜9 Trigger* = Random < 250 && Var(*AILv*) = [4,6];4〜6 Trigger* = Random < 100 && Var(*AILv*) = [1,3];1〜3 と言う様な条件をそれぞれの技に並べるよりも、コンボ自体のレベルをVarで管理し、 Trigger1 = 1 || Var(*ComboLv*):= 0; 0レベル・コンボ無し(この例ではLv1~10の変動型を想定しています) Trigger1 = Random < Var(*AILv*)*800 ;Lv1で80%,Lv2以上で100% Trigger1 = Var(*ComboLv*):= 1; 1レベル・すぐに〆 Trigger1 = Random < Var(*AILv*)*400 ;Lv1で40%,Lv3以上で100% Trigger1 = Var(*ComboLv*):= 2; 2レベル・ちょっとだけコンボ Trigger1 = Random < Var(*AILv*)*200 ;Lv1で20%,Lv5以上で100% Trigger1 = Var(*ComboLv*):= 3; 3レベル・見栄え重視コンボ Trigger1 = Random < Var(*AILv*)*Var(*AILv*)*10 ;Lv1で1%,Lv5で25%,Lv10で100% Trigger1 = Var(*ComboLv*):= 4; 4レベル・やや強いコンボ Trigger1 = Random < Var(*AILv*)*Var(*AILv*)* 9 ;Lv1で0.9%,Lv5で22.5%,Lv10で90% Trigger1 = Var(*ComboLv*):= 5; 5レベル・最良コンボ と言う風にLv1なら1レベルが80%,2レベルが32%,3レベルが6.4%,4レベルが0.064%というくらいの確率で、 先に確率の処理を行い技側に Var(*ComboLv*)=x や Var(*ComboLv*)=[x,x] で条件を加える方が、 確率やコンボの確認管理は楽になります。 その他、以下のような記述を先頭に足しておくことで Trigger1 =(Var(*ComboLv*)=[0,9]);コンボ中でない コンボ始動記述でVarの数値を変更しコンボルートを管理するというのもあり。 Trigger1 = Var(*ComboLv*)=999999 || P2MoveType != H ;コンボ終了数値or相手くらい中でない Trigger1 = Var(*ComboLv*):=0 ;リセット こうしたコンボ管理変数があれば通常技の暴発も防ぎやすくなるが、 無闇やたらに使う変数を増やすと、後の処理が面倒になってしまうため注意 レベル設定などの調節ができると、同キャラの違うレベル同士で戦わせ違いを見るなど、 一つのAIでも楽しみ方の幅が広がります。 >始動速度遅延 攻撃などの動作始動が速すぎるため相手の攻撃に潰されたり、 あるいは反撃などが速すぎてそればっかりになってしまう場合などには、 遅延条件を加えて始動を遅くして、様子を見るようにするのが「遅延処理」です。 ただし遅延処理を行う事によって危機を回避する分、 チャンスを逃したりすることもありえますので、ご自身の裁量で。 反撃・回避反応用の反応レベル式は>こちら< 基本的な遅延処理は、 Time > x が基本なのですが、これには多少問題があり、 ご存知の通り、Timeは「そのステート内での経過フレーム数」を数えますが、 Ctrlがあると小まめに動いていたり、立ちステート維持などの処理を行っても、 Timeがその度にリセットされてしまい、遅延が長いと動く確率もかなり下がります。 例えばTime>=60で1秒遅延させたい場合、最低60Fになりますが動作を考慮すると、 少なくとも1秒以上の長い遅延の効果が掛かり、ほとんど使えない状態とも言えます。 乱暴に言ってしまえば、信頼のできない条件になってしまうのです。 10程度であればそこまでの差は生まれませんが6Fは0.1秒、 12Fでは0.2秒だけしか違わず、息継ぎ程度の遅延にしかなりません。 確実な代替案はCtrlやMoveType=Iでの経過時間をVarで加算管理する方式。 Timeの代わりのようなイメージで条件を配置し、行動でプラスから0にリセット、 コンボが続けばAILvによってマイナスまでリセットするなどの応用も可能です。 攻撃を受けたときには多くプラスしたり、反対にマイナスになったりなども。 調整次第でAIには無いCommandの制限関係の遅延「らしく」見せることもできます。 最初に下方調整とありますが、実際は「不安定な部分の安定化」が目的です。 これらには対人やリスク回避などの考えも含まれています。 ただの弱体化ではないのです。 >その他 ■低確率不確定動作 私の使っているシステムで、旧名は対人用動作という名前でした。 文字通り、低確率で不確定な動作を行わせるもので、 実質メインとは別に低確率の大雑把なAIをつけることです。 問題として動作が不安定になりやすく、記述量がかなり多くなります。 かといって、目に見えるような見返りは無く、 今までも散々言ってきたパターンの緩和程度です。 元々パターンが少ないのであれば、必要ないかと。 ちなみに当たらない位置、当たらない状態でも攻撃すれば、 反応が必ずしも正確ではないと印象づけられ、 正確な反応とただのぶっぱかを混ぜてしまえば 多少反応速度を上げても、不正確さを口実にできます。 >参考[AIによる超反応の状態]のコメント ■自重システム 1度使ったらしばらくの間使わないというシステム。 Trigger1 = StateNo = xx && Time = 1;でVarSetを行い、時間経過と共に下げることで、 短い間隔で同じ技を使わないようにする、というもの。 特に有効な技だからといって使いすぎると、対人戦では「覚えられる危険」が増えます。 そのため見せる回数を減らすことにより、覚えられる危険を減らす、というもの。 もしくは単なる弱体化です。 ■A・Bスイッチ Aを使ったあとはAを使わずBを使い、Bを使ったBはBを使わずAを使う。 という技振りがしたい場合はVarを1つ使えば簡単にできます。 [state -1, 技] Type = ChangeState Value = Var(**):= xxxx ;技のステートをVarにも記録する。 TriggerAll = Var(**)!=xxxx ;記録したものでない。 TriggerAll = **** ;技の条件・他 このような記述方法を使うことで、連続で同じ技を使いません。 ただし、PrevStateNoなどを使っているわけではないため、 同じような記述をした別の技を使わない限り、この技を使うことはできませんが。 ■不確定認識 低確率不確定動作の本動作にさせるもの。 相手の情報を使うトリガー(P2,Enemy他)をRandom>=xxx||などで、 相手の情報がそうでない時にも技を振らせるようにしていしまいます。 Varを使うのなら P2BodyDist YやP2Dist Yなど相手の情報をVarで管理し、 代入した数値をレベルなどによって、変動させてしまう感じです。 またP2StateTypeやP2MoveTypeの情報をVarで管理し、 正確に反映させることを確率で抑え、たまにミスをさせるなど。 不確定動作でも言っていましたが、多くミスをするのであれば 正確な動作があっても、ミスの延長であると印象づけられます。 ■動作傾向切り替え型 基本動作を常時ランダムだけではなく、タイプ別に切り替える方式。 例えば、1.突撃タイプ 2.迎撃タイプ 3.空襲タイプといった分け方をし、 しばらく同じタイプの動作を行わせ、適度なタイミングで切り替えるというもの。 時間経過時以外にも、攻撃を食らった時などに切り替えるので、 パターンでのやられ続けなどを抑制できるかと思います。 この方式の特長は「タイプで動作を限定する」ことで、 特に近づく方法を歩いて・ダッシュ・攻撃・ジャンプという風にも分けられ、 動作の多いキャラでも歩き動作などを行わせ続けたりできます。 デメリットとしては複数の動作タイプを管理する必要があるのでやや面倒な点。 |
.キャラ対策 プログラムが最も苦手とするのは「無い情報に対する対応」です。 例えば、当身技を察知する情報は無いため反応は困難で、 暗転のあるものは、対人だと攻撃に対して行わないと待たれますが、 AIの場合は基本「攻撃動作中である」としか認識できず、はまります。 相手の基本的な攻撃範囲も、攻撃の中下段やガード不可も分かりません。 人間の場合は、「出来なかったら別の方法を試みる」ことを平然と行えますが、 AIの場合「できなかったを確認し別の行動を行わせる」ことは難しいです。 しかし、それらに対応することが不可能なわけではありません。 それらを補うのがP2Nameや記憶システムです。 >P2Name EnemyNear,Name="***" && EnemyNear,AuthorName="***" 相手のNameで、「相手キャラを認識する」という方法。 つまり、あらかじめ相手キャラの事を記述してしまい対応を行わせるもので、 もちろん記述してあるキャラクターにしか対応できません。 ただし、これは「特定の相手に対して有利をつける」という行為であり、 大げさに言えば タブー視されている方法 でもあります。 そのため理由も無く無闇やたらに使わないことはマナーです。 まあ、理由も無く無闇やたらに使えるほど容易なことでもありませんが。 そのため、使う部分はあくまで手操作では簡単だが、 基本的なAIトリガーでは対処が困難なことに限定し、 確実でなくそこそこの確率で対応を行う程度にとどめましょう。 手操作では簡単だが〜の例としては、ガード不可にガードしないことや、 当身技に引っかからないことなど。手間のかかるものとしては飛び道具の種類の確認。 ガードの上下などもそうですが、対応させる場合にはガード自体をVarで制御することに。 >記憶システム 呼び方としては学習機能とも。 そして、情報が無いなら収集すれば良いじゃないと言うのが記憶システム 分かりやすく言えば「状況を数値化して覚え、行動へ反映さえるシステム」のこと。 詳しくは[記憶システムについての話/txt]を参照してください。 こちらも使い方によっては凶悪さを引き出せる為、 手操作では簡単だが〜を目安にして、無闇に使わないようにしましょう。 これみたいなことは、控えるように。 P2Nameと違い相手を選びませんが、 「一度以上その状況になること」と「多くのVarスロット」が必要になる他、 システムによっては複雑な記述になる為、十分な知識と技術も必要になります。 とは言うものの、簡単なシステムであればVarも手間も掛かりません。 例えば「特定技が失敗した」ことを記憶させて「技の頻度を下げる」ということは楽です。 ;■失敗値加算 [State adi, xxx];攻撃を潰された。 Type = Varadd;Var加算 TriggerAll = Var(*AIフラグ*);AI処理条件 Trigger1 = MoveType = H ;くらい中である Trigger1 = Var(*Prev*) = xxxx;その直前のステート番号がxxxx var(*x失敗値*) = 1 IgNoreHitPause = 1 [State adi, xxx];当たらずに終わった(回避された・他) Type = Varadd;Var加算 TriggerAll = Var(*AIフラグ*);AI処理条件 Trigger1 = StateNo = xxxx ;ステート番号がxxxx(終了時のステートにあわせる。) Trigger1 = AnimTime = 0 ;技アニメの終了。(技の終了条件にあわせる) Trigger1 = P2BodyDist X = [x,xx] ;範囲内で Trigger1 = P2Dist Y = [x,xx] ;範囲内で Trigger1 = !MoveContact ;当たらなかった。始動技や投げ技用が主。 var(*x失敗値*) = 2;多めに IgNoreHitPause = 1 ;■StateNo記憶 [State adi, xxx];暫定・他に良い方法がある。 Type = Null TriggerAll = Var(*AIフラグ*);AI処理条件 Trigger1 = Var(*Prev*):= StateNo IgnoreHitPause = 1 と言う形で失敗を数え、技の方へ TriggerAll = Random%(1+Var(*失敗値*)) = 0; 失敗値が増えるほど確率が減る。 と記述し、こんな感じに確率を下げることができる。 技が成功した場合、失敗値を減らすということもできます。 どういう状態に対して、まで記憶するとなると複雑になりますが。 こうした記憶システムは、キャラによって重要な場面ができます。 例えば、主な攻撃手段が投げ技の場合、投げ技の効かないキャラは天敵です。 しかし投げ技が入らないことを察知し、投げ技を使わず、 攻撃を打撃に切り替えれば、戦えないこともありません。 人間であれば失敗を繰り返さないことは当然とできますし、 何よりキャラとしては死活問題ですので対応してもいい問題です。 またそのことは他のキャラにも言えることですが、 全ての技の失敗値を記録するにはVarが少なすぎるため、 重要な技が使えるかどうか、始動技が使えるかどうか程度になるかと。 ただし、「回避されて入らない」場合もあるので、そうした考慮も必要ですが。 またこうした記憶を次のラウンドへ引き継ぐ場合、相手側がチームモードだとキャラは変わるため、 戦闘開始前に相手のRoundsExisted(経験ラウンド数)でリセットするかどうかを確認しましょう。 RoundsExistedは、初めてのラウンドは0で数え始め、次のラウンドに1、その次が2になりますが、 チームモードでキャラが変わった際には、RoundsExistedは再び0になります。 なので、Enemy,RoundsExisted = 0で、相手が新しいキャラかどうかを察知できるわけです。 |
.Varの応用 いままでも色々なVarの使い方を紹介していますが、 その他のVarの応用方法などを紹介。 >PrevStateNo 例えばCtrlがある状態だと、信頼できなくなるPrevStateNoを、 Varで管理することにより確実なPrevStateNoの情報を持てます。 ただし、コンボなどはコンボ用にVarを管理した方が確実ですが。 [State adi, PrevStateNo] Type = Null ;Varset := TriggerAll = Var(*AIフラグ*);AI処理条件 Trigger1 = Var(*1*) != StateNo ;Timeがあり*1*とステート番号が違う時。 Trigger1 = 1 || Var(*2*) := Var(*1*) ;*1*を*2*へと代入し、 Trigger1 = Var(*1*) := StateNo ;*1*に現在のステート番号を代入する。 Ignorehitpause = 1 ;Var(*2*)でPrevStateNoを確認できる。 ;StateNo部分はIfElse( ((StateNo=[10,49])||(StateNo=[120,140])) ,0 ,StateNo);レバー系動作を立ち(0)と同様に ; と言う様な記述に変えると、基本動作同士では上書きしなくなる。 こうしたものは記憶システムなどでもよく使います。 自分だけでなく相手のステートも記録しておくことで、 何に対して、の情報を正確に取得することができたりなど。 >記述圧縮用Var 何度も繰り返し記述することになる条件を、 Varでフラグ管理し記述をそのVar1つに済ませるのが記述圧縮用Var。 ちなみにEnemyNear(Var(x)),なども、その一例です。 例えば、 TriggerAll = Alive && RoundState = 2 && Var(*AIフラグ*); AI最低限記述であるこれらを [State adi, FF];AI最低限フラグ Type = Null Trigger1 = Var(*AI行動フラグ*) := ( Alive && RoundState = 2 && Var(*AIフラグ*) ) Ignorehitpause = 1 と言う方法でVarSetを行い技側には、 TriggerAll = Var(*AI行動フラグ*);AI最低限記述 このVar1つだけを書く形にする事で、文字数を結構減らせます。 ただし、これらだけだと処理的には同じですので、文字数が減る以外のメリットはありませんが、 [State adi, operation unify - Off];オフ type = Varset var(*AI動作*) = 0 TriggerAll = Var(*AIフラグ*);AI処理条件 Trigger1 = !Alive || RoundState !=2 ;最低限記述を満たさない [State adi, operation unify - Neutral];ニュートラル Type = Varset var(*AI動作*) = 1 TriggerAll = Alive && RoundState = 2 && Var(*AIフラグ*);AI最低限記述 Trigger1 = Var(*AI動作*) = 0;オフから起動 Trigger2 = Var(*AI動作*) = [100,xxx];コンボなどから Trigger2 = P2MoveType != H || P2StateType = L;相手がくらい中でなくなった時か倒れ状態になった時。 ;Var(*AI動作*) ; 0 = オフ(AI停止状態) ; 1 = AIの基本動作 ; 2~10 特異動作 ;10~99 特定状況など ;100~*** コンボなど と言う風な感じにすれば、AIの動作パターンを管理することにも使えます。 コンボはつなぐ技のValueに +(Var(*AI動作*):=xx)*0 という感じで代入するか、 これらと一緒に状態を監視して変更していく形に。ただ技で代入する方が楽なはずです。 また特定状況、例えば起き攻めなどは、こちらで監視して変更する形になりますが、 コンボでそういったものを追加した場合も含め、それぞれニュートラルへ戻す記述は必要です。 ちなみに、AIフラグとAI動作をあわせることは出来ますが、 あまり合わせ過ぎても管理が面倒なので割愛。 もちろんよく分からない場合、無理に導入する必要はありません。 こうした応用は自分でわかる範囲にとどめましょう。 >Var共有 また、Varの使いすぎには注意しましょう。 計100とはいえど多くありませんし、キャラの分を差し引けば少ないものです。 その為、Varを使うものは限定し、それでも多い場合は、 [ヘルパー/txt]や、Varを共有させるなどの対処が必要になります。 ヘルパーの方は主にtxtを参照していただくとして、 Varの共有とは「1つのVarで複数の情報を管理する」というものです。 ■桁分割方式 Varの限界値は[ 2147483647 , -2147483648 ]ですので、 9桁までは、0〜9を安定して使うことができます。※Fvarは処理が違うため無理です。 しかしVarで使う桁数は多くても3、4桁で十分なことも多く、 [,,,,,xxxx]という感じに大きい桁数が広く空いてしまいます。 そこで4桁までしか使わないの情報と、3桁までしか使わないの情報がある時、 [,,,,,xxxx]の使っていない桁へ[,,,,,,yyy]を入れることによって、 [,,yyyxxxx]と言うように1つのVarへまとめてしまおうというのが、この方式です。 参照はFloor(Var(**)/10000)とVar(**)%10000などの不要な桁を除外する数式で、 代入はそれらで代入する以外の桁も参照して一緒に代入することになります。 分かるかとは思いますが、かなり面倒で、最終手段と思っても構いません。 使うとしても、頻繁に記述するようなVarは避け、 使う回数の少ないVarにとどめておくことをオススメします。 ■bitフラグ方式 詳しくは[ビット演算のメモ/txt]を参照してください。 そちらで私に可能な限り、説明しています。 大雑把に説明すると、Varは[ 32bit-Int型 ]で数値を保持しています。 bit(ビット)とは[ 0か1 ]の情報で、Varはそれが[ 32bit ]連なっていまして、 [ 00000000000000000000000000000000 ]で 0 を表し、 [ 01111111111111111111111111111111 ]は最大値の 2147483647 を表し、 [ 10000000000000000000000000000000 ]が最小値の-2147483648 を表しています。 二つの数値のこうした0と1の並びを比べて、数値を変更するのが[ bit演算 ]で、 それを使って32個それぞれで0,1のフラグを管理する方式が[ bitフラグ方式 ]です。 ( ただし32個目はマイナスの数値なので、あまり使われてないかと思いますが。 ) bit演算を行うための、[ bit演算子 ]は以下のようなもので &(and)はbitの同じ桁の数値が1同士なら1に、それ以外の桁は0にするもの。 1&1=1 1&0=0 0&0=0 |(or) はbitのどちらかの桁に1があれば1に、無ければ0のままにするもの。 1|1=1 1|0=1 0|0=0 ^(xor)はbitのどちらかの桁だけが1なら1に、それ以外は0にしちゃうもの。 1^1=0 1^0=1 0^0=0 MUGENではこの3つしか使えません。多分 またbit演算子や数式などを応用することで、 このbitで分けた中に複数の数値を入れることも可能です が、そこまでする人は稀有です。いますけど。 >その他 ■設定項目 AILv論に書いてあることですが。 特に際立った性能を持つ部分などは、AILvとは別に調整できるようにしておく方が便利です。 特殊防御(ブロッキング等)しかり、強力な攻撃(コンボなど)もしかり。 多様な戦い方の出来るキャラクターであれば、 それらの傾向の調整ができても役に立つかもしれません。 ■感情システム 大雑把に言ってしまうと、失敗記憶システムなどの簡易版。 変動数値を特定の動作に対するものではなく、戦い方の傾向へ影響させるようにして、 数値の変動条件も多様な場面で合わせるようにしたものです。 例としてはガードを崩された・攻撃を潰されたなどの時にネガティブへ、 攻撃を潰した・ガードを潰したなどの時にポジティブへ傾くようにし、 ネガティブではガードを優先しやすく、ポジティブでは攻撃を優先しやすくなる感じ。 で特にネガティブなら超必殺技の使用も避けたり、 特にポジティブなら超必殺技を遠慮なくぶっぱなしたりなどの、極端さも。 またそれ以外でも、攻撃をたくさん受けた後に「ひよる」動作をさせたり、 ライフを大きく減らされた時に逃げを優先する「チキン状態」にしたりなど。 アイディア次第で、様々な使い方ができます。 |
.Varの応用2・条件の補助・Temporary Var (2011/05/16追記) 「情報を保持し続けられる」という特性は必ずしも活かす必要はありません。 「1フレーム中の一部の記述」でのみ使うようなVarを「TempVar」という風に呼び、 主に条件式や計算式をTempVarに用いることで式を分割し条件式を簡素化させることができます。 代表的な例としては、速度計算の際にフレーム数などをVarへ入れるようにすると 他の条件式でもフレーム数や距離条件を変えるだけで使えます。 [state -1,うんぬん];慣性のつく空中技の例。 Type = ChangeState Value = *技のステート番号* TriggerAll = AI最低限条件〜 TriggerAll = 前提条件〜 TriggerAll = 状況条件〜 ; TriggerAll= fvar(9) := 12 ;★攻撃判定発生フレーム TriggerAll= Pos Y + ( (Vel Y*fvar(9)) + (Const(Movement.Yaccel))*( (fvar(9)*(fvar(9)-1))/2 ) ) < 0 TriggerAll =1 || fvar(9) := fvar(9)-(EnemyNear,ID<ID&&!(EnemyNear,MoveType!=A&&MoveType=A)||EnemyNear,MoveType=A&&MoveType!=A);発生フレーム補正(処理順が相手の方が早いなら-1) ;判定発生まで自分が着地しない。 重力係数はYAccel*( (n*(n-1))/2 )で計算可能。 ; TriggerAll=1||fvar(8):= EnemyNear,Vel X ;相手の速度(コンボではEnemyNear,GetHitVar(XVel)*EnemyNear,Facingかな?) TriggerAll=1||fvar(8):= 0-(fvar(8)*fvar(9)*fvar(14)*(Ifelse(EnemyNear,StateType!=A,0.5,1))) TriggerAll=1||fvar(8):= fvar(8)*(EnemyNear,Vel X > 0 || EnemyNear,BackEdgeDist >5);相手が壁際でないか、 TriggerAll=1||fvar(8):= fvar(8)*(EnemyNear,Vel X < 0 || EnemyNear,FrontEdgeDist>5);相手が壁に向かってない。 TriggerAll=1||fvar(9):= fvar(9)+(EnemyNear,ID<ID&&!(EnemyNear,MoveType!=A&&MoveType=A)||EnemyNear,MoveType=A&&MoveType!=A);発生フレーム補正戻し(自分の速度を計算するため。) TriggerAll=1||fvar(8):= fvar(8) - (Vel X*fvar(9))*(Ifelse((Vel X<0),BackEdgeDist,FrontEdgeDist)>5)) ;適当なX速度(摩擦係数の合計を仮に0.5としてますが実際はもっと複雑です。) TriggerAll= (fvar(30) = [-5,5])|| Fvar(30) + fvar(8) = [ -100 , 59 ] ;★P2BodyDist X(-5,5は衝突中用) ; ↑でX距離の計算終了。以下Y距離の計算。 ; TriggerAll =1 || fvar(9) := fvar(9)-(EnemyNear,ID<ID&&!(EnemyNear,MoveType!=A&&MoveType=A)||EnemyNear,MoveType=A&&MoveType!=A);発生フレーム補正 TriggerAll=1||fvar(8):= EnemyNear,Const(Movement.YAccel) ;相手の落下係数(コンボならEnemyNear,GetHitVar(YAccel)) TriggerAll=1||fvar(8):= ( (EnemyNear,Vel Y*fvar(9))+(fvar(8))*( (fvar(9)*(fvar(9)-1))/2 ) );相手の速度 TriggerAll=1||fvar(8):= fvar(8) * (EnemyNear,StateType=A) ;相手が空中でないなら相手速度計算不要。 TriggerAll=1||fvar(9):= fvar(9)+(EnemyNear,ID<ID&&!(EnemyNear,MoveType!=A&&MoveType=A)||EnemyNear,MoveType=A&&MoveType!=A);発生フレーム補正戻し TriggerAll=1||fvar(8):= fvar(8) - ( (Vel Y*fvar(9))+(Const(Movement.YAccel))*( (fvar(9)*(fvar(9)+1))/2 ) ) ;適当なY速度 Trigger1 = EnemyNear,Pos Y > -10 &&(Pos Y - fvar(32) + fvar(8) = [-73,14]);★P2Dist Y,相手地上 Trigger2 = Fvar(31) + fvar(8) =[-73,14] ;★P2Dist Y,相手中心へ ;調整する場所は大体★の行だけで使えます。 使えるはず。 ;fvar(8),fvar(9)以外のfvarの内容はコチラ↓これらの数値はTempでなく常用のVar。 ;>(Fvar(30):=P2BodyDist X) (Fvar(31):=P2Dist Y + EnemyNear,Const(Size.Head.Pos.Y)) ;>(Fvar(32):=IfElse(EnemyNear,StateType=S,EnemyNear,Const(Size.Head.Pos.Y),EnemyNear,Const(Size.Mid.Pos.Y))) ;>(Fvar(14):=IfElse((Pos X <= EnemyNear,Pos X ^^ EnemyNear,Facing = 1),1,-1));相手の向き数値 ;あとenemyNearも本来は生きてる相手を指定するようになってます。 こうした記述なら発生フレームと距離条件を変えるだけで他の記述にも使うことができて便利です。 もちろん発生までに相手の速度が変化したら当たる保証はありませんが、 相手と自分の速度を認識して攻撃するため技の精度や判断の速さは向上します。 速度計算は素早い判断のために必要とも言えますが、X距離の摩擦関係は精確な計算がかなり面倒です。 どのくらい面倒かといえば攻撃を当てた後の19F後のX距離を計算させたい場合 TriggerAll=1||fvar(8):=0&&Fvar(9):=EnemyNear,GetHitVar(XVel)*EnemyNear,Facing;相手速度 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));1 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));2 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));3 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));4 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));5 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));6 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));7 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));8 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));9 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));10 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));11 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));12 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));13 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));14 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));15 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));16 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));17 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));18 TriggerAll=1||fvar(8):=Fvar(8)+(Fvar(9):=Fvar(9)*Fvar(7))*(EnemyNear,BackEdgeDist>10+fvar(8));19 ;Fvar(7):=相手の摩擦係数。 Trigger1 = fvar(30)-fvar(8) < 70 && P2Dist X > 0;P2BodyDist X ;※相手速度,EnemyNear,GetHitVar(XVel)*EnemyNear,Facingはヒットポーズ中限定 ;※ヒットポーズを抜けた場合はEnemyNear,Vel Xで処理する必要があります。 こんな感じになります。訳がわかりません。 フレーム数に応じて条件の行を増やしたり減らしたりすることになります。 累乗計算が正確に出来ないため、こうした回りくどい方法を取らざるをえないのですが、 TempVarを使わない場合、おそらく一行の文字数制限に引っかかって使えません。 前者の速度計算でも、TempVarを使わないとかなり簡素化しなければ使えないでしょう。 ただし、ここまで複雑な計算をしていても、 相手の速度処理が特殊な場合や途中で速度を変化させた場合は計算が無意味になるので注意。 フレーム数が大きければ大きいほどその危険性が高まるので、全ての技でここまでする必要はありません。 「繊細な記述」というのは「不測の状況に脆い記述」なのです。 あと別に難しい計算はせずとも汎用的な記述にするためfvarを使うこともあります。 Trigger1 = StateNo = xxx && MoveContact = 1 ;技の接触直後 Trigger1 = fvar(8) := 相手硬直フレーム-着地硬直フレーム-地上最速技発生フレーム ; Trigger1 = Pos Y + ( (Vel Y*fvar(8)) + (Const(Movement.Yaccel))*( (fvar(8)*(fvar(8)-1))/2 ) ) > 0 ;着地F算出 ;こうした条件で状況Varを変更し空中技から着地直後に最速攻撃をつなげさせるという感じ。 これなら他の技で感知させたい時も記述を増やしてfvar(8)に代入する数値を変えるだけでいいので便利。 詳しい速度計算に関しては>速度計算のメモ<を参照してください。 記述テンプレートもあるので、使えそうでしたらどうぞ。 |
.細かいこと ■ ^^ の使い道。(xor,どちらかが真である) 〜と〜のどちらかが真なら真、という意味の「 ^^ 」 私の理解範囲で唯一使える方法は(↑の例でも使っている)「向き判定」だけです。 自分が相手へ向いているかどうか、また相手が自分へ向いているかどうかを調べる際、 一対一の場合はP2Dist Xで判定できてしまいますが、 HelperType=Playerがいたりタッグモードの場合は、不正確になってしまいます。 そこで「自分が右側 ^^ 自分が右向き」を条件にすることで、 Trigger1 = Pos X > EnemyNear,Pos X ^^ Facing = 1;自分が右側^^自分が右向き ; 自分が右○ 右向き○ では 敵 自→ ; 自分が左× 右向き○ なら 自→ 敵 ○真 ; 自分が右○ 左向き× なら 敵 ←自 ○真 ; 自分が左× 左向き× では ←自 敵 このように、自分が相手に向いているかどうかを一行で知ることができます。 相手が〜となれば「相手が右側 ^^ 相手が右向き」を条件にして、 Trigger1 = Pos X <= EnemyNear,Pos X ^^ EnemyNear,Facing = 1 相手の向きを確認することができる上、この条件式を少し変えることで、 Trigger1 = Pos X <= EnemyNear(1),Pos X ^^ EnemyNear(1),Facing = 1 ;二番目に近い相手が〜 Trigger1 = Pos X <= Partner,Pos X ^^ Partner,Facing = 1 ;パートナーが自分へ向いている のようにP2に限定されない相手の向きを知ることができます。 ただこれの使い道は、私の分かる範囲で↑の例でも使っている向き判定と Posで距離を計算するときの、const(Size.***.(Front or Back))の判定くらいです。 ■Velの性質 >速度処理(Vel)の基本/Vels.txt<を参照 ■Attack.Dist(またはGuard.Dist)の距離 InGuardDistで感知される距離の話です。 めくりの話で「GuardDistは基準位置から前にしか伸びない」と話しましたが、 ではどこまで伸びるのか? という話。 まずAttack.Distの設定はCns[Size]のAttack.Distの数値が基本的に使われ、 それ以外にもステートコントローラーのType=AttackDistの指定数値、 技命令Type=HitDefの中のGuard.Distで設定された数値などが使われます。 Projectileの場合は、Proj.Attack.Distなどなど。 相手本体からのAttackDistをInGuardDistで感知できる最長距離は、 「基準位置から基準位置まで」のP2Dist Xとほぼ同じ数値になります。 実は「基準位置まで」Attack.Distが届いていなければガードの反応はしません。 まあめくりの話からも分かることなのですが、 P2BodyDist Xのような衝突判定までではないのです。 ■MUGEN本体のGetHitVarバグ(AIに関わる部分の話) Win版のMUGENではGetHitVarがMoveType!=Hでも残ったまま参照できてしまうため、 GetHitVar(HitCount)だけでダメージ補正を換算してると、 コンボの後の次の一撃目まで補正が入ってしまうという不具合がたまにあります。 それだけなら可愛いものなのですが、GetHitvar(HitCount)がリセットされない不具合があるのです。 GetHitvar(HitCount)はMoveType!=Hからガード以外のMoveType=Hになったときリセットされます。 つまりガードのヒット硬直中にガードを崩すとGetHitvar(HitCount)がリセットされないので、 コンボの後にガードさせて中下段などで崩すとGetHitvar(HitCount)が継続されてしまい、 ノックバック補正などがGetHitvar(HitCount)制御だと、コンボ数よりも強い補正がかかるわけです。 なので、もしGetHitVar(HitCount)でノックバックなどが制御されている場合は、 コンボ始動時にGetHitVar(HitCount)を感知させて別のコンボもできるようにしておくと安全です。 ■1P2Pの違いによる勝率の差異 1P2Pの違いに、勝率が影響するかどうかの話ですが、 行動が一定であったり、行動自体が確実である場合、強く発生します。 酷い場合、キャラの組み合わせは一緒でも、 1Pか2Pかで勝つ方が決まってしまうようなこともあります。 これは結局、RandomやCPUなどの不確定要素が無ければ、 試合の経過や結果は何度繰り返しても同じになることの延長です。 そういうこともある為、勝率を数える場合は1P2Pを入れ替えたりもしましょう。 特に相手の情報を利用した処理が多い場合、顕著になりやすいかと。 代表例としてはInGuardDistの時、基本的な攻撃を行わない場合、 先に攻撃をする側が処理の早い1P側になりやすく、 1P先手→2P反応の状態が度々起こってしまうことになり、 2P側の反応が1Pに勝つか負けるかで勝敗が決まってしまう状態です。 それがもっと極端になったものが不確定要素の無いAI同士の対決ですね。 ■整頓すること。 記述の方法が乱雑であったり配置が整頓されていないと、 ミスや不具合の原因になったり、忘れたとき分からなくなります。 そのため システム・ガード・コンボ・行動 などを明確に分けて、 Varの条件や複雑な記述は何を意味するのか説明を加えておくと後で楽です。 単純な記述であっても名札をつけておきましょう。 そうしておくことで、他の人が見ても分かりやすいAIにできます。 ■分からないとき・思いつかないとき 悩みつつ、かといって休憩という感じではないときは、 他の人のAI記述を見たり、トリガー表やステコン表を眺めましょう。 意外な発見があったりします。 ただし、AIによっては記述が理解のできない領域に達している場合もあります。 ( 特にbit方式の数値も使って管理しきっているキャラとか。 ) またCNS処理などでちょっと分からないことを調べたいとき、 私の場合テスト用のKFMを使ってます。 この手引きに書かれている細かいことの一部はそうして調べました。 >その5へ |
.あまり関係なこと ※私の主観 ■インフレーション? 私は昔のことは知りませんが、 昔のAIが昨今のAIに敵わなくなったのだとしたら、 AIの目指す強さや技術の蓄積量にあるのではないでしょうか。 第一にAIが単なる「対戦相手」ではなく「自動操作」になったのでは? つまり人間が勝てるかどうかは二の次になっているのでは、ということ。 現在では超反応AI同士、なんて言葉が使われるように、 より「最良のプレイヤー」を目指しているのではないでしょうか。 そして第二に、増え続けるキャラに相手の多様化と、 そのキャラ対策と技術の蓄積による動作の正確さの向上、などでは? 以前の条件式では「予想外」が多くなりやすく、 その「予想外」を少なくすることができているのではと。 今回教えている記憶システム、失敗値のシステムはその最たる例でしょう。 ただ昔のAIが現在でも強い〜という話はAIだけの話じゃないと思います。 それは現在の技術でAIを作ればもっと強くなる可能性があります。 簡潔に言えば「AIだけの強さではない」ということです。 ■良い勝負をすること 私がこのAI制作の手引きで教えたいことは「良い勝負をさせる」ことです。 このサイトのトップページに 「強さの極限に娯楽はない。そして求められるのは娯楽である。」 と書いてあるのは、AIはあくまでそのための役者であるという意趣、趣向です。 そのためにここでは、あくまでパターンを嫌い、確実性を嫌い、 複数の選択、不確定性の選択、魅せ要素の選択を教えています。 しかし、これらは弱みです。だけれども、単純な弱みではありません。 いかに相手と戦うかという意図を持った強みに頼らぬ弱みです。 勝つだけが勝負ではありません。勝つか負けるかの瀬戸際こそ勝負。 私にとっては「勝った!」と言わせることが、何よりの喜びです。 もちろん、勝てばそれはそれでうれしいものですが、 極端な話、自分がおかしいのではないか、とも考えられるので。 ですので、おかしいな、と思うところは自分の裁量で調整しちゃってください。 ■Extremelyに思いをはせて しかしながら、強さ、というものも目指してみたくなるものです。 もしその形が思いついたならば、AILvのExで挑戦してみてはいかがでしょうか? 特殊ガードや無敵に当身を最大限使い、無闇な隙を作らず確実な攻撃だけをし、 永久・ループがあるのならば遠慮なく使い、死に技や下位互換は一切使わない。 飛び道具でのトリカゴも無闇にせず、遠距離に隙の少ない技がなければ近づき、 隙が少なければ中距離からの突進技、空中からの攻撃は対空を考慮して控える。 近距離は1F投げか待ちからの超反応を主とし、対空も超反応させることを考える。 相手に勝たせるつもりのない、エクストリームリー。 もちろん、ちゃんと「勝つだけ」のレベルだと注意書きはしておきましょう。 ■アッパー改変について 分かりやすく言うとボスモードの話。 キャラによってはAIレベルを最大限に上げると性能も変化するというもの。 私としては特殊モードとしてあってもいいとは思いますが、 するなら改変していいのかを確認し、改変の箇所と状態を明示すべきです。 ただそれはいわゆるCPU製作の難しいシミュレーションゲームなどで見られる AIのつたなさを補うような「CPUハンデ」であり、AIそのものではありません。 なので、この手引きにおいては説明していません。 ■ADI流 良い試合をさせるためには、勝った試合にまず悩み、負けた試合にまず笑おう。 そして、何をすればいいか、改めて考えよう。 文句があるなら自分がやる。 文句を言われても自分で判断し決める。 |