오늘 학습 키워드

TopDown 클론 코딩

오늘 학습 한 내용을 나만의 언어로 정리하기

TopDown

투사체

private void OnTriggerEnter2D(Collider2D collision)
{
    if(levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))
    {
 
    }
    else if (rangeWeaponHandler.target.value == (rangeWeaponHandler.target.value | (1 << collision.gameObject.layer)))
    {
 
    }
 
}
  • Layer 비교
    • Layer는 이진 값으로 되어있음
      • ex. 3번 레이어가 켜져있으면 (1000), 5번도 켜져있으면 (101000)
    • collision.gameObject.layer는 정수로 나올 것임.
      • ex. 3번 레이어가 켜져있으면 3, 4번 레이어가 켜져있으면 4.
    • 그래서 1을 왼쪽 쉬프트 연산하는거임

파티클 구현

  • Simulation Space - World : World Position에 생성됨
  • Color over Lifetime : 시간 지남에 따른 색 변화
  • Size over Lifetime : 시간 지남에 따른 크기 변화
  • Limit Velocity over Lifetime : 속도 제한
  • 애니메이션 이벤트 연결하기
    • 특정 애니메이션 구간에 실행될 함수를 지정할 수 있는데, 그러려면 스크립트가 같이 달려있어야 함

적 오브젝트 기초 구성

코루틴

  • 시간 기반 / 비동기적 작업 처리를 위한 메소드. 중간에 실행을 중단하고 나중에 재개할 수 있음
    • 비동기적 작업 : 프레임 단위로 나눠서 실행됨
    • yield 키워드를 사용해 특정 조건/시간까지 기다렸다가 재개할 수 있음
  • yield return 종류
    • null : 다음 프레임까지 대기
    • new WaitForSeconds(시간) : 지정된 시간(초)만큼 대기
    • new WaitForSecondsRealtime(시간) : 실제 시간 기준으로 지정된 시간(초)만큼 대기
    • new WaitUntil(조건) : 조건이 참이 될 때까지 대기
    • new WaitWhile(조건) : 조건이 거짓이 될 때까지 대기
    • break : 코루틴 실행을 즉시 종료
  • 코루틴을 실행할 때에는 StartCoroutine(함수명)으로 써야함
  • 코루틴은 IEnumerator 형태로 반환하게 되어있음
// 몬스터 소환에 코루틴을 사용한 예시
private IEnumerator SpawnWave(int waveCount)
{
    enemySpawnComplite = false;
    yield return new WaitForSeconds(timeBetweenWaves); // timebetweenWaves 초 만큼 대기함
 
    for(int i = 0; i < waveCount; i++)
    {
        yield return new WaitForSeconds(timeBetweenSpawns); // timeBetweenSpawns 초 만큼 대기한 후 적 소환
        SpawnRandomEnemy();
    }
 
    enemySpawnComplite = true;
 
}

적 소환하기

private void SpawnRandomEnemy()
{
    if(enemyPrefabs.Count == 0 || spawnAreas.Count == 0)
    {
        Debug.LogWarning("Enemy Prefabs 또는 Spawn Areas가 설정되지 않았습니다.");
        return;
    }
 
    GameObject randomPrefab = enemyPrefabs[Random.Range(0, enemyPrefabs.Count)]; // 랜덤 적 GameObject
 
    Rect randomArea = spawnAreas[Random.Range(0, spawnAreas.Count)]; // 스폰될 수 있는 위치 중 하나
 
    Vector2 randomPosition = new Vector2( // 스폰될 수 있는 위치 내에서 랜덤값
        Random.Range(randomArea.xMin, randomArea.xMax),
        Random.Range(randomArea.yMin, randomArea.yMax));
 
    GameObject spawnEnemy = Instantiate(randomPrefab, new Vector3(randomPosition.x, randomPosition.y), Quaternion.identity);
    EnemyController enemyController = spawnEnemy.GetComponent<EnemyController>();
 
    // 생성된 적에 달려있는 EnemyController를 가져와서
    // 활성화 되어있는 적 목록에 추가
 
    activeEnemies.Add(enemyController); 
}

