openlang

체력바 하나를 빨갛게 했더니 전부 빨개졌다

이번엔 Godot UI에서 공유 리소스에 당했어요. 작은 프로토타입의 유닛마다 체력바 씬이 붙어 있는데, 채움 부분에 StyleBoxFlat 오버라이드를 건 ProgressBar예요. 체력이 25 아래로 떨어지면 그 바 하나만 빨갛게 하고 싶어서 이렇게 썼어요.

func _on_health_changed(value: float) -> void:
	$Bar.value = value
	var fill := $Bar.get_theme_stylebox("fill") as StyleBoxFlat
	fill.bg_color = Color.RED if value < 25.0 else Color.GREEN

첫 유닛이 한 대 맞자마자 화면의 모든 바가 빨개졌어요. 오버라이드가 바 씬 안에 저장된 서브 리소스였고, 씬의 모든 인스턴스가 똑같은 StyleBoxFlat 객체를 받은 거예요. 거기에 속성 하나를 바꾸면 전부 바뀌어요. 보고 나면 당연하죠, 객체가 하나니까요.

헷갈리는 건 고치는 방법이에요. _ready에서 duplicate()를 부를지, resource_local_to_scene을 켤지, 아니면 아예 스타일박스에 쓰는 걸 그만둘지요.

이어지는 대화 10

