127 lines
5.5 KiB
C++
127 lines
5.5 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "Components/ActorComponent.h"
|
|
#include "GameplayTagContainer.h"
|
|
#include "ProjectEleri/Ai/NpcScheduleAsset.h"
|
|
#include "ProjectEleri/Public/Interface/SaveableObjectInterface.h"
|
|
#include "NpcScheduleComponent.generated.h"
|
|
|
|
class ATimeOfDayManager;
|
|
class UStateTreeComponent;
|
|
class UNpcStateMachineComponent;
|
|
class UScheduleSubsystem;
|
|
|
|
/**
|
|
* Drives one NPC from its schedule, and keeps that NPC frozen while the player is nowhere near it.
|
|
*
|
|
* This ticks at a low fixed rate instead of every frame. A schedule is a pure function of the NPC's
|
|
* tag and the minute of the day, so an update is a map lookup and a scan of a handful of time
|
|
* windows, and nothing about it needs per frame resolution.
|
|
*
|
|
* The component keeps ticking while the NPC it belongs to is frozen, because noticing the player
|
|
* coming back is the one thing that cannot be switched off.
|
|
*
|
|
* It is not itself the NPC's behaviour. What an assignment means is the state machine component's
|
|
* business: this works out what the NPC should be doing and hands it over, and freezing is part of
|
|
* that handover, because whether the NPC was watched while it was away is what decides whether it may
|
|
* be put back in place outright or has to be seen walking there.
|
|
*/
|
|
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent), BlueprintType, Blueprintable)
|
|
class PROJECTELERI_API UNpcScheduleComponent : public UActorComponent
|
|
{
|
|
GENERATED_BODY()
|
|
|
|
public:
|
|
|
|
UNpcScheduleComponent();
|
|
|
|
virtual void BeginPlay() override;
|
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
|
|
|
/** Which schedule this NPC follows, looked up in UEleriGameSettings::NpcSchedules. An empty tag
|
|
* means the NPC is not scheduled and the component stays out of the way. */
|
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Schedule", meta = (Categories = "NpcTags"))
|
|
FGameplayTag NpcTag;
|
|
|
|
/** Past this distance from the player the NPC is frozen in place, which switches off its tick, its
|
|
* behaviour, its movement and its collision. The NPC itself stays resident because it is not
|
|
* spatially loaded, so it is still there holding its position when the player comes back. */
|
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Schedule", meta = (ClampMin = "0.0", UIMin = "0.0"))
|
|
float FreezeDistance = 6000.0f;
|
|
|
|
/** What this NPC was last told to do */
|
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Schedule")
|
|
FResolvedNpcAction CurrentAction;
|
|
|
|
/** True once the block above has run at least once */
|
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Schedule")
|
|
bool bHasAction = false;
|
|
|
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Schedule")
|
|
bool IsFrozen() const { return bFrozen; }
|
|
|
|
/** True once the clock has given this NPC an assignment. Until it has there is nothing to walk to
|
|
* and nothing to do. */
|
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Schedule")
|
|
bool HasAction() const { return bHasAction; }
|
|
|
|
/** What the clock last decided this NPC should be doing. Meaningless before HasAction. Not a
|
|
* blueprint function because CurrentAction above is already readable from a blueprint. */
|
|
const FResolvedNpcAction& GetCurrentAction() const { return CurrentAction; }
|
|
|
|
/** Re-reads the clock. Returns true when that produced an assignment different from the one this
|
|
* NPC was already on, which is the question the state machine asks on unfreeze: an NPC whose day
|
|
* has moved on without it while it was out of sight is one that has to be seen walking, and one
|
|
* whose assignment is unchanged can simply be put where it should already have been. */
|
|
UFUNCTION(BlueprintCallable, Category = "Schedule")
|
|
bool RefreshSchedule();
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Schedule")
|
|
void SetFrozen(bool bNewFrozen);
|
|
|
|
/** Everything about this NPC that a load has to put back: where it stands, and the assignment it
|
|
* was on, so the first refresh after the load recognises the schedule's answer as the one it
|
|
* already had instead of handing the NPC a fresh one. */
|
|
FNpcScheduleSaveData PrepareSaveData() const;
|
|
|
|
/** Puts an NPC back on the spot and the assignment it was saved with. Records for a different tag
|
|
* are ignored, and an NPC that had no assignment yet is left to the schedule. */
|
|
void ApplySaveData(const FNpcScheduleSaveData& Data);
|
|
|
|
protected:
|
|
|
|
/** The manager, cached because GetActorOfClass walks every actor in the level and this is asked for
|
|
* twice a second by every NPC. */
|
|
ATimeOfDayManager* FindTimeOfDayManager();
|
|
|
|
/** The pawn distances are measured from, cached for the same reason as the manager. */
|
|
AActor* FindViewTargetActor();
|
|
|
|
bool ShouldBeFrozen();
|
|
|
|
/** Carries out whatever bFrozen currently asks for. Kept separate from SetFrozen so that the first
|
|
* application always happens, which is what starts the behaviour of an in range NPC. */
|
|
void ApplyFreezeState();
|
|
|
|
UStateTreeComponent* GetStateTreeComponent();
|
|
|
|
/** The behaviour driving this NPC, null when it has none. Found once and kept, because this is
|
|
* asked for on every refresh and every freeze. */
|
|
UNpcStateMachineComponent* GetStateMachineComponent();
|
|
|
|
UScheduleSubsystem* GetScheduleSubsystem();
|
|
|
|
private:
|
|
|
|
TWeakObjectPtr<ATimeOfDayManager> CachedTimeOfDayManager;
|
|
TWeakObjectPtr<AActor> CachedViewTarget;
|
|
TWeakObjectPtr<UNpcStateMachineComponent> CachedStateMachine;
|
|
|
|
/** False until the freeze state has been applied once */
|
|
bool bFreezeStateApplied = false;
|
|
|
|
bool bFrozen = false;
|
|
}; |