
RGB 컬러를 Add 노드에 입력으로 넣어주면 나머지 입력에 Brightness 값 하나만 넣어줘도 알아서 세 곳에 더해준다
RGB 값이 1을 넘어가 포스트 프로세싱 효과가 적용되는 일을 막으려면, Saturate 노드를 이용해서 값을 0~1 사이로 제한해줄 수 있다.
최소~최댓값을 직접 적용하려면 Clamp 노드를 사용할 수도 있다.
더하면 밝아지고, 곱하면 어두워지고, 나누면 밝아진다. (0~1 값 기준)
Add의 밝기 조절 범위를 0~1에서 -1~1로 만들어주면 밝게 뿐만 아니라 어둡게도 할 수 있다.

Texture 2D Asset으로 프로퍼티를 만들어준 후에, Sample Texture 2D 노드에 연결해줘야 한다.
Sample Texture 2D만으로는 프로퍼티를 못 만듦.
UV 노드를 연결해줄 수도 있지만, 기본적으로 0번 채널이 들어가있다.
텍스처 Tiling과 Offset을 조절하려면 Graph Inspector > Node Settings > Use Tiling and Offset 체크

흑백(Grayscale)은 컬러가 없는 상태, 즉 RGB의 숫자가 동일한 상태.
RGB 각각의 채널을 모두 Add한 후 Divide로 나눠주면 흑백이 된다.
이 방법도 좋고 빠르긴 하지만, 정확도와는 거리가 멀다.
RGB의 밝기(휘도)는 우리 눈에 밝게 인식되는 강도가 각각 다르기 때문.
녹색 > 빨강 > 파란 순으로 밝아 보인다. (녹색이 가장 밝아 보임)

휘도 비율을 RGB에 각각 곱한 후 더해주면 더 정확할 것이다.
직접 Multiply 노드를 추가해줄 수도 있겠지만,
Saturation 노드를 추가한 후 Saturation 값을 0으로 만들어주면 휘도 차이 별 강도가 적용된 흑백을 만들 수 있다.
(Saturate 노드와는 다르다)
Saturation에 프로퍼티를 적용하면 외부에서 채도를 조절하는 기능도 구현할 수 있다.

노드별 적용 예시

Lerp : Linear Interpolation, 선형 보간
한쪽이 0이고 다른 한쪽이 1이라면, 가운데는 0.5가 된다
T가 0이면 A 이미지만 나오고, 1이면 B 이미지만 나온다.
Lerp는 두 이미지를 자연스럽게 전환하는데 유리하다.

알파 채널이 있는 이미지와 Lerp 노드를 활용하면 이미지를 적절하게 합성할 수 있다.

알파채널의 색은 숫자이고,
검은 부분은 0이니 A 텍스쳐가 나오고,
흰 부분은 1이니 B 텍스쳐가 나온다.
회색의 알파 채널이 있었다면 반반 섞였을 것이다.
Tiling and Offset과 함께 사용하면 벽이나 바닥에 새겨진 자국이나 마크 등을 그릴 수 있다.

UV의 원리 : 3D로 이루어진 오브젝트에 2D 그림을 입히기 위해 벗겨내어 펼쳐 놓은 것과 비슷한 느낌
UV는 2차원 좌표다.
UV 배치는 엔진이나 툴마다 다르다.
언리얼, DirectX는 왼쪽 위가 Vector2 (0,0)이고,
유니티, OpenGL은 왼쪽 아래가 (0,0)이다.
텍스처에 좌표 할당해주는 거라고 생각하면 될듯.

UV 노드의 색은
좌측 하단은 (0,0,0)
우측 하단은 (1,0,0)
좌측 상단은 (0,1,0)
우측 상단은 (1,1,0)
을 시각적으로 나타낸 것

U와 V에 각각 0.3을 더해주고 적용시키면 텍스처가 왼쪽 아래로 살짝 내려온다.
단색이나 줄무늬로 나온다면 텍스처의 Filter 옵션이 Repeat나 Clamp여서 그런 것.
텍스처 필터 옵션

(0,0)에는 무슨 숫자를 곱해도 0이니 변화가 없을 거고,
(1,1)인 부분에 2를 곱하면 (2,2)가 될 것이다.
덧셈은 이동(Offset), 곱셈은 타일링(Tiling)이다.
Tiling And Offset이라는 노드도 있다.

