【Unreal Engine】AI Perception入門:見つけたら追い、見失ったら探す敵を作る

作成: 2026-07-20最終更新: 2026-09-06

UE5のAI Perceptionで視覚と聴覚を設定し、見つけた・見失った・聞こえたを分けて受け取ります。Behavior Treeへ調査の枝を足し、手がかりの位置で5秒待って巡回へ戻る敵を図解します。

柱の陰に隠れたのに、敵が今の居場所をぴったり追ってくる。逆に、姿を消した途端に何もなかったように巡回へ戻ってしまう。敵らしい動きを作るには、何を知っていて、何を見失ったのかを分ける必要があります。

AI Perceptionは、UEのAIに視覚や聴覚を持たせる仕組みです。「相手が見えた」「物音がした」という通知を受け取り、それを敵の判断につなげます。

視界に入った相手に気づき、範囲の外にいる相手には気づかない敵のイメージ

この記事でわかること

  • 気づく・覚える・行動するを3つに分ける設計
  • Sightの距離・角度・遮蔽の決め方
  • 見つけた/見失った/聞こえたを、通知から見分ける方法
  • 手がかりの位置を調べてから巡回へ戻る枝の足し方

今回は、見つけたら追い、見失ったら手がかりの位置を調べ、5秒待って巡回へ戻る敵を作ります。姿が見えなくても、物音がすれば、その位置へ向かうようにします。

Sponsored

気づく・覚える・行動するを分ける

足音が聞こえたとき、AI Perceptionが知らせるのは「この場所で音がした」という情報です。その場で待つのか、音の方へ歩くのかは、別に決めます。

AI Perceptionが感知し、Blackboardへ記録し、Behavior Treeが行動を選ぶ
仕組み今回の役割
AI Perception見えた・聞こえたという情報を集める
Blackboard追う相手や、調べたい位置を保存するメモ
Behavior Treeメモを見て、追跡・調査・巡回を選ぶ

実践はBehavior Tree入門の続きです。そこで作った BP_EnemyBP_EnemyAIControllerBB_EnemyBT_Enemy を使います。敵が巡回し、プレイヤーを見つけたら追いかけるところまで動かしておいてください。

この記事では、視覚の設定と ApplySightUpdate 関数を拡張します。以前の通知処理と、今回の処理を同時に走らせないことが大切です。置き換える接続は手順の中で示します。

見る側と、見られる側を用意する

見る側は BP_EnemyAIController です。開いて「Add Component」から AI Perception を追加します。Behavior Treeの記事で追加済みなら、そのコンポーネントを使います。

AI Controllerは、敵のキャラクターを操作する側です。ここに感知の設定を集めると、受け取った情報をBlackboardへ渡しやすくなります。

AI Controllerに感知する部品を付け、プレイヤーをSightの感知対象として登録する

見られる側の BP_ThirdPersonCharacter では、次を確認します。

  1. 「Class Defaults」のActorの「Tags」に Player を追加する。Component Tagsとは別です。
  2. AI Perception Stimuli Sourceを追加する。
  3. 「Auto Register as Source」をオンにする。
  4. 「Register as Source for Senses」の「+」から AISense_Sight を追加する。

Stimuli Sourceは、「このActorを感知対象として登録する」ための部品です。付けるだけでなく、どの感覚に登録するかも指定します。

標準のSightでは、Pawn(Characterを含む)が自動登録される既定動作もあります。今回は登録を明示し、設定の違いに左右されにくい手順にしています。普通のActorを視覚の対象にするときも、この方法を使えます。

聴覚は、登録しただけで足音が自動的に届く仕組みではありません。音を出す側から通知する手順を、後で追加します。

Sightで距離・角度・遮蔽を決める

AI Perceptionの「Senses Config」に AI Sight config を追加します。すでにあれば同じ設定を開き、次の値にそろえましょう。距離の単位はcmです。

設定今回の値意味
Sight Radius1200初めて見つけられる距離。12m
Lose Sight Radius1600一度見つけた相手を、距離で見失う境界。16m
Peripheral Vision Half Angle60正面から片側60度。左右合わせて120度
Auto Success Range from Last Seen Location-1見え続ける特例を無効にする
Max Age5古い感知情報の期限。行動の待ち時間とは別
Starts Enabledオン実行開始時から視覚を有効にする

