โœ๏ธToday I Learned

๐Ÿ“… 2025-12-18

  • ํƒ€์ด๋จธ๋กœ ์ฃผ๊ธฐ์ ์ธ ์—ฐ๋ฃŒ ์†Œ๋ชจ ์‹œ์Šคํ…œ ๊ตฌํ˜„
  • ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋กœ UI์™€ ๋กœ์ง ๋ถ„๋ฆฌ
  • ํŠน์ • ์•„์ดํ…œ๋งŒ ๋ฐ›๋Š” ์ปค์Šคํ…€ ์ธ๋ฒคํ† ๋ฆฌ ์ปดํฌ๋„ŒํŠธ
  • StartTime ์ €์žฅ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” ๊ตฌํ˜„

ํƒ€์ด๋จธ๋กœ ์ฃผ๊ธฐ์ ์ธ ์—ฐ๋ฃŒ ์†Œ๋ชจ ์‹œ์Šคํ…œ ๊ตฌํ˜„

์—ฐ๋ฃŒ๋ฅผ ๋„ฃ์œผ๋ฉด ๋ถˆ์„ ์ผค ์ˆ˜ ์žˆ๋Š” ๊ฐ€๋กœ๋“ฑ ์•กํ„ฐ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. ํ•ต์‹ฌ์€ ์„œ๋ฒ„์—์„œ ํƒ€์ด๋จธ๋กœ ์—ฐ๋ฃŒ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์†Œ๋ชจํ•˜๊ณ , ์—ฐ๋ฃŒ๊ฐ€ ์—†์œผ๋ฉด ์ž๋™์œผ๋กœ ๋ถˆ์„ ๋„๋Š” ๊ตฌ์กฐ๋‹ค.

๊ตฌํ˜„ ๋ฐฉ์‹

void ATSStreetLamp::SetFuelTimer()
{
    if (bIsFueling) return;
    
    GetWorldTimerManager().SetTimer(
        FuelTimerHandle, 
        this, 
        &ATSStreetLamp::UseFuel, 
        MaintenanceInterval,  // ์ดˆ๋‹น ์†Œ๋ชจ ๊ฐ„๊ฒฉ
        true,   // ๋ฐ˜๋ณต
        0.f     // ์ฆ‰์‹œ ์‹œ์ž‘
    );
    bIsFueling = true;
}

void ATSStreetLamp::UseFuel()
{
    if (LampInventory->GetItemCount(MaintenanceCostID) > 0)
    {
        // ์—ฐ๋ฃŒ ์†Œ๋ชจ & ๋ถˆ ์ผœ๊ธฐ
        LampInventory->ConsumeItem(MaintenanceCostID, 1);
        ChangeLightScaleByErosion(ErosionSubSystem->GetCurrentErosion());
        StartTime = GetWorld()->GetTimeSeconds();  // ๐Ÿ”‘ ํ˜„์žฌ ์‹œ๊ฐ„ ์ €์žฅ
    }
    else
    {
        // ์—ฐ๋ฃŒ ์—†์œผ๋ฉด ๋ถˆ ๋„๊ธฐ & ํƒ€์ด๋จธ ์ค‘์ง€
        SetLightScale(0);
        GetWorldTimerManager().ClearTimer(FuelTimerHandle);
        bIsFueling = false;
    }
    OnFuelModeChanged.Broadcast(bIsFueling);
}

ํ•ต์‹ฌ ํฌ์ธํŠธ

  • MaintenanceInterval๋งˆ๋‹ค UseFuel() ํ˜ธ์ถœ๋กœ ์—ฐ๋ฃŒ 1๊ฐœ์”ฉ ์†Œ๋ชจ
  • ์—ฐ๋ฃŒ๊ฐ€ ์žˆ์œผ๋ฉด ๊ณ„์† ํƒ€์ด๋จธ ๋ฐ˜๋ณต, ์—†์œผ๋ฉด ClearTimer()๋กœ ์ค‘์ง€
  • StartTime์„ Replicated๋กœ ์ €์žฅํ•ด์„œ ํด๋ผ์ด์–ธํŠธ๋„ ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ๊ณ„์‚ฐ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ
  • bIsFueling ์ƒํƒœ๋กœ ํ˜„์žฌ ๊ฐ€๋™ ์ค‘์ธ์ง€ ์ถ”์ 

๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋กœ UI์™€ ๋กœ์ง ๋ถ„๋ฆฌ

