Files
ProjectEleri/Source/ProjectEleri/Ai/NpcScheduleComponent.h

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;
};