はじめに
第8回ぷちコンにて実装した、 UE4 で自分のプレイヤー以外の時間(Tick,Physics)を止める方法を検討したときの覚書き。
参考までに応募したやつ ↓
http://www.youtube.com/watch?v=TMK2cYWaAyw
検討1: SetGamePaused → 不採用
これでは自分を含めて全部止まってしまう。
そもそもポーズ画面でこれを使うつもりだったので、不採用。
検討2: CustomTimeDilation + SetSimulatePhysics → 不採用
確かに、これで個別に Tick と物理を止めることは可能。
が、メッシュに対しては、個別に SetSimulatePhysics
を呼ぶ必要がある。めんどくさい。
(事前に MeshComponent
検索して設定する機能をつけた BP を継承すれば良い話ではあるが)
あと、時間を止めるトリガーが来た瞬間に、ステージ内の自分以外の全アクターに対してこの処理が走ることになる。
うーん、もっとスマートな方法はないものか・・・。
検討3: UWorld::bPlayersOnly, UWorld::bShouldSimulatePhysics → 今回採用
実は、UWorld 内のプロパティをいじると、特定のアクター以外の時間を止めることができる。
UWorld::bPlayersOnly
これを true にすると、ローカルでコントローラーに Possess されている APawn(もしくはその継承クラス, ACharactorとか) のみ Tick を走らせる ようになる。
もっと厳密にいうと、ワールドの TickType
が強制的に ELevelTick::LEVELTICK_ViewportsOnly
になる。
Actor が Tick による更新指示を受ける部分のコードを読めばわかるのだが、
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 で共用の関数として、以下のようにして実装した。
#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);
};
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