UI ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•ด FOnFuelModeChanged ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. ์—ฐ๋ฃŒ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธํ•ด์„œ ์œ„์ ฏ์—์„œ ๊ตฌ๋…ํ•˜๋„๋ก ํ–ˆ๋‹ค.

๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ์„ ์–ธ & ์‚ฌ์šฉ

// .h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFuelModeChanged, bool, bIsFueling);

UPROPERTY(BlueprintAssignable, Category = "Lamp|Events")
FOnFuelModeChanged OnFuelModeChanged;

// .cpp
UPROPERTY(ReplicatedUsing = OnRep_IsFueling)
bool bIsFueling = false;

void ATSStreetLamp::OnRep_IsFueling()
{
    OnFuelModeChanged.Broadcast(bIsFueling);
}

์œ„์ ฏ ์ธก ๊ตฌํ˜„

  • ์œ„์ ฏ์—์„œ OnFuelModeChanged๋ฅผ ๊ตฌ๋…
  • bIsFueling์ด false๋ฉด ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” ์—…๋ฐ์ดํŠธ ํƒ€์ด๋จธ pause
  • true๋ฉด ํƒ€์ด๋จธ ์žฌ๊ฐœํ•ด์„œ 0.2์ดˆ๋งˆ๋‹ค ๋‚จ์€ ์‹œ๊ฐ„ ๊ณ„์‚ฐ

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์•กํ„ฐ ๋กœ์ง๊ณผ UI ์—…๋ฐ์ดํŠธ๊ฐ€ ์™„์ „ํžˆ ๋ถ„๋ฆฌ๋˜๊ณ , ์œ„์ ฏ์€ ์ƒํƒœ ๋ณ€ํ™”์—๋งŒ ๋ฐ˜์‘ํ•˜๋ฉด ๋œ๋‹ค.


ํŠน์ • ์•„์ดํ…œ๋งŒ ๋ฐ›๋Š” ์ปค์Šคํ…€ ์ธ๋ฒคํ† ๋ฆฌ ์ปดํฌ๋„ŒํŠธ

๋žจํ”„๋Š” ์—ฐ๋ฃŒ ์•„์ดํ…œ๋งŒ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. UTSLampInventory๋ฅผ ๋งŒ๋“ค์–ด์„œ CanPlaceItemInSlot()์„ ์˜ค๋ฒ„๋ผ์ด๋“œํ–ˆ๋‹ค.

bool UTSLampInventory::CanPlaceItemInSlot(int32 StaticDataID, EInventoryType InventoryType, int32 SlotIndex)
{
    if (Super::CanPlaceItemInSlot(StaticDataID, InventoryType, SlotIndex))
    {
        if (StaticDataID != MaintenanceCostID)  // ์—ฐ๋ฃŒ ์•„์ดํ…œ๋งŒ ํ—ˆ์šฉ
        {
            return false;
        }
    }
    OnFuelTransferred.Broadcast();  // ์—ฐ๋ฃŒ ์ถ”๊ฐ€๋จ ์•Œ๋ฆผ
    return true;
}

void UTSLampInventory::ClearSlot(FSlotStructMaster& Slot)
{
    Super::ClearSlot(Slot);
    Slot.ItemData.StaticDataID = MaintenanceCostID;  // ๋นˆ ์Šฌ๋กฏ๋„ ์—ฐ๋ฃŒ ID๋กœ
    Slot.MaxStackSize = MaintenanceCostQty;
}

์ถ”๊ฐ€ ๊ตฌํ˜„

  • Internal_TransferItem()์—์„œ๋„ MaintenanceCostID ์ฒดํฌ
  • ์—ฐ๋ฃŒ๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด OnFuelTransferred ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
  • ๋žจํ”„ ์•กํ„ฐ์—์„œ ์ด๊ฑธ ๊ตฌ๋…ํ•ด์„œ SetFuelTimer() ํ˜ธ์ถœ
// BeginPlay์—์„œ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฐ”์ธ๋”ฉ
LampInventory->OnFuelTransferred.AddDynamic(this, &ATSStreetLamp::SetFuelTimer);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์—ฐ๋ฃŒ๋ฅผ ๋„ฃ๋Š” ์ˆœ๊ฐ„ ์ž๋™์œผ๋กœ ํƒ€์ด๋จธ๊ฐ€ ์‹œ์ž‘๋œ๋‹ค.


StartTime ์ €์žฅ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” ๊ตฌํ˜„