기즈모 그리기

  • 기즈모 : 개발을 위한 아이콘.
private void OnDrawGizmosSelected()
{
    if (spawnAreas == null) return;
 
    Gizmos.color = gizmoColor;
    foreach (var area in spawnAreas)
    {
        Vector3 center = new Vector3(area.x + area.width / 2, area.y + area.height / 2);
        Vector3 size = new Vector3(area.width, area.height);
 
        Gizmos.DrawCube(center, size);
    }
}
  • 대강 그림판으로 설명

스테이지 구성

  • 기존의 EnemyManager에서 직접 컨트롤하던 스테이지를 GameManager가 하도록 변경함

데미지 / 피격처리

데미지 처리

  • 기존 ProjectileController 에서 수정함.
private void OnTriggerEnter2D(Collider2D collision)
{
    // level과 부딛힘
    if(levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))
    {
        DestroyProjectile(collision.ClosestPoint(transform.position) - direction * .2f, fxOnDestroy);
    }
 
    // target과 부딛힘. 데미지 처리 필요
    else if (rangeWeaponHandler.target.value == (rangeWeaponHandler.target.value | (1 << collision.gameObject.layer)))
    {
        ResourceController resourceController = collision.GetComponent<ResourceController>();
        if (resourceController != null)
        {
            resourceController.ChangeHealth(-rangeWeaponHandler.power);
            if (rangeWeaponHandler.IsOnKnockback)
            {
                BaseController controller = collision.GetComponent<BaseController>();
                if (controller != null)
                {
                    controller.ApplyKnockback(transform, rangeWeaponHandler.KnockbackPower, rangeWeaponHandler.KnockbackTime);
                }
            }
        }
        DestroyProjectile(collision.ClosestPoint(transform.position), fxOnDestroy);
    }
}

사망 처리

  • BaseController (모든 움직이는 캐릭터들의 부모) 에서 관리
// BaseController.cs
public virtual void Death()
{
    // 죽었을 때 처리
    // 1. 속도 0으로
    _rigidbody.velocity = Vector3.zero;
 
    // 2. 모든 스프라이트 빨갛고 약간 투명하게
    foreach(SpriteRenderer renderer in transform.GetComponentsInChildren<SpriteRenderer>())
    {
        Color color = renderer.color;
        color.a = 0.3f;
        renderer.color = color;
    }
 
    // 3. 코드 동작 안하도록 끄기. 참고 : MonoBehaviour가 상속하는게 Behaviour임.
    foreach(Behaviour component in transform.GetComponentsInChildren<Behaviour>())
    {
        component.enabled = false;
    }
 
    // 4. 2초 이후에 삭제
    Destroy(gameObject, 2f);
}
  • 이걸 실제로 불러주는 부분은 ResourceController에 있음.
public bool ChangeHealth(float change)
{
    if(change == 0 || timeSinceLastChange < healthChangeDelay)
    {
        return false;
    }
 
    timeSinceLastChange = 0;
    CurrentHealth += change;
    CurrentHealth = CurrentHealth > MaxHealth ? MaxHealth : CurrentHealth;
    CurrentHealth = CurrentHealth < 0 ? 0 : CurrentHealth;
 
    if(change < 0)
    {
        animationHandler.Damage();
    }
 
    if(CurrentHealth <= 0f)
    {
        Death(); // 여기서 호출
    }
    return true;
}
 
private void Death()
{
    baseController.Death(); // 본인이 알고있는 baseController의 Death 호출
}

스테이지 순환

  • EnemyManager에서 관리
  • 적 생성 : EnemyManager에서 정해진 순서대로
  • 적 파괴 : 파괴 자체는 EnemyController에서 인지하고, 그걸 EnemyManager로 정보를 전달함

무기 착용 / 공격

적 무기 추가

  • 기존에 있던 P_Bow_EquipWeapon 을 두 개 복제해서 사용함
  • Target Layer를 Enemy가 아닌 Player로 변경함

