2022. 11. 16. 17:20ㆍProjects/Charon
졸업 작품을 마무리하는 시즌이라 바빠서, 개발에만 몰두하다 보니 글을 쓸 시간이 없었네요. 마감이 다가오니, 효율적이고 좋은 소스 코드를 짜는 것과 시간 안에 마무리 하는 것 사이에서 타협을 해야 했습니다.
그러다 보니, 이게 좋은 방법인가를 고민할 겨를이 많이 없어 하드 코딩도 어느 정도 들어간 것 같습니다. 시간이 나면 차차 다시 고민해보며 리팩토링을 해보고 싶네요.
글을 안 쓴 지 오래 되었다 보니, 어디서부터 글을 써야할 지 모르겠더군요,,, 그래서 일단 최근에 한 것 중 글을 독립적으로 쓸 수 있는 부분부터 써보도록 하겠습니다.
1. 쿼터뷰(isometric view) 시점의 문제점
이 게임은 3D 로그라이크 핵 앤 슬래시 게임으로, 쿼터뷰 시점을 가지고 있습니다. 위에서 살짝 대각선 방향인 부분에서 내려다 보는 시점을 의미하지요.
사실, isometric view란 말이 정확한 개념이긴 합니다. isometric view는 Orthographic 투영과 관련된 내용이라 3D에서는 거의 쓰이진 않지만, 디자이너가 없는 저희 팀 형편상 isometric 2D Sprite를 구하기가 쉽지 않았기에 3D로 불가피하게 결정된 케이스긴 합니다.
위와 같은 특징을 가진 쿼터뷰는 게임 내의 환경 오브젝트(건물, 나무 등)가 캐릭터를 가릴 수 있다는 문제점이 존재합니다.
이러한 문제점을 해결하는 방법으로, 오브젝트가 캐릭터를 가리는 동안에만 해당 오브젝트를 투명화 처리하는 방법이 있습니다. 보통 해당 오브젝트의 Material Rendering Mode를 Transparent로 처리하여 Albedo의 알파값을 조절하는 방식으로 구현할 수 있습니다. (창문과 같은 투명 오브젝트를 만들 때 유용한 방법이죠.)
하지만 제 게임에서 이 방법을 적용하는 것에는 무리가 있었습니다.
2. LOD가 존재하는 Material의 투명화 문제
LOD(Level Of Detail)는 카메라와의 거리에 따라 품질을 다르게 처리하는 걸 말합니다. 예를 들어, 배틀그라운드와 같은 게임에서 나와 1km에 떨어져 있는 집을 수많은 폴리곤(Polygon)들을 사용하여 고퀄로 해놓을 필요는 없을 겁니다. 멀리 있어서 잘 안 보일테니까요. 안 보이는 지점까지 멀어졌다면 아예 Culling하면 되겠지요.
그래서 해당 오브젝트에 대해 거리에 따라 사용할 품질들을 그룹으로 관리하는 게 LOD Group입니다.
시야 거리에 따라 기둥 오브젝트의 품질이 다소 변하는 걸 볼 수 있습니다. 이렇게 최적화를 해주는 것이죠. 다만, 여러 개의 메시(Mesh)들을 그룹으로 관리하기에 위에서 적용한 투명화 방법을 사용하면 다음과 같이 투명화가 되곤 했습니다.
절망했습니다. 모르겠어서 구글링도 열심히 해보고 했더니 2시간이 훌쩍 지났더군요. 그러다가 런타임 중에 Material의 Rendering Mode를 변경하는 아이디어를 발견할 수 있었습니다.
(솔직히, Material의 내부 프로퍼티까지 다 살펴본 것이 아니었기에 코드의 정확한 동작 원리는 설명하지 못합니다..)
☑️문제 해결 시작하기
1. "TransparentObject" 스크립트 작성하기
우선 알고리즘 아이디어는 다음과 같습니다.
- 캐릭터와 카메라 사이에 캐릭터를 가리는 오브젝트를 검사한다.
- 해당 오브젝트의 Material Rendering Mode를 "Fade"로 변경, Source를 "Albedo alpha", Albedo alpha 값을 천천히 내린다. 알파 값을 다 내렸다면, 타이머를 동작한다.
- 오브젝트가 투명화가 진행이 다 되었는데도 캐릭터를 가리고 있다면, 타이머를 계속 초기화한다.
- 아니라면, 지정한 시간이 다 경과되었을 경우, 다시 Reset한다.
제가 구매한 에셋들은 자식 오브젝트들에게 MeshRenderer가 다 붙어 있어서, GetComponentsInChildern()을 이용했습니다. 소스코드는 아래에 더보기를 누르면 있습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TransparentObject : MonoBehaviour
{
public bool IsTransparent { get; private set; } = false;
private MeshRenderer[] renderers;
private WaitForSeconds delay = new WaitForSeconds(0.001f);
private WaitForSeconds resetDelay = new WaitForSeconds(0.005f);
private const float THRESHOLD_ALPHA = 0.25f;
private const float THRESHOLD_MAX_TIMER = 0.5f;
private bool isReseting = false;
private float timer = 0f;
private Coroutine timeCheckCoroutine;
private Coroutine resetCoroutine;
private Coroutine becomeTransparentCoroutine;
void Awake()
{
renderers = GetComponentsInChildren<MeshRenderer>();
}
public void BecomeTransparent()
{
if (IsTransparent)
{
timer = 0f;
return;
}
if (resetCoroutine != null && isReseting)
{
isReseting = false;
IsTransparent = false;
StopCoroutine(resetCoroutine);
}
SetMaterialTransparent();
IsTransparent = true;
becomeTransparentCoroutine = StartCoroutine(BecomeTransparentCoroutine());
}
#region #Run-time 중에 RenderingMode 바꾸는 메소드들
/// Runtime 중에 RenderingMode를 바꾸는 방법을 찾아보니, 다음과 같은 코드를 사용한다고 함. <summary>
// 0 = Opaque, 1 = Cutout, 2 = Fade, 3 = Transparent
private void SetMaterialRenderingMode(Material material, float mode, int renderQueue)
{
material.SetFloat("_Mode", mode);
material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
material.SetInt("ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.EnableKeyword("_ALPHABLEND_ON");
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = renderQueue;
}
private void SetMaterialTransparent()
{
for(int i = 0; i< renderers.Length; i++)
{
foreach(Material material in renderers[i].materials)
{
SetMaterialRenderingMode(material, 3f, 3000);
}
}
}
private void SetMaterialOpaque()
{
for (int i = 0; i < renderers.Length; i++)
{
foreach (Material material in renderers[i].materials)
{
SetMaterialRenderingMode(material, 0f, -1);
}
}
}
#endregion
public void ResetOriginalTransparent()
{
SetMaterialOpaque();
resetCoroutine = StartCoroutine(ResetOriginalTransparentCoroutine());
}
private IEnumerator BecomeTransparentCoroutine()
{
while (true)
{
bool isComplete = true;
for(int i =0; i< renderers.Length; i++)
{
if (renderers[i].material.color.a > THRESHOLD_ALPHA)
isComplete = false;
Color color = renderers[i].material.color;
color.a -= Time.deltaTime;
renderers[i].material.color = color;
}
if (isComplete)
{
CheckTimer();
break;
}
yield return delay;
}
}
private IEnumerator ResetOriginalTransparentCoroutine()
{
IsTransparent = false;
while (true)
{
bool isComplete = true;
for (int i = 0; i < renderers.Length; i++)
{
if (renderers[i].material.color.a < 1f)
isComplete = false;
Color color = renderers[i].material.color;
color.a += Time.deltaTime;
renderers[i].material.color = color;
}
if (isComplete)
{
isReseting = false;
break;
}
yield return resetDelay;
}
}
public void CheckTimer()
{
if (timeCheckCoroutine != null)
StopCoroutine(timeCheckCoroutine);
timeCheckCoroutine = StartCoroutine(CheckTimerCouroutine());
}
private IEnumerator CheckTimerCouroutine()
{
timer = 0f;
while (true)
{
timer += Time.deltaTime;
if(timer > THRESHOLD_MAX_TIMER)
{
isReseting = true;
ResetOriginalTransparent();
break;
}
yield return null;
}
}
}
이렇게 소스코드를 작성하고, 투명화 처리가 되길 원하는 오브젝트에 붙이고, Layer를 EnvironmentObject로 변경하여 줍니다.
2. 카메라에서 플레이어 캐릭터를 가리는 오브젝트 탐지하기
오브젝트가 몇 개 겹쳐서 가릴 수도 있으므로, RaycastAll()을 사용하여 모두 탐지하도록 합니다.
// Camera script
void LateUpdate()
{
// Player는 싱글톤이기에 전역적으로 접근할 수 있습니다.
Vector3 direction = (Player.Instance.transform.position - transform.position).normalized;
RaycastHit[] hits = Physics.RaycastAll(transform.position, direction, Mathf.Infinity,
1 << LayerMask.NameToLayer("EnvironmentObject"));
for (int i = 0; i < hits.Length; i++)
{
TransparentObject[] obj = hits[i].transform.GetComponentsInChildren<TransparentObject>();
for (int j = 0; j < obj.Length; j++)
{
obj[j]?.BecomeTransparent();
}
}
}
여기서도 GetComponentsInChildren()을 이용하여 TransparentObject 스크립트를 가져오는 이유는 빈 오브젝트에 자식으로 묶어서 오브젝트들을 관리하기 때문입니다.
위에서 보여드린 기둥 오브젝트는 ddi_pillar_01이며, 이것은 Obejcts라는 빈 오브젝트의 자식에서 관리되고 있음을 볼 수 있습니다. 아무튼, 이렇게 하고 나면 투명화가 잘 처리되는 것을 볼 수 있습니다.
결과
이제 벽이나 오브젝트가 캐릭터를 가려서 몬스터와 싸우는데 불편함이 없어졌습니다.
'Projects > Charon' 카테고리의 다른 글
[Charon] #10. "카론의 노" 무기 공격 및 스킬 구현하기 (9) | 2023.04.04 |
---|---|
[Charon] #9. 대시(Dash) 선입력(Input Buffer) 기능 추가하기 (0) | 2022.11.16 |
[Charon] #7. 무기 기본 3타 콤보 공격 구현하기 (긴 글 주의) (0) | 2022.10.06 |
[Charon] #6. 휴머노이드 애니메이션 수정과 무기 탈장착 시스템 기반 만들기 (1) | 2022.10.05 |
[Charon] #5. 상태 패턴(State Pattern) 도입하기 (4) | 2022.10.04 |