半径が2つあるのは、境界でちらつかせないため

見つける距離と見失う距離が同じだと、相手が境界をまたぐたびに、発見と見失いが切り替わります。見失う距離を少し広げると、一度見つけた相手は、少し離れても見続けられます

初めて見つける距離は12m。一度発見していれば、視線が通る間は16mまで距離に余裕を持たせる

ただし、Lose Sight Radiusの内側なら壁越しに見える、という意味ではありません。距離以外に、向きと遮蔽物の条件も満たす必要があります。

Half Angleは、視野全体の半分

Half Angleは半分の角度です。60 なら、正面から左へ60度、右へ60度。全体では120度になります。90 なら前方180度で、真後ろまで見えるわけではありません。

正面の左右に60度ずつ広がる視野。半角60度で全体120度になる

見た目の壁と、視線を遮る壁は一致させる

遮蔽(しゃへい) は、壁などに視線を遮られることです。標準のSightは、見る側と対象の間が遮られているかを調べます。

実験用に、プレイヤーが全身を隠せる大きな箱や壁を置きます。そのコリジョンで VisibilityをBlock にしてください。Blockは、そのチャンネルの判定を遮る設定です。

使うチャンネルを変更したプロジェクトでは、「Project Settings」→「AI System」の「Default Sight Collision Channel」に合わせます。詳しくはコリジョンチャンネルの記事で扱っています。

陣営の設定と、Playerタグを確認する

視界の距離と角度が合っていても、感知する陣営の設定で対象外になることがあります。「Detection by Affiliation」は、この絞り込みです。

項目対象
Detect Enemies敵対する相手
Detect Neutrals中立の相手
Detect Friendlies味方

今回のようにチームを設定していないプレイヤーは、中立として扱われます。Sightの Detect Neutralsをオン にしてください。敵のBlueprintだから、UEが自動的にプレイヤーを敵対陣営へ振り分けるわけではありません。

中立の相手を感知対象に含め、その後Playerタグでプレイヤーを選ぶ

中立を対象にすると、プレイヤー以外も通知されます。そこで通知を受けたあと、Actor Has TagでPlayerタグのある相手だけを通すようにします。

陣営の設定は「センサーが拾う対象」、Actor Tagは「拾ったあと、ゲームの処理に使う相手」を決めるものです。ここを分けて考えると、敵同士で追いかけ合う問題も防げます。

通知を「どの感覚か」で分ける

AI Perceptionの詳細にある On Target Perception Updated の「+」から、イベントを追加します。すでにある場合は、そのイベントを使います。

イベントから受け取るのは、対象の Actor と、今回の感知結果をまとめた Stimulus(刺激) です。「刺激」は難しく考えず、いつ、どこで、何を感知したかを入れた通知データだと捉えてください。

Stimulus から Break AIStimulus を作ると、中身を取り出せます。

項目今回使う意味
Successfully Sensedこの刺激を感知したか。true/falseの値
Stimulus Location刺激に記録された位置。調査先として保存する
Tag音などに付けた名前。FootstepやGunshotなどの種類分けに使える

視覚なら、Successfully Sensedがtrueで発見、falseで見失いとして扱えます。しかし、音の通知も同じイベントへ届きます。falseをすべて「プレイヤーを見失った」と扱うと、音の情報が古くなっただけで追跡を止めかねません。

同じイベントへ視覚と聴覚の通知が届く。感覚の種類を分けてからSuccessfully Sensedを読む

種類を調べるノードが Get Sense Class for Stimulus です。Stimulusを渡すと、AISense_SightAISense_Hearing といった感覚の種類を表すクラスが返ります。具体的な配線は実践で作ります。

なお、視覚は見え方の変化を、聴覚は新しく報告された音などを通知します。毎フレーム呼ばれる位置追跡用のイベントではありません

Nキーで、AIに聞こえる物音を出す

足音をスピーカーから鳴らす処理と、AIに「ここで足音がした」と知らせる処理は別です。まずは実際の音声を用意せず、通知だけを試してみましょう。

聞く側のAI Perceptionで、「Senses Config」に AI Hearing config を追加します。

設定今回の値
Hearing Range2000(20m)
Detect Neutralsオン
Max Age5
Starts Enabledオン

Sightだけでなく、Hearing側でもDetect Neutralsをオンにします。