서로의 근거를 따라
  1. 고유 링크

    그중 두 방법은 값을 바꾸는 동작을 그대로 두고 바마다 바꿀 객체를 따로 줍니다. 세 번째는 동작 자체를 바꿉니다. 채움의 모습이 두 가지뿐이라면, 절대 바뀌지 않는 스타일박스 리소스 두 개를 add_theme_stylebox_override로 갈아 끼우면 됩니다. 그러면 어떤 바도 바뀔 수 있는 것을 소유하지 않고, 공유는 더 이상 문제가 되지 않습니다.

    바마다 복제하는 방식은 색이 연속적일 때, 즉 초록에서 빨강으로 섞여 갈 때 더 맞습니다. 그 경우 프리셋은 끝없이 늘어납니다. 그래서 묻습니다. 바가 보여 주는 것이 임계값입니까, 그라데이션입니까?

  2. 고유 링크

    코드가 제일 적게 드는 건 체크박스 하나예요. 인스펙터에서 StyleBoxFlat을 고르고 Resource 항목을 펼쳐서 Local To Scene을 켜면 돼요. 그러면 바 씬의 인스턴스마다 인스턴스화될 때 자기 사본을 받고, 핸들러는 지금 그대로 둬도 돼요. 저라면 거기서 시작하고, 그걸로 부족할 때만 뭔가를 더 쓰겠어요.

  3. 고유 링크

    get_theme_stylebox("fill")를 눈여겨보세요. 언젠가 오버라이드를 지우면 이 줄이 테마의 스타일박스를 돌려주고, 핸들러가 그 테마를 쓰는 모든 ProgressBar의 색을 바꿔요. 쓰기 전에 duplicate()를 하면 두 경우 다 막히고, StyleBoxFlat은 얕은 복사로 충분해요.

  4. 고유 링크

    켜기 전에 스타일박스가 실제로 어디 있는지 먼저 확인해 보면 좋아요. Local To Scene은 씬이 쓰는 리소스에 작용하거든요. 바 씬에 내장된 서브 리소스인가요, 아니면 오버라이드가 가리키는 .tres 파일인가요? 제가 알기로는 둘 다 인스턴스마다 복제될 거예요. 다만 부모 노드의 Theme에서 오는 스타일박스라면 다른 객체라서, 바 자체 오버라이드에 켠 체크박스는 거기까지 닿지 않아요. 그걸 확인하고 나면 체크박스는 안전한 첫걸음이에요.

  5. 고유 링크

    내장 서브 리소스예요. health_bar.tscn 안에 바로 저장돼 있고 위쪽에 Theme은 하나도 없어요. Local To Scene을 켰더니 돼요. 맞은 유닛의 바만 빨개지고 나머지는 초록으로 남아요. 기본값이 아니라 체크박스라는 게 살짝 짜증 나긴 하는데 이해는 해요. 스타일박스는 대부분 공유하라고 있는 거고, 전부 복사하면 그게 더 이상하겠죠.

  6. 고유 링크

    이제 스타일박스가 몇 개나 있는지 궁금해져요. _ready에서 바 두 개의 $Bar.get_theme_stylebox("fill").get_instance_id()를 찍어 보세요. 우리가 생각하는 곳에서 복사가 일어난다면 체크박스 전에는 같은 숫자 하나, 켠 뒤에는 서로 다른 두 숫자가 나와야 해요. 그다음엔 에디터에 놓은 바 말고 실행 중에 instantiate()로 추가한 바로도 해 보세요. 그것도 자기 사본을 받는지 보고 싶어서요.

  7. 고유 링크

    스타일박스를 왜 건드려요? self_modulate는 노드마다 따로예요. 그 바 하나에만 빨강을 주면 공유되는 게 없어요. 바가 이백 개면 노드 이백 개에 색 값 이백 개, 추가 리소스는 영 개예요.

  8. 고유 링크

    그거 제일 먼저 해 봤고, 그래서 스타일박스까지 온 거예요. self_modulate는 바가 그리는 모든 걸 곱해서, 회색 배경도 채움과 같이 검붉게 변해요. 그러면 체력이 얼마나 남았는지 읽을 수가 없어요. 맞았을 때 잠깐 번쩍이는 데는 괜찮은데, 계속 유지되는 상태에는 안 맞아요.

  9. 고유 링크

    저도 프리셋 쪽에 한 표를 던집니다만, 객체 수와는 다른 이유입니다. 지켜야 할 규칙은 각 바의 색이 자기 유닛의 체력에만 달려 있고 다른 것에는 달려 있지 않다는 것입니다. 공유되는 가변 스타일박스는 한 유닛의 체력이 다른 바의 색을 정하게 만들어 이 규칙을 깹니다. 복제는 모든 사본이 실제로 만들어진다는 전제에서 규칙을 되살립니다. 프리셋 두 개는 어떤 사본에도 기대지 않고 규칙을 되살립니다. 핸들러가 현재 값으로 하나를 고르고, 프리셋 자체는 결코 바뀌지 않기 때문입니다. 그래도 체력이 딱 25일 때는 따로 시험해 보겠습니다. <와 <=가 거기서 갈리기 때문입니다.

  10. 고유 링크

    아이디를 찍어 봤어요. 체크박스 전에는 바 여섯 개가 전부 같은 아이디였고, 켠 뒤에는 여섯 개가 다 달랐어요. instantiate()로 추가한 바도 자기 걸 받았고요. 그러니까 설명대로 정확히 됐어요.

    그런데 결국 프리셋 스타일박스 두 개로 바꿨어요. 거의 tally6 말 때문이에요. 핸들러는 이만큼 줄었어요.

    const FILL_OK := preload("res://ui/fill_ok.tres")
    const FILL_LOW := preload("res://ui/fill_low.tres")
    
    func _on_health_changed(value: float) -> void:
    	$Bar.value = value
    	var fill: StyleBox = FILL_LOW if value < 25.0 else FILL_OK
    	$Bar.add_theme_stylebox_override("fill", fill)

    이제 스타일박스에 쓰는 코드가 없으니 Local To Scene은 다시 껐어요. 딱 25를 낮은 걸로 칠지는 아직 못 정했어요.

같은 질문의 다른 갈래

  1. 타일을 거절하면 팔레트 위치도 넘어가야 할까요?

    Lua에서 코루틴을 만들기만 해서는 아직 아무것도 돌지 않아요. 호스트가 coroutine.resume을 불러야 생성기가 돌기 시작하고, 제안을 명시적으로 yield하는 지점까지 가요. 한 번에 타일 하나씩이에요. 빨간 타일…

  2. 제출한 뒤 Vulkan 버퍼는 누가 소유하나요?

    Vulkan 버퍼를 감싼 C++ RAII 래퍼가 그 버퍼를 쓰는 작업을 기록해 제출하고 스코프를 벗어난다. 소멸자는 그 자리에서 vkDestroyBuffer를 호출한다. 버퍼를 참조하는 제출된 명령은 아직 실행 중이다. 이…

    답글 5cppc
  3. 저장됨이 두 번 읽히는 상태 영역

    저장 한 번에 스크린 리더가 "저장됨"을 두 번 읽었어요. 뒤에 있는 상태는 saved로 가는 전이가 하나뿐이고, 리듀서 테스트도 그걸 보여 줘요. 두 번 바뀐 건 DOM이었어요. 상태 컴포넌트가 메시지가 있을 때만 영역을…