근거리 무기 / 적 구현

근거리 무기 오브젝트 생성

  • 근거리 충돌 처리
// 형태를 가지고 있는 레이캐스트 사용
RaycastHit2D hit = Physics2D.BoxCast(
    transform.position + (Vector3)Controller.LookDirection * collideBoxSize.x, // 내가 있는 위치 + 내가 보고 있는 방향으로
    collideBoxSize, // 미리 정해둔 충돌 기준 크기만큼
    0, // 0도로
    Vector2.zero, // 방향 없이
    0, // 거리 0 (근거리니까)
    target // 타겟만 찾기
    );
  • 근거리 무기의 경우 Rotate를 재설정 해주어야 함
public override void Rotate(bool isLeft)
{
    // base.Rotate(isLeft);
    if (isLeft)
        transform.eulerAngles = new Vector3(0, 180, 0);
    else
        transform.eulerAngles = new Vector3(0, 0, 0);
}

사운드 처리

VFX 처리

  • SoundSource 를 만들어서 Prefab화 함. 필요할 때 부르게
public class SoundSource : MonoBehaviour
{
    private AudioSource _audioSource;
 
    public void Play(AudioClip clip, float soundEffectVolume, float soundEffectPitchVariance)
    {
        if(_audioSource == null)
            _audioSource = GetComponent<AudioSource>();
 
        // 만약에 클립 재사용 되는 경우에 밑에 있는 비활성화 (Disable) 함수 실행 안되게 하려고 캔슬하는거임.
        CancelInvoke();
        _audioSource.clip = clip;
        _audioSource.volume = soundEffectVolume;
        _audioSource.Play();
        _audioSource.pitch = 1f + Random.Range(-soundEffectPitchVariance, soundEffectPitchVariance); // 효과음 랜덤성
 
        Invoke("Disable", clip.length + 2);
    }
 
    public void Disable()
    {
        _audioSource?.Stop();
        Destroy(this.gameObject);
    }
}

BGM 처리

  • BGM 은 SoundManager에서 씀
public class SoundManager : MonoBehaviour
{
    public static SoundManager instance;
    [SerializeField][Range(0f, 1f)] private float soundEffectVolume;
    [SerializeField][Range(0f, 1f)] private float soundEffectPitchVariance;
    [SerializeField][Range(0f, 1f)] private float musicVolume;
 
    private AudioSource musicAudioSource;
    public AudioClip musicClip;
 
    public SoundSource soundSourcePrefab; // 효과음 내주는 애
 
    private void Awake()
    {
        instance = this;
        musicAudioSource = GetComponent<AudioSource>();
        musicAudioSource.volume = musicVolume;
        musicAudioSource.loop = true;
    }
 
    private void Start()
    {
        ChangeBackGroundMusic(musicClip);
    }
 
    public void ChangeBackGroundMusic(AudioClip clip)
    {
        musicAudioSource.Stop();
        musicAudioSource.clip = clip;
        musicAudioSource.Play();
    }
 
    public static void PlayClip(AudioClip clip)
    {
        SoundSource obj = Instantiate(instance.soundSourcePrefab);
        SoundSource soundSource = obj.GetComponent<SoundSource>();
        soundSource.Play(clip, instance.soundEffectVolume, instance.soundEffectPitchVariance);
    }
}

UI 구성

  • 저번처럼 UI를 State로 관리함.

UI Anchor (UI 앵커)

  • UI 앵커 : Canvas 상의 UI 요소가 화면 크기나 해상도가 변경되더라도 일정한 위치와 크기를 유지하도록 돕는 기능

UI 렌더링 순서

  • 하이어라키 순서와 동일함.

  • 상단부에서 하단부로 내려오는 순서대로 그려짐 맨 하단부에 있는게 위에 그려짐

  • 순서 변경 방법

    1. 하이어라키에서 드래그 앤 드롭으로 바꾸기
    2. 스크립트에서 Transform.SetSiblingIndex 로 바꾸기
  • 캔버스 추가 설정

    • 캔버스 마다의 Sort Order를 변경할 수 있음. 숫자가 높은 캔버스가 위에 그려짐.