次に、プレイヤーの BP_ThirdPersonCharacter のEvent Graphを開きます。キーボードのNイベントを追加し、「Pressed」から Report Noise Event へ白い実行線をつなぎます。

ピン接続・値
Noise LocationGet Actor Location(TargetはSelf)のReturn Value
Loudness1.0
InstigatorSelf
Max Range0
TagFootstep
NキーからReport Noise Eventを呼び、プレイヤーの位置とSelfを渡す

Instigatorは「この音を発生させたActor」です。今回ならプレイヤー自身なのでSelfを渡します。受け取る側では、この相手にPlayerタグがあるかを確認します。

Loudnessは音の強さ、Max Rangeは音側で設ける距離の上限です。今回はLoudnessを1、Max Rangeを0にして音側の上限を設けず、聞く側のHearing Rangeで20mまでとします。Loudnessと距離の関係は設定の組み合わせで変わるため、最初は1のまま範囲を調整すると分かりやすくなります。

音声の再生とAIへの通知は別。今回の条件では20m以内の聞く側が反応する

標準のHearingは、Sightのように壁がVisibilityをBlockしているだけでは遮断されません。壁越しに音へ反応するのは、この実験では正常です。壁でどれだけ音を弱めるかまで扱う場合は、別の判定を足します。

あとで足音へ連動させるなら、Nキーの代わりにAnimation Notifyから同じ通知を呼べます。

Sponsored

実践:手がかりを調べて巡回へ戻る

ここから、感知の通知をBlackboardへ書き、敵の行動につなぎます。見えている相手を追うことと、以前そこにいたと分かる場所を調べることが、今回の使い分けです。

巡�回、発見、追跡、見失い、手がかりの位置で5秒待つ流れ

調査中は、その場所で5秒とどまります。周囲を歩き回る探索アニメーションなどはまだ付けず、行動の切り替わりを確認します。

1. 調査先のメモを足す

BB_EnemyLastKnownLocation を追加し、型を Vector にします。Vectorは、ここではX・Y・Zの3値で位置を表す型です。Behavior Treeの記事で作った2項目と合わせて、次の3つを使います。

Key名保存するもの
TargetActorObject(Base Class: Actor)今見えている、追跡対象の相手
LastKnownLocationVector視覚や物音から得た調査先の座標
PatrolLocationVector次の巡回先

Actorの参照は、相手が動いても同じ相手を指します。Vectorは、保存した時点の座標です。 音だけを頼りにするときは座標を使い、姿を見ていない相手の現在位置を追わせないようにします。

2. 通知の入口を1つにする

BP_EnemyAIController で、次の3関数を用意します。すでにある ApplySightUpdate は、入力を1つ追加して中身を更新します。

関数入力
HandlePerceptionNoticeNoticeActor:Actor Object Reference、NoticeStimulus:AIStimulus
ApplySightUpdateSeenActor:Actor Object Reference、bSensed:Boolean、SensedLocation:Vector
ApplyNoiseUpdatebSensed:Boolean、NoiseLocation:Vector

まず、イベントの実行出力を HandlePerceptionNotice へつなぎます。ActorをNoticeActor、StimulusをNoticeStimulusへ渡します。以前のイベントからApplySightUpdateへの直接接続は、この接続に置き換えてください。

イベントから対象と刺激をHandlePerceptionNoticeへ渡す

HandlePerceptionNoticeの中では、次の順に接続します。

  1. NoticeActorを Actor Has Tag のTargetへ渡し、Tagを Player にする。
  2. 結果を Branch のConditionへつなぐ。関数入口の実行出力をBranchへつなぎ、False側は処理を終える。
  3. True側から Get Sense Class for Stimulus の実行入力へつなぎ、StimulusにはNoticeStimulusを渡す。
  4. Return Valueから Equal (Class) を作り、比較するクラスを AISense_Sight にする。結果を次のBranchのConditionへつなぎ、Get Sense Class for Stimulusの実行出力も、そのBranchへつなぐ。
  5. True側でApplySightUpdateを呼ぶ。False側では、もう一つのEqual (Class)とBranchで AISense_Hearing かを確認し、Trueの場合だけApplyNoiseUpdateを呼ぶ。
Playerタグの確認後、刺激の感覚クラスを調べてSightとHearingの処理へ分ける