Add에 매개변수에 Time을 넣어주면 텍스처가 흘러간다.
반대로 흘거가게 하고 싶으면 Time 노드에 Negate 노드(-1 곱함)를 붙이면 된다.
이를 이용하여 물이 흘러가는 효과를 줄 수 있다.
UV에 Time을 더하면 이동하지만, 곱하면 아무것도 보이지 않는다.
Time의 숫자는 유니티가 커질 때부터 지금까지의 시간이므로 그 숫자가 매우 커질 수 있기 때문이다.
불 이펙트를 만드는 방법에는 3가지가 있다
알파 채널을 가진 불 이미지를 준비한다.
Unlit 셰이더로 만들어서 어두운 곳에서도 빛나고, 빛이 있다고 해도 더 밝아지는 일이 없게 한다.
(Unlit은 불 같은 이펙트에는 매우 많이 사용되는 옵션이고, 빛 연산이 없어서 가볍다)
Graph Inspector > Graph Settings > Surface Type > Transparent로 바꿔서 알파 채널을 넣을 수 있게 한다.
Blending Mode: Transparent가 활성화되면 나오는 선택 모드
Render Face : 앞면과 뒷면 중 어떤 면에 렌더링 될지 결정하는 기능. 이펙트는 대부분 Both.

불이 위로 흘러간다.
(실수했던 점 : Combine 안 하고 Add만 해서 UV에 Float 숫자 하나가 더해지기만 함)

위로 움직이는 불 텍스처와 그냥 모닥불 이미지의 RGBA값을 곱한 결과를 Base Color에 넣어주고,
알파 채널만 따로 곱해준 결과를 Alpha 채널에 넣어주면 불 이펙트가 만들어진다.
퀄리티가 높은 건 아니지만, 멀리서 보이는 작은 불 등에 사용하기 괜찮다.
이 방법은 흐르는 물, 폭포, 동그랗게 퍼져나가면서 점점 사라지는 플라즈마 이펙트 등에도 사용할 수 있다.

Float를 곱해서 포스트 프로세스 Bloom 효과를 이용할 수도 있다
Global Volume > Tone Mapping > ACES : 좀 더 필름같은 강렬한 느낌

검은 텍스처의 R 채널은 전부 0이다.
UV에 0을 더해주면 아무 일도 발생하지 않는다.

회색을 UV에 더해주면 0.5만큼 움직인 느낌이 아니다.
텍스처는 sRGB 상태이지만 우리가 원하는 건 '회색'을 원하는게 아니라
'0.5'라는 데이터를 출력하길 원하므로, Linear 텍스처로 만들어줘야 한다.
텍스처를 선택하고 Inspector에서 sRGB (Color Texture) 토글을 해제해야 한다.

가운데가 밝은 이미지를 넣으면 가운데가 이동한다.
이미지의 밝기에 따라 이동하는 것이 다르다는 것을 알 수 있다.
(역시 sRGB를 꺼줘야 제대로 된다)

노이즈를 넣으면 더 구겨진다

노이즈의 UV에 Time을 더해서 흐르듯 변화하는 노이즈를 만들 수 있다
checker만 불 이미지로 바꾸면 일렁이는 불을 만들 수 있다
(노이즈를 위로만 흘러가게 하면 더 자연스러워진다)
버텍스 컬러를 적용하는 법
Package Manager에서 Polybrush 인스톨 > Tool > Polybrush > Polybrush Window > 가운데 삼각형 버튼 누르고 그리면
아무것도 보이지 않는다. 버텍스 컬러는 '기본적으로 출력되지 않기' 때문이다.
셰이더를 만들어서 버텍스 컬러를 보이게 만들어주어야 한다.

