골드메탈 - 언데드 서바이버 따라하기 네 번째 (영상 11 ~ 13 정리 노트)
유니티 게임 개발 무료 강의 - 네 번째 따라하기
강사: 골드메탈 (나무위키 링크)
강의 링크 (영상 22 개)
의욕 저하로.. 잠시 쉬다가.. 다시 11 번 영상부터.. 😑
레이어(Layer)를 활용한다.
Enemy (Prefab Asset) 에서 추가한 Layer 를 지정하고, child object 에도 적용한다
Physics2D.CircleCastAll(): 원형의 캐스트를 쏘고 모든 결과를 반환하는 함수
void FixedUpdate()
{
targets = Physics2D.CircleCastAll(transform.position, scanRange,
Vector2.zero, 0, targetLayer);
}
1. 캐스팅 시작 위치
2. 원의 반지름
3. 캐스팅 방향
4. 캐스팅 길이
5. 대상 레이
Scanner.cs
public class Scanner : MonoBehaviour
{
public float scanRange;
public LayerMask targetLayer;
public RaycastHit2D[] targets;
public Transform nearestTarget;
void FixedUpdate()
{
targets = Physics2D.CircleCastAll(transform.position, scanRange,
Vector2.zero, 0, targetLayer);
nearestTarget = GetNearest();
}
Transform GetNearest()
{
Transform result = null;
float diff = 100;
foreach (RaycastHit2D target in targets)
{
Vector3 myPos = transform.position;
Vector3 targetPos = target.transform.position;
float curDiff = Vector3.Distance(myPos, targetPos);
if (curDiff < diff)
{
diff = curDiff;
result = target.transform;
}
}
return result;
}
}
Player GameObject 에 Scanner.cs 를 할당 후에 Scan Range 와 Target Layer 를 설정
FixedUpdate() 시마다 가장 가까운 Enemy 를 찾아서 Nearest Target 을 설정하기 때문에 비워둔다..
이렇게 자주 해도 괜찮은건가? 성능 문제?
Prefabs 에서 Bullet 0 선택해서 Scene 에 드래그 앤 드랍해서 편집 후에
다시 Prefabs 로 드래그 앤 드랍해서 새로운 아이템으로 등록 가능
편집 후에 Prefabs 로 드래그 앤 드랍한 경우에 새로운 것을 만들 수 있는 선택지가 주어진다
세 번째 시간 - 총탄 생성하기
Weapon.cs 에서 Scanner 에 있는 메소드를 쓰기 위해서 Player.cs 에 아래와 같이 Scanner 에 대한 처리를 추가해준다
Player.cs
public class Player : MonoBehaviour
{
...
public Scanner scanner;
...
void Awake()
{
...
scanner = GetComponent<Scanner>();
}
직접 만든 스크립트도 컴토넌트로 동일하게 취급해서 GetComponent<>() 이용
Weapon.cs
public class Weapon : MonoBehaviour
{
...
float timer;
Player player;
void Awake()
{
player = GetComponentInParent<Player>();
}
... void Update()
{
switch (id)
{
case 0:
transform.Rotate(Vector3.back * speed * Time.deltaTime);
break;
default:
timer += Time.deltaTime;
if (timer > speed)
{
timer = 0f;
Fire();
}
break;
}
}
public void Init()
{
switch (id)
{
case 0:
speed = 150;
Batch();
break;
default:
speed = 0.3f;
break;
}
}
... void Fire()
{
if (!player.scanner.nearestTarget)
return;
Transform bullet = GameManager.instance.pool.Get(prefabId).transform;
bullet.position = transform.position;
}
}
Weapon 1 의 Parent Object 가 Player 이기 때문에
Player player = GetComponentInParent<Player>() 를 이용해서 player 오브젝트에 있는 scanner 변수를 사용할 수 있고, player.scanner.nearestTarget 값을 읽을 수 있다.
네 번째 시간 - 총탄 발사하기
총탄은 속도가 필요하므로 Rigidbody 2D 를 추가하고,
중력의 영향을 받지 않기 위해서 Gravity Scale 을 0 으로 한다.
Weapon.cs - 총알 (Bullet 1) 이 가장 가까운 적을 향해 날아가게 된다.
public class Weapon : MonoBehaviour
{
...
void Fire()
{
if (!player.scanner.nearestTarget)
return;
Vector3 targetPos = player.scanner.nearestTarget.position;
Vector3 dir = targetPos - transform.position;
dir = dir.normalized;
Transform bullet = GameManager.instance.pool.Get(prefabId).transform;
bullet.position = transform.position;
bullet.rotation = Quaternion.FromToRotation(Vector3.up, dir);
bullet.GetComponent<Bullet>().Init(damage, count, dir);
}
}
normalized : 현재 벡터의 방향은 유지하고 크기를 1로 변환된 속도
FromToRotation : 지정된 축(Vector3.up / z축)을 중식으로 목표를 향해 회전하는 함수
12 번째 영상
Coroutine : 생명 주기와 비동기처러 실행되는 함수
IEnumerator : 코루틴만의 반환형 인터페이스
yield : 코루틴의 반환 키워드
플레이어 기준의 반대 방향 처리를 위해서 KnockBack() 에서 현재 위치 - 플레이이 위치
값을 이용
AddForce 함수로 힘 가하기를 수행하지만, 방향만 가진 벡터가 되기 위해서 normalized 값 사용
순간적인 힘이므로 Impulse 속성으로 설정
GetCurrentAnimatorStateInfo() 의 parameter 는 layer index 인데,
현재 Base Layer 만 있으니 해당 index 값인 0 으로 하면 된다
public class Enemy : MonoBehaviour
{
...
WaitForFixedUpdate wait;
void Awake()
{
...
wait = new WaitForFixedUpdate();
}
// Update is called once per frame
void FixedUpdate()
{
if (!isLive || anim.GetCurrentAnimatorStateInfo(0).IsName("Hit"))
{
return;
}
...
}
...
void OnTriggerEnter2D(Collider2D collision)
{
if (!collision.CompareTag("Bullet"))
return;
health -= collision.GetComponent<Bullet>().damage;
StartCoroutine(KnockBack()); // or, StartCoroutine("KnockBack");
if (health > 0)
{
anim.SetTrigger("Hit");
}
else
{
// .. Die
Dead();
}
}
IEnumerator KnockBack()
{
yield return wait; // 다음 하나의 물리 프레임을 딜레이
Vector3 playerPos = GameManager.instance.player.transform.position;
Vector3 dirVec = transform.position - playerPos;
rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
}
...}
두 번째 시간 - 사망 리액션
컴포넌트의 비활성화는 .enabled = false
리지드바디의 물리적 비활성화는 .simulated = false
SetBool 함수를 통해 죽는 애니메이션 상태로 전환
스프라이트 렌더러의 Sorting Order 감소 (Player 는 5)
재활용을 위해서 OnEnable 함수에서 되돌려야 한다
기존 Dead() 함수는 바로 없어지게 만들기 때문에 직접 호출하지 않고, Event 로 호출하게 한다
Enemy.cs
public class Enemy : MonoBehaviour
{
...
Collider2D coll;
void Awake()
{
...
coll = GetComponent<Collider2D>();
}
void OnEnable()
{
target = GameManager.instance.player.GetComponent<Rigidbody2D>();
isLive = true;
coll.enabled = true;
rigid.simulated = true;
spriter.sortingOrder = 2;
anim.SetBool("Dead", false);
health = maxHealth;
}
void OnTriggerEnter2D(Collider2D collision)
{
if (!collision.CompareTag("Bullet"))
return;
health -= collision.GetComponent<Bullet>().damage;
StartCoroutine(KnockBack()); // or, StartCoroutine("KnockBack");
if (health > 0)
{
anim.SetTrigger("Hit");
}
else
{
isLive = false;
coll.enabled = false;
rigid.simulated = false;
spriter.sortingOrder = 1;
anim.SetBool("Dead", true);
Dead();
}
}
IEnumerator KnockBack()
{
yield return wait; // 다음 하나의 물리 프레임을 딜레이
Vector3 playerPos = GameManager.instance.player.transform.position;
Vector3 dirVec = transform.position - playerPos;
rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
}
void Dead()
{
gameObject.SetActive(false);
}
}
DeadEnemy 1 과 2 의 Animation 에 Event 를 추가해준다
세 번째 시간 - 처치 데이터 얻기
게임 메니저에 레벨, 킬수, 경험치 변수 선언
Inspector 가 지져분해니 Header (인스펙터의 속성들을 이쁘게 구분시켜주는 타이틀) 이용
GameManager.cs
public class GameManager : MonoBehaviour
{
public static GameManager instance;
[Header("# Game Control")]
public float gameTime;
public float maxGameTime = 2 * 10f;
[Header("# Player Info")]
public int level;
public int kill;
public int exp;
public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
[Header("# Game Object")]
public PoolManager pool;
public Player player;
...
public void GetExp()
{
exp++;
if (exp == nextExp[level])
{
level++;
exp = 0;
}
}
}
Enemy.cs
Dead 처리시에 경험치 처리 코드 추가
public class Enemy : MonoBehaviour
{
...
void OnTriggerEnter2D(Collider2D collision)
{
if (!collision.CompareTag("Bullet") || !isLive)
return;
health -= collision.GetComponent<Bullet>().damage;
StartCoroutine(KnockBack()); // or, StartCoroutine("KnockBack");
if (health > 0)
{
anim.SetTrigger("Hit");
}
else
{
isLive = false;
coll.enabled = false;
rigid.simulated = false;
spriter.sortingOrder = 1;
anim.SetBool("Dead", true);
GameManager.instance.kill++;
GameManager.instance.GetExp();
}
}
...
}
13 번째 영상
일반적인 게임오브젝트는 월드 공간에 배치
유저 인터페이스는(GUI)는 스크린 공간에 배치
- UI > Canvas 메뉴로 캔버스 생성
Canvas 의 경우에 RectTransform 이 사용됨. 스크린 전용 Transform 역할 컴포넌트
Screen Space - Camera => 스크린을 카메라에 맞추는 형
World Space => 오브젝트처럼 월드 공간에 배치하는 형
여러 해상도에서 Text 의 크기를 일정하게 하기 위해서는 아래 설정을 변경해주어야 한다.
강의 내용처럼 글자가 보이지 않는다면
Alignment 를 조정해서 중간에 오도록 해줘야 함
핵심 : 어느 해상도에서나 크기를 유지하는 것이 UI의 포인트
두 번째 시간 - 스크립트 준비
UI 컴포넌트를 사용할 때는 UnityEngine.UI 네임스페이스 사용
public class HUD : MonoBehaviour
{
public enum InfoType
{
Exp, Level, Kill, Time, Health
}
public InfoType type;
Text myText;
Slider mySlider;
void Awake()
{
myText = GetComponent<Text>();
mySlider = GetComponent<Slider>();
}
void LateUpdate()
{
switch (type)
{
case InfoType.Exp:
break;
case InfoType.Level:
break;
case InfoType.Kill:
break;
case InfoType.Time:
break;
case InfoType.Health:
break;
}
}
}
세 번째 시간 - 경험치 게이지
Canvas 에 Slider 추가 후에 앵커를 통해서 위치를 조정
아래 키 조합으로 이용
Shift : 기준점 변경
Alt : 위치(크기) 변경
Slider componet 의 Interactable 속성을 꺼야 게이머가 값을 수정할 수 없게 된다
슬라이더에 적용할 값: 현재 경험치 / 최대 경험치
HUD.cs
public class HUD : MonoBehaviour
{
...
void LateUpdate()
{
switch (type)
{
case InfoType.Exp:
float curExp = GameManager.instance.exp;
float maxExp = GameManager.instance.nextExp[GameManager.instance.level];
mySlider.value = curExp / maxExp;
break;
...
}
}
}
두 번째 시간 - 레벨, 킬수 텍스트
UI - Text - TextMeshPro 의 텍스트는 한글로 사용하기에 살짝 번거롭다고 한다.....
UI - Legacy - Text 메뉴로 텍스트 생성
UI - Image 에 사용된 이미지 크기가 작은 경우에 Set Native Size (오브젝트 크기를 스프라이트의 원래 크기로 변경) 를 선택한다
HUD.cs
public class HUD : MonoBehaviour
{
public enum InfoType
{
Exp, Level, Kill, Time, Health
}
public InfoType type;
Text myText;
Slider mySlider;
void Awake()
{
myText = GetComponent<Text>();
mySlider = GetComponent<Slider>();
}
void LateUpdate()
{
switch (type)
{
case InfoType.Exp:
float curExp = GameManager.instance.exp;
float maxExp = GameManager.instance.nextExp[GameManager.instance.level];
mySlider.value = curExp / maxExp;
break;
case InfoType.Level:
myText.text = string.Format("Lv.{0:F0}", GameManager.instance.level);
break;
case InfoType.Kill:
myText.text = string.Format("{0:F0}", GameManager.instance.kill);
break;
case InfoType.Time:
break;
case InfoType.Health:
break;
}
}
}
다섯 번째 시간 - 타이머 텍스트
Level 텍스트를 복사해서 Anchor Presets 를 열어서 Shift + Alt 키로 상단 정중앙에 배치한다
public class HUD : MonoBehaviour
{
...
void LateUpdate()
{
switch (type)
{
...
case InfoType.Time:
float remainTime = GameManager.instance.maxGameTime - GameManager.instance.gameTime;
int min = Mathf.FloorToInt(remainTime / 60);
int sec = Mathf.FloorToInt(remainTime % 60);
myText.text = string.Format("{0:D2}:{1:D2}", min, sec);
break;
...
}
}
}
여섯 번째 시간 - 체력 게이지
GameManager.cs 에 체력 변수를 추가해준다
...
[Header("# Player Info")]
public int Health;
public int maxHealth = 100;
...
Canvas 에 Create Empty 하는 경우에는 RectTransform 이 들어있다
슬라이드 복사 후에 슬라이드가 부모 오브젝트와 넓이가 같도록 앵커 설정 (Shift + Alt 후에)
게임 실행시에 Slider 가 Player 와 제대로 연결이 되지 않아 플레이어를 따라가는 UI 스크립트 생성
월드 좌표와 스크린 좌표는 서로 다르기 때문에 변환해주어야 한다
Camera.main.WorldToScreenPoint() 를 이용해서... 월드 상의 오브젝트 위치를 스크린 좌표로 변환
Follow.cs
public class Follow : MonoBehaviour
{
RectTransform rect;
void Awake()
{
rect = GetComponent<RectTransform>();
}
void FixedUpdate()
{
rect.position = Camera.main.WorldToScreenPoint(GameManager.instance.player.transform.position);
}
}

댓글
댓글 쓰기