Unity - '한 우산 아래'

물 저장량으로 활성화되는 오브젝트 만들기

망고와플 2026. 4. 15. 16:57

이번에는 물 저장량을 퍼즐 조건으로 사용할 수 있게 만들었다.

이전 글에서 퍼즐 연결 구조를 PuzzleConditionSource → PuzzleConditionGroup → Result 형태로 정리했다.

이전글 : https://mangowaffle.tistory.com/39

덕분에 이제 버튼이 아니더라도 조건만 만족할 수 있다면 같은 구조에 연결할 수 있다.

이번에 만든 것은 물 저장량 조건이다.

우산으로 물을 붓는다
→ 오브젝트에 물이 저장된다
→ 일정량 이상 차면 조건이 만족된다
→ PuzzleConditionGroup이 결과 이벤트를 실행한다

즉, 물을 일정량 이상 부으면 열리는 문이나 움직이는 플랫폼을 만들 수 있게 된 것이다.

구조

이번 구조는 이렇게 나눴다.

UmbrellaWaterTarget
- 우산에서 부은 물을 받는다.
- 현재 물 저장량을 관리한다.
- 물 무게를 Rigidbody 질량에 반영한다.

WaterAmountCondition
- UmbrellaWaterTarget의 저장량을 읽는다.
- 기준 물 양 이상이면 조건을 만족시킨다.

PuzzleConditionGroup
- WaterAmountCondition을 조건으로 받는다.
- 조건이 만족되면 결과 이벤트를 실행한다.

중요한 점은 UmbrellaWaterTarget이 직접 퍼즐 조건이 되지 않는다는 것이다.

물 받는 역할과 조건 판정 역할을 나누기 위해 WaterAmountCondition을 따로 만들었다.

앞 글에서 말한 책임 분리의 연장선이다.

UmbrellaWaterTarget

UmbrellaWaterTarget은 물을 받는 대상이다.

우산에서 물을 부으면 ReceiveWater()가 호출되고, 저장량이 증가한다.

public void ReceiveWater(float amount)
{
    if (isActivated || amount <= 0.0f)
    {
        return;
    }

    // 물은 requiredWater를 넘지 않게 저장한다.
    receivedWater = Mathf.Clamp(receivedWater + amount, 0.0f, requiredWater);

    if (receivedWater >= requiredWater)
    {
        isActivated = true;
    }

    RefreshWeight();
    RefreshVisual();

    // 물 저장량이 바뀌었음을 외부 조건 컴포넌트에 알린다.
    NotifyWaterChanged();
}

여기서 물 저장량이 바뀌면 NotifyWaterChanged()를 호출한다.

public event Action WaterChanged;

private void NotifyWaterChanged()
{
    WaterChanged?.Invoke();
}

이 이벤트 덕분에 다른 컴포넌트가 매 프레임 물의 양을 확인할 필요가 없다.

물의 양이 바뀐 순간에만 반응하면 된다.

물 무게 반영하기

이 오브젝트는 물을 담을수록 무거워질 수도 있다.

public float AddedWeight => receivedWater * waterWeightMultiplier;

그리고 Rigidbody가 연결되어 있다면, 기준 질량에 물 무게를 더한다.

private void RefreshWeight()
{
    if (!addWaterToRigidbodyMass || weightedRigidbody == null)
    {
        return;
    }

    weightedRigidbody.mass = baseMass + AddedWeight;
}

이렇게 하면 물을 많이 담은 오브젝트는 더 무거워진다.

나중에 무게 발판 위에 올렸을 때도 물의 양이 무게에 반영된다.

WaterAmountCondition

이제 물 저장량을 퍼즐 조건으로 바꿔주는 컴포넌트가 필요하다.

그 역할을 WaterAmountCondition이 맡는다.

public class WaterAmountCondition : PuzzleConditionSource
{
    [SerializeField] private UmbrellaWaterTarget waterTarget;
    [SerializeField] private bool useTargetRequiredWater = true;
    [SerializeField] private float requiredWater = 1.0f;

    public override bool IsSatisfied => IsWaterRequirementMet();

    private bool IsWaterRequirementMet()
    {
        return waterTarget != null && waterTarget.ReceivedWater >= RequiredWater;
    }
}

조건 자체는 단순하다.

waterTarget의 ReceivedWater가 RequiredWater 이상이면 true
아니면 false

useTargetRequiredWater가 켜져 있으면 UmbrellaWaterTarget의 requiredWater 값을 그대로 사용한다.

꺼져 있으면 WaterAmountCondition에서 별도의 기준값을 정할 수 있다.

이벤트로 조건 갱신하기

WaterAmountCondition은 UmbrellaWaterTarget.WaterChanged 이벤트를 구독한다.

private void OnEnable()
{
    CacheWaterTarget();
    SubscribeWaterTarget();
    RefreshSatisfiedState(false);
}

private void SubscribeWaterTarget()
{
    if (waterTarget == null)
    {
        return;
    }

    // 중복 구독을 막기 위해 먼저 제거한 뒤 다시 등록한다.
    waterTarget.WaterChanged -= OnWaterChanged;
    waterTarget.WaterChanged += OnWaterChanged;
 
}
   
여기서 `-=` 후 `+=`를 하는 이유는 중복 구독을 막기 위해서다.
`+=`는 이벤트에 함수를 등록한다는 뜻이다.  
그런데 같은 함수를 여러 번 등록하면 이벤트가 발생했을 때 그 함수도 여러 번 호출된다.
그래서 먼저 `-=`로 혹시 이미 등록되어 있는 핸들러를 제거하고,  
그 다음 `+=`로 다시 등록한다.
이렇게 하면 `OnWaterChanged`는 항상 한 번만 등록된다.

private void OnWaterChanged()
{
    RefreshSatisfiedState(true);
}

물이 들어와 저장량이 바뀌면 OnWaterChanged()가 호출된다.

그리고 조건 만족 여부를 다시 계산한다.

private void RefreshSatisfiedState(bool notifyChanged)
{
    bool nextSatisfied = IsWaterRequirementMet();

    if (satisfied == nextSatisfied)
    {
        return;
    }

    satisfied = nextSatisfied;

    if (notifyChanged)
    {
        NotifyChanged();
    }
}

여기서도 중요한 점은 상태가 바뀌었을 때만 NotifyChanged()를 호출한다는 것이다.

물은 조금씩 계속 들어올 수 있기 때문에, 매번 같은 상태를 반복 통지하지 않도록 했다.

PuzzleConditionGroup에 연결하기

이제 WaterAmountCondition을 PuzzleConditionGroup의 조건으로 넣으면 된다.

Condition Sources
- WaterAmountCondition

그리고 On Satisfied에 문이나 플랫폼을 연결한다.

On Satisfied
→ PuzzleMover.Activate()

이제 우산으로 물을 부어서 기준량을 넘기면 조건이 만족되고, 연결된 결과가 실행된다.

정리

이번 작업은 이전에 만든 퍼즐 조건 구조를 물 저장량에 적용한 것이다.

UmbrellaWaterTarget
→ WaterAmountCondition
→ PuzzleConditionGroup
→ PuzzleMover

UmbrellaWaterTarget은 물을 받는 역할만 맡고,

WaterAmountCondition은 그 물의 양이 조건을 만족하는지 판단한다.

이렇게 나누니 물 저장 오브젝트를 퍼즐 조건으로 자연스럽게 사용할 수 있었다.

이제 물을 일정량 이상 부어 문을 열거나, 플랫폼을 움직이는 퍼즐을 만들 수 있게 되었다.