Shader Graph를 만들어서 적용시킨 뒤에 Fragment에 Vertex Color를 연결시켜주면 버텍스 컬러가 출력된다.
다른 텍스처와 곱하거나 더해서 응용할 수 있다.
이것을 잘 이용하면, 라이트맵을 베이크 할 수 없는 상황에서
엠비언트 오클루젼(Ambient Occlusion)처럼 이용하거나 저렴한 라이트맵처럼 이용할 수 있을 것.
Ambient Occlusion : 구석진 부분에 생기는 아주 어두운 음영
버텍스 컬러는 일반적인 컬러와 동일하게 RGBA로 구성되어 있다.
하지만 버텍스 컬러는 일반적인 텍스쳐가 가지고 있어야 하는 UV와 아무 상관이 없기 때문에, 응용해서 사용할 곳이 많다.
(UV가 바뀌어도 버텍스 컬러는 바뀌지 않는다)

Lerp 함수의 특성상 검은색 즉, 0인 부분은 A가 나오고 흰색 1인 부분은 B가 출력되어
버텍스 컬러의 R 채널에 따라 첫 번째 텍스쳐와 두 번째 텍스쳐가 섞여서 출력된다.

Lerp를 여러 번 해주면 멀티 텍스쳐링도 가능하다.
public class PlayerController : MonoBehaviour
{
private void Update()
{
if (Input.GetMouseButton(0))
{
MoveToCursor();
}
}
private void MoveToCursor()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
bool hasHit = Physics.Raycast(ray, out hit);
if (hasHit) GetComponent<Mover>().MoveTo(hit.point);
}
}
Move와 Combat을 관장하는 PlayerController.cs 생성 및 리팩토링
monobehaviour나 Update 작성한 후에 Tab 누르면 스니펫 완성해준다 (원래는 그래야 하는데 내 vscode는 안해줌)
프리팹 옆에 있는 화살표 누르면 프리팹 편집기로 바로 들어갈 수 있다.
의존성은 안좋다. 하나 망가지면 다른 것도 망가질 수 있음.
사이클은 최악. 다 망가짐.
사이클 같은거 안 생기게 하려면 레이어로 다 뜯어놓으면 됨.
Control은 Combat에 의존하고, Combat은 Stats에 의존하고...
하위 객체로 갈 수록 안정성이 증가한다. 변경도 덜 해야됨. 이런거 변경하면 프로젝트가 불안정해짐.
using으로 네임스페이스를 명시해놓으면 의존성 관리를 더 잘 할 수 있게 된다.
네임스페이스에는 관련 있는 것들만 다 넣어놓으면 됨.
namespace RPG.Control
{
public class PlayerController : MonoBehaviour
{
// 생략
}
}
이제 다른 네임스페이스에 있는 클래스에선 using문을 사용해야 함.
namespace RPG.Control
{
public class PlayerController : MonoBehaviour
{
private void Update()
{
InteractWithCombat();
InteractWithMovement();
}
private void InteractWithCombat()
{
RaycastHit[] hits = Physics.RaycastAll(GetMouseRay());
foreach (RaycastHit hit in hits)
{
CombatTarget combatTarget = hit.transform.GetComponent<CombatTarget>();
if (combatTarget != null && Input.GetMouseButtonDown(0))
{
GetComponent<Fighter>().Attack(combatTarget);
}
}
}
RaycastAll은 지나가면서 받은 모든 오브젝트를 저장한다. 그래서 RaycastHit[]를 반환한다.
오브젝트로 가려진 적 클릭해도 공격할 수 있게끔 할 때 사용.
Input.GetMouseButtonDown(0) 조건 안 넣으면 요청이 계속해서 들어옴.
Ctrl + .를 통해 IDE의 인텔리센스 기능들 이용 가능
Inline temporal varialbe로 코드를 즉각 조정할 수 있다공격하는거라면 그냥 이동 시키면 안된다. 일단 print("Attack")으로 해둠.
if (combatTarget != null && Input.GetMouseButtonDown(0))
{
GetComponent<Fighter>().Attack(combatTarget);
return true;
}
이를 위해 메서드들이 bool값을 return하도록 바꿀건데,
이런식으로 if문 안에 return true를 넣지 않을거다.
cursor affordance (대상이 공격 가능한건지, 상호작용 가능한건지, 이동할 곳인지 커서로 표현) 을 구현할 것이기 때문.
private void Update()
{
if (InteractWithCombat()) return;
if (InteractWithMovement()) return;
print("Nothing to do");
}
private bool InteractWithCombat()
{
RaycastHit[] hits = Physics.RaycastAll(GetMouseRay());
foreach (RaycastHit hit in hits)
{
CombatTarget combatTarget = hit.transform.GetComponent<CombatTarget>();
if (combatTarget == null) continue;
if (Input.GetMouseButtonDown(0))
{
GetComponent<Fighter>().Attack(combatTarget);
}
return true;
}
return false;
}
근데 Console.Log()가 아니라 그냥 print() 때려도 되는 건 처음 알았네
private void Update()
{
if (target == null) return;
bool isInRange = Vector3.Distance(transform.position, target.position) > weaponRange;
if (isInRange)
{
GetComponent<Mover>().MoveTo(target.position);
}
else
{
GetComponent<Mover>().Stop();
}
}
public void StartMoveAction(Vector3 destination)
{
GetComponent<Fighter>().Cancel();
MoveTo(destination);
}
private void Update()
{
if (target == null) return;
if (GetIsInRange())
{
GetComponent<Mover>().MoveTo(target.position);
}
else
{
GetComponent<Mover>().Stop();
}
}
Mover와 Fighter 사이에 의존성 순환이 생겼다.
미사일을 상속받는 총알, 화살, 로켓이 있다면
미사일 받는다고 선언해놓으면 총알 넣어도 되고 화살 넣어도 된다
이게 치환 원칙
Mover랑 Fighter는 둘 다 MonoBehaviour 상속받으니까
MonoBehaviour로 받겠다고 선언하면 둘 다 받을 수 있음
(PlayerController도 받을 수 있다는게 문제인데, 그건 나중에 해결)
namespace RPG.Core
{
public class ActionScheduler : MonoBehaviour
{
MonoBehaviour prev_action;
public void StartAction(MonoBehaviour action)
{
if (prev_action == action) return;
if (prev_action != null)
{
print("Canceling " + prev_action);
}
prev_action = action;
}
}
}
Movement와 Combat을 관장하는 스케줄러를 도입하여 의존성 순환을 해결한다.
Move 시작하거나 Combat 시작할 때 스케줄러에 말하는 구조
근데 이래버리면 Scheduler가 Movement랑 Combat에 의존을 해버려서 또 다른 의존성 순환이 만들어진다.
GetComponent<ActionScheduler>().StartAction(this);
Mover랑 Fighter에서 행동 시작할 때 this로 자기 자신 넣어서 호출
IAction이라는 인터페이스를 만들어서 Movement랑 Combat을 묶은 뒤에
Scheduler는 IAction에 의존하면 의존성 방향 바꾸는 의존성 역전 가능
namespace RPG.Core
{
public interface IAction
{
void Cancel();
}
}
public void StartAction(IAction action)
{
if (prev_action == action) return;
if (prev_action != null)
{
prev_action.Cancel();
}
prev_action = action;
}

Transition Duration 또는 아래의 바를 건드려서 애니메이션 전환을 얼마나 자연스럽게 할지 정할 수 있다

애니메이션 파일 선택 > Inspector 하단 > 우측 상단 Avatar 아이콘 > Unity Model 혹은 적용하고 싶은 캐릭터 > 정확한 타격 시점 알아내어 게임 플레이 이벤트를 애니메이션과 동기화할 수 있다

근데 애초에 애니메이션 자체에 Hit()를 호출하는게 있었음.
private void AttackBehaviour()
{
GetComponent<Animator>().SetTrigger("Attack");
}
transition에 trigger 설정하고 스크립트로 trigger 체크
private void Update()
{
timeSinceLastAttack += Time.deltaTime;
// 생략
}
private void AttackBehaviour()
{
if (timeSinceLastAttack > timeBetweenAttacks)
{
GetComponent<Animator>().SetTrigger("Attack");
timeSinceLastAttack = 0;
}
}
애니메이션 trigger 설정에 쿨타임 두기
namespace RPG.Combat
{
public class Health : MonoBehaviour
{
[SerializeField] float health = 100f;
public void TakeDamage(float damage)
{
health = Math.Max(health - damage, 0);
print(health);
}
}
}
Fighter.cs의 Hit()에서 TakeDamage 호출
애니메이션에서 정확히 때리는 타이밍에 Hit()를 호출하기 때문에 휘두르기도 전에 데미지가 가는 상황 일어나지 않음