체력 감소 UI 구현

  • Delegate 사용
// ResourceController.cs
private Action<float, float> OnChangeHealth;
...
 
public bool ChangeHealth(float change)
{
    if(change == 0 || timeSinceLastChange < healthChangeDelay)
    {
        return false;
    }
 
    timeSinceLastChange = 0;
    CurrentHealth += change;
    CurrentHealth = CurrentHealth > MaxHealth ? MaxHealth : CurrentHealth;
    CurrentHealth = CurrentHealth < 0 ? 0 : CurrentHealth;
 
	// 체력 바뀌면 OnChangeHealth 호출
    OnChangeHealth?.Invoke(CurrentHealth, MaxHealth); 
 
    if(change < 0)
    {
        animationHandler.Damage();
        if(damageClip != null)
            SoundManager.PlayClip(damageClip);
    }
 
    if(CurrentHealth <= 0f)
    {
        Death();
    }
    return true;
}
...
 
public void AddHealthChangeEvent(Action<float, float> action)
{
    OnChangeHealth += action;
}
 
public void RemoveHealthChangeEvent(Action<float, float> action)
{
    OnChangeHealth -= action;
}
// GameManager.cs
private void Awake()
{
    Instance = this;
 
    player = FindObjectOfType<PlayerController>();
    player.Init(this);
 
    uiManager = FindObjectOfType<UIManager>();
 
 
    enemyManager = GetComponentInChildren<EnemyManager>();
    enemyManager.Init(this);
 
    _playerResourceController = player.GetComponent<ResourceController>();
    // 혹시 모르니 미리 지움
    _playerResourceController.RemoveHealthChangeEvent(uiManager.ChangePlayerHP);
    // 그리고 더하기
    _playerResourceController.AddHealthChangeEvent(uiManager.ChangePlayerHP);
}
 
// UIManager.cs
public void ChangePlayerHP(float currentHP, float maxHP)
{
    gameUI.UpdateHPSlider(currentHP / maxHP);
}
  • 기억 되짚기 : Func 는 반환이 있는 델리게이트, Action은 반환이 없는 델리게이트!

InputSystem 활용

  • 다양한 Input을 처리할 수 있게 해주는 시스템. 기존의 Legacy 버전과 새로운 버전, 두 가지 방식으로 입력 처리 가능
  • 특징
    • Unity 새로운 입력 처리 방식
    • 다양한 입력 장치를 쉽게 통합 가능
    • 키 매핑 변경이 간단함. 이벤트 기반 구조
  • 구조
    • Input Actions Asset
      • 입력 처리를 설정
    • Player Input 컴포넌트
      • Input Actions를 쉽게 적용할 수있는 컴포넌트
      • Unity Event 를 통해 입력 처리

사용방법

  1. Input Actions 를 추가한다.

2. Action Maps에 하나 추가

  1. 원하는 만큼 액션 추가

  2. 어떻게 값을 받아올 것인지 설정

  • 예시 1. Move 라는 액션에는 Vector2 방식으로 아래와 같이 가져옴 (WASD)

  • 예시 2. Look 이라는 액션에는 Vector2 방식으로 아래와 같이 가져옴 (마우스 위치)

  1. Input Action을 적용할 오브젝트에 컴포넌트로 Player Input을 추가함
  2. 그리고 Actions 에 방금 만든 Input Action을 추가함
  • 참고 : Behavior 를 Send Messages 방식으로 하면, 같은 오브젝트 안에 있는 스크립트에게 메시지를 전달함. 키가 입력되면 자동으로 역호출함.

  • 참고 2: Unity Events 로 하면 Button에 OnClick 이벤트 등록하는 것 마냥 할 수도 있긴 함. 여기서는 Send Message 방식으로 구현할 것

이번 TopDown에서 적용

  • 원래 HandleAction에 있던 내용을 확인해 봄.