2つの呼び出しへ渡す値は、以下です。NoticeStimulusから Break AIStimulus を作って取り出します。

呼び出す関数渡す値
ApplySightUpdateSeenActor=NoticeActor、bSensed=Successfully Sensed、SensedLocation=Stimulus Location
ApplyNoiseUpdatebSensed=Successfully Sensed、NoiseLocation=Stimulus Location

Actor Has TagとEqual (Class)は、値を調べるノードなので白い実行線を通しません。一方、Get Sense Class for Stimulusには白い実行ピンがあります。Getという名前だけで省略しないようにしてください。

この段階では2つの関数の入口にPrint Stringを置き、視覚側で Sight、聴覚側で Hearing と表示すると確認できます。各Blueprintをコンパイル・保存してからPlayし、正面に入る・壁に隠れる・Nキーを押す、を試しましょう。目と耳の通知が分かれて届けば、次の処理を足せます。

Actor TagのPlayerは「誰か」、感覚クラスは「目か耳か」、音のTagのFootstepは「どんな音か」を表します。音のタグ名をGunshotへ変えても、感覚の種類はHearingのままです。

3. 見えている相手と、調べる座標を書き分ける

AI ControllerでBlackboardを読み書きするときは、Get Blackboard(Target=Self)のReturn Valueからノードを作ります。Set Value as ObjectSet Value as VectorGet Value as ObjectClear Value のTargetは、すべてこのBlackboard Componentです。

この Blackboard Componentは、実行中のこの敵のメモを操作するための部品です。コンテンツブラウザのBB_Enemyアセットや、敵のCharacterをTargetへつなぐ場所ではありません。既存の On Possess → Run Behavior Tree(BT_Enemy) は残しておきます。

ApplySightUpdateにBehavior Treeの記事の処理が残っている場合は、以下の処理へ置き換えます。入口からBranchへつなぎ、ConditionにbSensedを渡します。共通の入口でPlayerタグを確認したので、この関数では感知結果を扱います。

True:相手が見えた

  1. Set Value as Objectで、Key Nameを TargetActor、Object ValueをSeenActorにする。
  2. 続けてClear Valueで、Key Nameを LastKnownLocation にする。

相手の姿が見えたら追跡を優先し、以前の調査先は消します。

見えた相手をTargetActorへ保存し、古いLastKnownLocationを消す

False:追っている相手を見失った

  1. Get Value as Objectで TargetActor を読み、SeenActorと Equal (Object) で比べる。
  2. 結果をBranchのConditionへつなぎ、bSensedがFalseの実行線を、このBranchへつなぐ。
  3. Trueの場合だけ、Set Value as Vectorで LastKnownLocation にSensedLocationを書き込む。
  4. 続けてClear Valueで TargetActor を消す。比較結果がFalseなら何もしない。
同じ相手を見失った場合、調査座標を保存してから追跡対象を消す

今回は、その視覚通知に記録された位置を調査先にします。感知は一定の間隔で更新されるため、画面上の最後の1フレームの位置と厳密に一致するとは限りません。隠れたあとの相手へGet Actor Locationを呼び続けると、AIが知らないはずの現在位置まで追ってしまうので、ここでは保存した座標を使います。

先に座標を書き、その後で追跡対象を消すと、追跡が終わった時点で次の調査先が用意されています。同じ相手かを比べるのは、別のActorの通知で追跡対象を消さないためです。

聴覚:見えていないときだけ、音の位置を調べる

ApplyNoiseUpdateは、次の接続にします。

  1. 入口からBranchへつなぎ、ConditionにbSensedを渡す。False側は何もしない。
  2. True側で、Get Value as Objectから TargetActor を読む。そのReturn Valueを Is Valid で調べる。
  3. 有効な相手がいなければ、Set Value as Vectorで LastKnownLocation にNoiseLocationを書く。相手がいれば何もしない。
音を感知し、見えている追跡対象がいない場合だけ調査先を書き込む

Is Validは、参照先が有効かを確かめるノードです。ここでは実行ピン付きのIs Validを使い、「Is Not Valid」出力から保存処理へ進めます。音の情報が古くなった通知では、調査先をもう一度書きません。

これで、目の前のプレイヤーを追っている途中に足音が鳴っても、調査へ切り替わらずに済みます。

