C++
UE4

UE4 でプレイヤー以外の Tick と Physics を止める方法の検討 (第8回ぷちコン)

More than 1 year has passed since last update.

はじめに

第8回ぷちコンにて実装した、 UE4 で自分のプレイヤー以外の時間(Tick,Physics)を止める方法を検討したときの覚書き。

参考までに応募したやつ ↓


http://www.youtube.com/watch?v=TMK2cYWaAyw

検討1: SetGamePaused → 不採用

SetGamePaused.png

これでは自分を含めて全部止まってしまう。
そもそもポーズ画面でこれを使うつもりだったので、不採用。

検討2: CustomTimeDilation + SetSimulatePhysics → 不採用

CustomTimeDilation.png

確かに、これで個別に Tick と物理を止めることは可能。
が、メッシュに対しては、個別に SetSimulatePhysics を呼ぶ必要がある。めんどくさい。
(事前に MeshComponent 検索して設定する機能をつけた BP を継承すれば良い話ではあるが)

あと、時間を止めるトリガーが来た瞬間に、ステージ内の自分以外の全アクターに対してこの処理が走ることになる。
うーん、もっとスマートな方法はないものか・・・。

検討3: UWorld::bPlayersOnly, UWorld::bShouldSimulatePhysics → 今回採用

実は、UWorld 内のプロパティをいじると、特定のアクター以外の時間を止めることができる

UWorld::bPlayersOnly

これを true にすると、ローカルでコントローラーに Possess されている APawn(もしくはその継承クラス, ACharactorとか) のみ Tick を走らせる ようになる。
もっと厳密にいうと、ワールドの TickType が強制的に ELevelTick::LEVELTICK_ViewportsOnly になる。
Actor が Tick による更新指示を受ける部分のコードを読めばわかるのだが、

Actor.cpp
void AActor::TickActor( float DeltaSeconds, ELevelTick TickType, FActorTickFunction& ThisTickFunction )
{
    //root of tick hierarchy

    // Non-player update.
    const bool bShouldTick = ((TickType!=LEVELTICK_ViewportsOnly) || ShouldTickIfViewportsOnly());
    if(bShouldTick)
    {
        // If an Actor has been Destroyed or its level has been unloaded don't execute any queued ticks
        if (!IsPendingKill() && GetWorld())
        {
            Tick(DeltaSeconds); // perform any tick functions unique to an actor subclass
        }
    }
}

// ~省略~

/** If true, actor is ticked even if TickType==LEVELTICK_ViewportsOnly */
bool AActor::ShouldTickIfViewportsOnly() const
{
    return false;
}

となっている。
つまり、アクターは基本的に ELevelTick::LEVELTICK_ViewportsOnly 時は Tick が走らない
しかし、AActor の継承クラスの一部において ShouldTickIfViewportsOnly() を override しているものがある。

  • ACameraRig_Crane
  • ACameraRig_Rail
  • ACineCameraActor
    • これらの3つはシーケンサ用で必ず true を返すようになっている。
  • APlayerCameraManager
    • APlayerController の参照を持っているときだけ true を返す。
    • APlayerController がスポーンされるときには必ず渡されるものなので、これも基本的には true を返すと見てよい
  • APawn
    • 「ローカルでコントロールされている」かつ 「APlayerController がセットされている」という条件で true を返す

ということで、自分で継承クラスを作って ShouldTickIfViewportsOnly() を override しない限りは、
カメラ関係以外ではローカルでコントローラーに Possess されている APawn かその継承クラスのみ Tick が走ることになる。

なので、これを true にすれば、プレイヤー以外の Tick を止めることができる。

UWorld::bShouldSimulatePhysics

これを false にすると、ワールドのすべての Physics による更新をしなくなる。
ただし、これはあくまで Physics の更新を止めるだけであり、オブジェクト同士の作用(ぶつかったときの力など)はそのまま受け付けている状態になる。
そのため、解除した瞬間に、蓄積された作用が一気に適用される挙動となる。

この挙動を起きないようにするためには、やはり SetSimulatePhyshics を false にしないといけない。
が、今回作ったゲームでは、むしろその挙動の方が面白かったので、あえてそのままにしてある。

実装

UWorld も上記のプロパティもブループリント公開はされてないので C++ で組む。
FunctionLibrary で共用の関数として、以下のようにして実装した。

CommonBlueprintFunctionLibrary.h
#include "Kismet/BlueprintFunctionLibrary.h"
#include "CommonBlueprintFunctionLibrary.generated.h"

UCLASS()
class ONEMINUTE_API UCommonBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:

    UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject") , Category="TimeStop")
    static void StartTimeStop(UObject* WorldContextObject);

    UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject") , Category="TimeStop")
    static void FinishTimeStop(UObject* WorldContextObject);
};
CommonBlueprintFunctionLibrary.cpp
void UCommonBlueprintFunctionLibrary::StartTimeStop(UObject* WorldContextObject)
{
    UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject);
    if(World) {
        World->bPlayersOnly = true;
        World->bShouldSimulatePhysics = false;
    }
}

void UCommonBlueprintFunctionLibrary::FinishTimeStop(UObject* WorldContextObject)
{
    UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject);
    if(World) {
        World->bPlayersOnly = false;
        World->bShouldSimulatePhysics = true;
    }
}

あとは、ブループリントから必要なタイミングで呼び出せばよい。
やってみたときの実験動画 → https://ue4-mstdn.tokyo/@negimochi/1098