protected override void HandleAction()
{
	// 이동
    float horizontal = Input.GetAxisRaw("Horizontal");
    float vertical= Input.GetAxisRaw("Vertical");
    movementDirection = new Vector2(horizontal, vertical).normalized;
	// 이동
 
	// 보는 방향 수정
    Vector2 mousePosition = Input.mousePosition;
    Vector2 worldPos = camera.ScreenToWorldPoint(mousePosition);
    lookDirection = (worldPos - (Vector2)transform.position);
 
    if(lookDirection.magnitude < .9f)
    {
        lookDirection = Vector2.zero;
    }
    else
    {
        lookDirection = lookDirection.normalized;
    }
	// 보는 방향 수정
 
	// 발사
    isAttacking = Input.GetMouseButton(0);
}
  • 이동은 OnMove()로, 보는 방향 수정은 OnLook() 으로, 발사는 OnFire()로 옮겨주면 됨.
  • 왜냐? SendMessage로 역 호출되기 때문임.
// 변경된 PlayerController.cs
void OnMove()
{
    float horizontal = Input.GetAxisRaw("Horizontal");
    float vertical = Input.GetAxisRaw("Vertical");
    movementDirection = new Vector2(horizontal, vertical).normalized;
}
 
void OnLook()
{
    Vector2 mousePosition = Input.mousePosition;
    Vector2 worldPos = camera.ScreenToWorldPoint(mousePosition);
    lookDirection = (worldPos - (Vector2)transform.position);
 
    if (lookDirection.magnitude < .9f)
    {
        lookDirection = Vector2.zero;
    }
    else
    {
        lookDirection = lookDirection.normalized;
    }
}
 
void OnFire()
{
    isAttacking = Input.GetMouseButton(0);
}
  • 하지만 이건 기존의 방식이랑 동일하니까, 매개변수를 넣음으로써 Input Action을 지대로 사용하자.
// Input System을 쓰는 방식으로 변경된 PlayerController.cs
void OnMove(InputValue inputValue)
{
    movementDirection = inputValue.Get<Vector2>().normalized;
}
 
void OnLook(InputValue inputValue)
{
    Vector2 mousePosition = inputValue.Get<Vector2>();
    Vector2 worldPos = camera.ScreenToWorldPoint(mousePosition);
    lookDirection = (worldPos - (Vector2)transform.position);
 
    if (lookDirection.magnitude < .9f)
    {
        lookDirection = Vector2.zero;
    }
    else
    {
        lookDirection = lookDirection.normalized;
    }
}
 
void OnFire(InputValue inputValue)
{
    isAttacking = inputValue.isPressed;
}
  • 하지만, 발사하려는 게 아니고 UI를 누르려고 한 경우에는 예외처리를 해주어야 하기 때문에 OnFire만 아래 코드로 바꿔줌.
// PlayerController.cs > OnFire()
void OnFire(InputValue inputValue)
{
	// 포인터가 이벤트 시스템을 눌렀을 경우에는 스킵함
    if (EventSystem.current.IsPointerOverGameObject()) return;
    isAttacking = inputValue.isPressed;
}

스탠다드반 OT

  • Firebase는 WebGL과 적용이 안됨.
    • 자바스크립트로 불러와서 C#으로 옮겨야 함.
  • 개발자는 주어진걸 구현하는게 제일 중요하다.
  • 유니티 자체는 쓸 수 있는 분야가 많으니까, 넓은 시야로 보자.
  • 나라장터도 한 번 보자. 어떤 사업이 크게 뜨는지 보임
  • TA쪽은 디자인 패턴을 확실하게 본다. (근데 그거 하나로 취직하긴 힘듬)
  • 게임 업계가 아니더라도 관련 포지션을 해봤다면 좋음. (ex. 플러그인 폴더)
  • 쉬지않고 개발하다보면 기회는 온다.
  • 대표로써 딱 하나만 보고 개발자를 뽑아야 한다면, 본인의 수준을 알고 있는 개발자. 메타인지가 잘 된 개발자.
  • 유나이트나 발표회 같은 컨퍼런스 참여하면 신기술 많이 배울 수 있음.
  • 구글 스토어에 출시하는건 구글이 승인해준걸로 인정됨. 이런 승인 권한이 있는 곳에 출시하면 되긴하는데 분류에 따라 또 달라짐
  • 개발할때 테스트할 방법을 생각해야한다.
  • 깃허브 중요함!!!
  • 개발할때 자기가 어느 한군데 꽂혀서 시간보내고 있다는 사실을 최대한 빠르게 자각하는 방법은 사람마다 다르지만.. 차라리 완전히 빠지는게 나음. 계속 빠져봐야됨. 계속 뻘짓을 해봐야 더 대처능력이 늘게 되기 때문.
  • IOS 빌드용 맥북이 따로 필요함