4. 調査の終わりに、メモを消すTaskを作る

BT_Enemy の「New Task」から BTTask_BlueprintBase を選び、BTT_ClearLastKnownを作ります。変数 TargetKey を追加し、型を Blackboard Key Selector、「Instance Editable」をオンにします。

Key Selectorは、木に置いたTaskから「どのメモを消すか」を指定するための型です。次の3ノードを白い実行線でつなぎます。

Event Receive Execute AI → Clear Blackboard Value → Finish Execute

Clear Blackboard ValueのKeyにはTargetKeyのGetを渡し、Finish ExecuteのSuccessをオンにしてください。

TaskはTargetKeyで指定したBlackboardの値を消し、Finish Executeで成功を返す

ControllerではKey Nameで操作しましたが、BTTaskの中ではKey Selectorを受け取るBlackboard用ノードを使います。名前が似ているため、使う場所とKeyピンの型を見比べましょう。

5. 追跡と巡回の間に、調査の枝を入れる

BT_Enemy のSelectorへSequenceを追加し、名前を「調べる」にします。左から、追跡・調査・巡回の順になるように配置します。

左から追跡、調査、巡回の3つの枝を選ぶBehavior Tree

Behavior Treeの記事の「追って攻撃する」と「巡回する」は残します。追跡側はTargetActorがIs Set、Observer AbortsがBothであることを確認してください。

「調べる」のSequenceに、BlackboardのDecoratorを追加します。Decoratorは、その枝を使う条件です。

設定
Blackboard KeyLastKnownLocation
Key QueryIs Set
Notify ObserverOn Value Change
Observer AbortsBoth

Is Setは「調査先が入っているか」。Bothは、条件の変化で実行中の枝や右側の巡回を中断できる設定です。On Value Changeでは、空かどうかだけでなく座標の変更も監視します。新しい場所で物音がしたときも、調査のやり直しにつなげられます。

Sequenceの下に、Move To、Wait、BTT_ClearLastKnownを左から順に接続します。3つはSequenceの子として並べます。Task同士を親子につなぐ構造ではありません。

調査のSequenceにMove To、Wait 5秒、メモ消去Taskが子として並ぶ。Move ToにはForce Successを付ける
ノード設定
Move ToBlackboard Key=LastKnownLocation、Acceptable Radius=100
Move Toの到着判定Reach Test Includes Agent Radius / Goal Radiusを両方オフ
Move Toの経路Allow Partial Pathをオフ、Observe Blackboard Valueをオン
WaitWait Time=5、Random Deviation=0
BTT_ClearLastKnownTarget Key=LastKnownLocation

到着の許容距離を100cmにして、目印の約1m以内で待ちます。5秒は、移動が終わってからの時間です。

また、調査のMove Toを右クリックし、「Add Decorator」から Force Success を追加します。これは、移動が失敗しても、木には成功扱いで次へ進ませるものです。目的地へ強制的に移動させる機能ではありません。

行けない地点だった場合は、そこで5秒待って調査を終えます。この処理がないと、移動の失敗でSequenceが止まり、最後のメモ消去まで進めず、同じ調査を繰り返してしまいます。

BTT_ClearLastKnownで値を消すと、調査の条件が外れて巡回へ戻れます。(0, 0, 0) を書くのは「原点を調べる」という別の意味なので、空にするときはClearを使う点も覚えておきましょう。

6. 動きとメモを見比べる

各Blueprintをコンパイルし、Behavior Treeも含めて保存します。試験場所は、同じ高さの広い床に、全身を隠せる壁を1つ。敵とプレイヤーの間にNavMeshがつながっていることも、P で確かめておきます。

Play中にBT_Enemyを開き、「Debug Object」で配置した敵のControllerを選ぶと、実行中の枝とBlackboardの値を見られます。

試すこと見る結果
正面の12m以内へ入るTargetActorにプレイヤーが入り、追跡へ切り替わる
壁の陰へ移動する調査座標が入り、TargetActorが空になる
隠れたまま待つ敵が調査先へ向かい、到着後5秒待って巡回へ戻る
待っている途中で姿を見せる待機を中断し、追跡へ切り替わる
姿が見えない場所でNを押す20m以内ならHearingの通知が届き、音の位置を調べに来る
調査中に、別の位置でNを押すLastKnownLocationが変わり、新しい場所の調査へ切り替わる

