이번에는 물 저장량을 퍼즐 조건으로 사용할 수 있게 만들었다.
이전 글에서 퍼즐 연결 구조를 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은 그 물의 양이 조건을 만족하는지 판단한다.
이렇게 나누니 물 저장 오브젝트를 퍼즐 조건으로 자연스럽게 사용할 수 있었다.
이제 물을 일정량 이상 부어 문을 열거나, 플랫폼을 움직이는 퍼즐을 만들 수 있게 되었다.
'Unity - '한 우산 아래'' 카테고리의 다른 글
| 회전 플랫폼 위에서 플레이어를 자연스럽게 움직이기 (7) | 2026.04.16 |
|---|---|
| 회전 플랫폼 만들기 - AutoRotator와 MovingPlatformSurface (1) | 2026.04.16 |
| 퍼즐 연결 구조 리팩터링하기 (0) | 2026.04.15 |
| 맨손으로 밀고 당길 수 있는 오브젝트 만들기 (0) | 2026.04.15 |
| Unity Profiler로 디버그 UI 프레임 드랍 원인 찾기 (0) | 2026.04.15 |