개인 질문

  • Q. 상용 서버를 공부할만한 방법
    • 가상 윈도우 하나 만들어서 돌려보는게 좋음.
    • 포톤, 파이어베이스 단계별로 넘어가기
    • 파이어베이스 먼저 하기. 60%는 업계에서 사용
    • 어드레서블 ? 검색해보기.
  • Q. 디지털 트윈쪽 회사를 알아보는것도 도움이 크게 될것으로 기대하고 있는데 괜찮을지
    • 아예 신입이 들어가기는 어려움. 건축이나 공장 시스템. 딱딱한 산업분야에서 진행중임.
  • Q. 게임 수학을 공부할때 좋은 책이 있을까요? 아니면 참고자료..?
    • 개발쪽으로 더 하고싶으면 파이어베이스를 먼저 공부해보자!

개인프로젝트 시작

  • 해야되는게 산더미임
  • 일단 맵을 먼저 만들었음. 시간이 없기 때문에, 이번 강의에서 주어진 타일을 재사용 하기로 결정함.

캐릭터 이동

  • 일단 지금 당장은 움직이는게 플레이어밖에 없기 때문에, PlayerController 하나만 만들기로 결정.
  • 그리고 Input Manager 되게 유용한 것 같아서 사용했음.
  • WASD 말고 방향키로도 움직이게 하고싶어서, 그렇게 진행했음.
  • 따라서… 캐릭터 이동 및 맵 탐색은 클리어!

맵 설계 및 상호작용 영역

  • 일단 아이디어는, 각 방에 들어갈 때 마다 무슨 방인지 방 이름이 게임 상단에 나오게 하는것이었음.
  • 내일 하자…

잊지 말자

  1. Instantiate(오브젝트, 위치, 회전) 으로 만들 수도 있다!
  2. transform.right != Vector2.right : tranform.right는 오브젝트의 오른쪽임. 물체의 오른쪽을 설정해주면 나머지 부분 회전도 알아서 들어감

학습하며 겪었던 문제점 & 에러

문제 1

  • 문제&에러에 대한 정의

분명 Input Action 설정하고 붙여줬는데 플레이어가 움직이질 않음.

  • 내가 한 시도
  1. 일단 로그를 찍어서, OnMove()가 호출되는지 확인함 호출되기는 함. 그럼 함수에서 문제인듯
  2. 내부 변수인 MovementDirection을 수정만 하지 대입을 안하는 상태였음.
  3. Movement 함수 작성함
  • 해결 방법 Movement 함수 작성함

  • 이 문제&에러를 다시 만나게 되었다면?

이해하고 베껴오자..

문제 2

  • 문제&에러에 대한 정의

플레이어가 움직이다보면 이따구로 이상하게 뒤집어짐

  • 내가 한 시도

Rotate Z 를 Freeze 해봤음.

  • 해결 방법

그게 정답임!

  • 새롭게 알게 된 점

콜라이더에 부딛치다보면 회전할 수 있으니 주의.

  • 이 문제&에러를 다시 만나게 되었다면?

설정을 제대로 했나 확인해보자..

내일 학습 할 것은 무엇인지

  • 개인프로젝트 필수과제 끝내기..