「回避で使った方法がそのまま使えなかった」
前回の記事で、回避(Roll)のラグ対策として「Receiver Authority」を実装しました。
ざっくり言うと「自分の画面でギリギリ回避したならノーダメージ」を、サーバーが最終判定しても保証するしくみです。
ガード(Block)にも同じことをしたい。
ボタンを押したのにガードが間に合わなかった、という体験をなくしたい。
でも実装しようとしたら、回避とは根本的に別の方法が必要だとわかりました。
回避とガードで何が違うのか
回避の場合、ダメージを受けるかどうかは「避けたか/避けなかったか」の2択です。
避けていたなら、その攻撃は完全に空振り扱いにすればいい。
実装上は CanTakeDamage = false にするだけで、攻撃の当たり判定を完全に無視できます。
ガードの場合、それが使えません。
ガードは「攻撃が当たった上で受け止める」という動作です。
CanTakeDamage = false にしてしまうと、FCS 内部の EventBlock が呼ばれなくなる。
EventBlock の中にはスタミナ消費・パリィ判定・ガードブレイクがまとめて入っています。
つまりガードの「中身」が全部スキップされてしまいます。
| 回避(Roll) | ガード(Block) | |
|---|---|---|
| 攻撃の扱い | 完全ミス(すり抜け) | 当たった上で受け止める |
CanTakeDamage = false |
使える | 使えない(EventBlock が呼ばれなくなる) |
| スタミナ消費 | なし | あり(ガードブレイクに直結) |
| パリィ判定 | なし | あり |
では何をどう変えるか
問題の本質
FCS の標準実装では、ガード判定の核心部分である CanBlockAttack がサーバー側の BlockUp 変数を参照しています。
CanBlockAttack → サーバーの BlockUp == true?
→ true:EventBlock(ガード成功処理)
→ false:TakeDamage(通常ダメージ)
Ping が 100ms あると、ボタンを押してからサーバーに届くまでに 100ms かかります。
その間に攻撃が来ると、サーバーの BlockUp はまだ false のまま。
自分の画面ではガードしているのに、サーバーはガードしていないと判断してしまいます。
解決策:クライアントの BlockUp をサーバーに「押し付ける」
考え方はシンプルです。
- クライアントでガードボタンを押した瞬間、即座に
BlockUp = trueをセット(これが「自分がガードしていた」という正確な情報源) - 攻撃が飛んできたとき、クライアントでその
BlockUpの値をサーバーへ送る - サーバーはその値で自分の
BlockUpを強制上書きしてから、既存の判定フローに流す
Server_AcceptDamage の冒頭:
DefensiveComponent → Set BlockUp(クライアントから受け取った値で上書き)
↓
CanBlockAttack(上書き後の BlockUp を評価)
→ true:EventBlock(スタミナ消費・パリィ・ガードブレイク)
→ false:TakeDamage(通常ダメージ)
CanBlockAttack をスキップしないのが重要です。
この関数には「凍結中はガード不可」「ガード不能攻撃を通す」といった属性チェックが含まれているからです。
スキップするとゲームのバランスが崩れます。
もう一つの改修:アニメーションの即時化
ダメージ遮断の話とは別に、ガードのアニメーション自体も遅延しています。
FCS の標準実装では、ボタンを押すと:
1. SendBlock(クライアント)
2. SR_BeginBlock(Server RPC)
3. MC_BeginBlock(Multicast)← ここでアニメーション再生
Multicast を経由するので、アニメーション開始まで Ping 分待たされます。
「ガードを構えた感触」がなく、操作が重く感じます。
こちらは攻撃システムで使っている「3段構えアーキテクチャ」をそのまま流用します。
【ローカル即時】ボタン押下
→ Set BlockUp(true) + PlayAnimMontage + MovementSpeed(Block)
→ SR_StartGuard(自作 Server RPC)
【サーバー】SR_StartGuard
→ 状態更新(BlockUp, MovementSpeed)
→ MC_StartGuard(自作 Multicast)
【他クライアント】MC_StartGuard
→ IsLocallyControlled?
自分 → スキップ(ローカルで再生済み)
他人 → アニメ + 状態更新
既存の SR_BeginBlock は Multicast を内包しているため置き換えが必要です。
SR_BeginBlock を呼ぶと二重再生になります。
パリィも同じ問題がある
設計を詰めていて気づいたのですが、パリィも同じ問題を抱えています。
FCS ではパリィは ParryUp という、BlockUp とは別の変数で管理されています。
パリィ窓(ジャストガードの受付時間)中に攻撃が来たとき、サーバーの ParryUp がまだ届いていなければ:
BlockUpは force-overwrite されるのでガードは成立する- でも
ParryUpはfalseのまま - → ガード成功・パリィ不発
なので bWasParryingOnClient も同様にクライアントから渡す必要があります。
Server_AcceptDamage の冒頭:
Set BlockUp(bWasBlockingOnClient で上書き)
Set ParryUp(bWasParryingOnClient で上書き) ← これも必須
Client_VerifyHit 内でクライアントの BlockUp と ParryUp を両方取得して渡すだけです。
構造はまったく同じなので、実装コストはほぼゼロです。
「でも敵のスキ(よろけ)は即時にならないの?」
パリィが成功すると、敵は一定時間よろけて無防備になります(スキができる)。
この「よろけアニメーション」は即時化されるのか、気になったので整理します。
パリィ成功後のフローはこうなっています。
ParryUp force-overwrite → EventBlock → パリィ成功
→ AttackParried(攻撃者への処理)
→ StaggerCalculation(よろけ時間計算)
→ MC_AttackBlockedAnim(Multicast)← 全クライアントへよろけアニメを配信
最後の Multicast はサーバーから全員に送られるので、よろけアニメーションは Ping 分だけ遅れて見えます。
つまり「パリィが成功と認定される」は即時化されますが、「敵がよろける見た目」は即時化されません。
| 即時化 | 理由 | |
|---|---|---|
| パリィの成功判定 | ✅ | Receiver Authority で force-overwrite 済み |
| 敵のよろけアニメーション | ❌ | Multicast 経由(サーバー → 全クライアント) |
「でもパリィ中に敵がまだ剣を振ってくるんじゃないの?」と気になったので、ここで整理します。
50〜100ms の間、自分のパリィアニメが再生されているのに、敵はまだ攻撃モーションのままに見えます。
よろけアニメの Multicast が届いてはじめて、敵がよろけ始めます。
つまり見た目のズレは確かに起きます。
ただし、ダメージは来ません。
サーバーではパリィ処理が完了した瞬間に AI はスタガー状態になっています。
その後の AI の攻撃はサーバー側でブロックされるため、画面上で剣が振られて見えても当たり判定はありません。
| 状態 | |
|---|---|
| 見た目 | 50〜100ms、敵がまだ剣を振っているように見える |
| ダメージ | 来ない(サーバーでスタガー済み) |
「当たり判定はズレない」さえ保証されていれば、PvE として十分と判断しています。
敵アニメーションの先行予測まで実装すると複雑さが跳ね上がり、今のスコープには合いません。
設計として面白かった点
今回の設計で一番「なるほど」と思ったのは、サーバーに嘘をつかせるという発想でした。
サーバーは「ガードしていない」と思っている。
でもクライアントは「確かにガードしていた」と知っている。
だからクライアントが「自分はガードしていたよ」とサーバーに伝えて、サーバーの認識を書き換える。
PvE ゲームだからこそ成立する方式です。
PvP だとクライアントの情報は信頼できないので、この設計は使えません。
現在の状況
設計は確定しました。実装は進行中です。
完成したら改めてデバッグ方法と一緒に続きを書きます。