์„œ๋ฒ„์—์„œ๋งŒ ํƒ€์ด๋จธ๊ฐ€ ๋Œ์ง€๋งŒ, ํด๋ผ์ด์–ธํŠธ๋„ ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ”๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด StartTime์„ Replicated ๋ณ€์ˆ˜๋กœ ๋งŒ๋“ค์—ˆ๋‹ค.

UPROPERTY(Replicated)
float StartTime = 0.f;

// UseFuel()์—์„œ ์—ฐ๋ฃŒ ์†Œ๋ชจํ•  ๋•Œ๋งˆ๋‹ค ๊ฐฑ์‹ 
StartTime = GetWorld()->GetTimeSeconds();

์œ„์ ฏ์—์„œ ๊ณ„์‚ฐ

// ์œ„์ ฏ์˜ 0.2์ดˆ ํƒ€์ด๋จธ์—์„œ
float OperatingTime = CurrentTime - StartTime;
float Progress = 1.0f - (OperatingTime / MaintenanceInterval);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด:
1. ์„œ๋ฒ„๊ฐ€ ์—ฐ๋ฃŒ ์†Œ๋ชจํ•  ๋•Œ๋งˆ๋‹ค StartTime ์—…๋ฐ์ดํŠธ
2. ํด๋ผ์ด์–ธํŠธ๋Š” StartTime๊ณผ ํ˜„์žฌ ์‹œ๊ฐ„ ์ฐจ์ด๋กœ ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ๊ณ„์‚ฐ
3. MaintenanceInterval๋กœ ๋‚˜๋ˆ ์„œ ์ง„ํ–‰๋ฅ  ๊ณ„์‚ฐ

์„œ๋ฒ„ ํƒ€์ด๋จธ ์—†์ด๋„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” ์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅ!


๐Ÿ’ก ๋А๋‚€ ์  (What I Felt)

Tick์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋ถ€ํ•˜๋ฅผ ์ค„์ด๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ๋‹ค. ์—ฐ๋ฃŒ๋ฅผ ์†Œ๋น„ํ•˜๊ฑฐ๋‚˜ ๋ถˆ์„ ๋„๋Š” ์‹œ์ ๋งŒ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ๋กœ ํ™•์‹คํ•˜๊ฒŒ ๋™๊ธฐํ™”ํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ์˜ˆ์ธกํ•˜์—ฌ ๋‚จ์€ ์—ฐ๋ฃŒ ๊ฐ€๋™์‹œ๊ฐ„์„ UI์— ๋‚˜ํƒ€๋ƒˆ๋‹ค. ์ด๋•Œ๋„ Tick๋ณด๋‹ค๋Š” ํƒ€์ด๋จธ๋ฅผ ์‚ฌ์šฉํ•ด ๋žจํ”„๊ฐ€ ๊ฐ€๋™๋˜๋Š” ์ˆœ๊ฐ„์—๋งŒ ํƒ€์ด๋จธ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ถ€ํ•˜๋ฅผ ์ค„์˜€๋‹ค.

๊ธฐ์กด์˜ ์ธ๋ฒคํ† ๋ฆฌ๋ฅผ ์ปค์Šคํ…€ํ•˜์—ฌ ํŠน์ • ์•„์ดํ…œ๋งŒ ๋„ฃ์„ ์ˆ˜ ์žˆ๊ณ  ์ตœ๋Œ€๋กœ ๋„ฃ์„ ์ˆ˜ ์žˆ๋Š” ์—ฐ๋ฃŒ ์•„์ดํ…œ์˜ ์ˆ˜๋„ ์ œํ•œํ•˜๋„๋ก CanPlaceItemInSlot()๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ–ˆ๋‹ค. ๊ธฐ์กด์—๋Š” ์•„์ดํ…œ ๋ฐ์ดํ„ฐ์—์„œ MaxStack์„ ๊ฐ€์ ธ์™€ ์Šฌ๋กฏ์— ์ ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ๋Š”๋ฐ ์ด๋ฒˆ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์Šคํƒํ•  ๋•Œ ์Šฌ๋กฏ์˜ MaxStack์„ ๊ฐ€์ง€๊ณ  ์˜ค๋Š” ๋ฐฉ์‹์œผ๋กœ ์ˆ˜์ •ํ–ˆ๋‹ค. ๊ธฐ์กด ์ธ๋ฒคํ† ๋ฆฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฝค ํ™•์žฅ์„ฑ ๋†’๊ฒŒ ๋งŒ๋“  ๊ฒƒ ๊ฐ™์•„ ๋ฟŒ๋“ฏํ•˜๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€