場所を変えながら音を何度も出すと、調査先が更新され、なかなか巡回へ戻りません。最初の確認ではNを一度だけ押し、敵が待ち終わるまで静かにしてみてください。

うまくいかない場合は、通知が届くか → メモが変わるか → 木が切り替わるか → 移動できるかの順に見ます。

症状確認するところ
Sightのログが出ない視野・距離・遮蔽物、SightのDetect Neutrals、対象の登録とPlayerタグ
Hearingのログが出ないN入力、Report Noise Eventの実行、Instigator、Hearingの設定
ログは出るがメモが変わらないGet BlackboardのTarget、Key Nameの綴り、Run Behavior Treeでの起動
メモは変わるが巡回を続ける枝の左右順、DecoratorのKeyとObserver Aborts
調査先から戻らないWaitの後のTask、Target Key、Finish Execute
壁の裏を追い続けるSightの遮蔽設定とAuto Success Range、TargetActorが消えているか
真後ろのはずなのに発見される敵自身が旋回していないか、視覚と聴覚を混同していないか、実際の視野角

移動そのものが止まる場合は、NavMeshの記事のNキーの移動実験で土台を確認できます。

Sponsored

感知の期限と、探す時間は別

「Max Ageを5にしたから、5秒探してくれる」と考えたくなりますが、この2つは担当が違います。

Perceptionの古い情報の期限と、Behavior Treeの到着後5秒待機を別々の時間軸で示す

Max Ageは、Perceptionが持つ感知情報の期限です。0は期限なし。古いActorの情報を忘れる動作を使う場合は、「Project Settings」→「AI System」の「Forget Stale Actors」も有効にします。

一方、Waitは敵の行動としての待ち時間です。この記事では「調査先へ移動してから5秒待つ」と木に書き、その後でLastKnownLocationを消しています。Perception側の情報が期限切れになっても、自分でBlackboardへ保存した値が自動で消えるわけではありません。

視界を実際に表示する

Play中のGameplay Debuggerでも、感知の状態を確認できます。既定では '(アポストロフィ)で開き、テンキーの 4 でPerceptionの情報を表示します。日本語配列などで開けない場合は、プロジェクト設定のGameplay Debuggerで「Activation Key」を確認してください。

視野角を狭くしたり広くしたりすると、背後を取れる範囲も変わります。まずはHalf Angleを 60 → 30 にして、敵の正面へ回り込む実験をすると違いが分かりやすいでしょう。

「見え続ける特例」は、ステルスの手触りを変える

Auto Success Range from Last Seen Locationは、相手が最後に見えた位置から、どれだけ近くにいれば見えている扱いを続けるかという設定です。AI自身からの距離ではありません。

遮蔽を試す間は-1のままにしておき、基本の動きができてから調整しましょう。Dominant Senseも、問い合わせた対象位置で優先する感覚を選ぶ設定であり、今回のBlackboardへの書き込み順や行動の優先順位を自動で決めるものではありません。

おまけ:先に知っておくと良いこと

  • 見えているのに気づかない: Sightは距離だけでなく 角度と遮蔽 の影響を受けます。正面にいても、あいだのメッシュのCollision設定によっては見つけてもらえません
  • 陣営は後回しでよい: Blueprintだけで陣営を扱うと詰まりやすい部分です。まずは Detect Neutrals を使い、敵か味方かはタグやインターフェースで判断すると進めやすくなります
  • 通知は「変化」で届く: 毎フレーム「見えている」と報告されるわけではありません。見つけた瞬間と見失った瞬間に届きます。追いかけ続けたいなら、最後に見えた位置を自分で覚えておきます
  • 感知の期限と、探す時間は別: Perception側が忘れるまでの時間と、AIが探し続ける時間は別々に決められます

まとめ

見えている相手はActorとして追い、姿を見失ったときや物音だけがしたときは、座標を手がかりにします。この違いがあると、敵は「今の居場所を知っている」と「このあたりにいたはず」を使い分けられます。

まずは通知の種類とBlackboardの値を見ながら、隠れる・物音を出す・再び姿を見せる、を試してみてください。追跡と調査が切り替わる理由が分かれば、足音の大きさや探す時間も、遊びに合わせて調整できます。

参考資料

Unreal Engine このセクションのノート98