기록 보관소

[Unity/유니티] 기초-뱀서라이크: 플레이 캐릭터 선택[14] 본문

유니티 프로젝트/뱀서라이크

[Unity/유니티] 기초-뱀서라이크: 플레이 캐릭터 선택[14]

JongHoon 2023. 7. 21. 23:56

개요

유니티 독학을 위해 아래 링크의 골드메탈님의 영상들을 보고 직접 따라 해보면서 진행 상황을 쓰고 배웠던 점을 요약한다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

📚유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com


뱀서라이크: 플레이 캐릭터 선택[14]

1. 캐릭터 선택 UI

캐릭터 선택 UI로 쓸 Character Group 오브젝트 생성 및 변경

  • Grid Layout Group : 자식 오브젝트를 그리드 형태로 정렬하는 컴포넌트

Button Start를 Character Group의 자식 오브젝트로 옮기고, Image를 추가해서 수정. Text와 버튼 이름도 변경.
버튼 Text 수정
Text Name을 복사해 설명을 나타내는 Text로 수정
버튼 이미지 색상 변경
변경된 버튼 색상에 맞춰 Text의 Outline 색상도 변경
캐릭터 선택 버튼을 복사해서 스프라이트, 텍스트, 색상을 변경.


2. 선택 적용하기

//GameManager Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;	//장면 관리(Scene Manager 같은)를 사용하기 위한 네임스페이스.

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public bool isLive;	//시간 정지 여부 확인 변수
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
	[Header("# Player Info")]
	public int playerId;
	public float health;
	public float maxHealth = 100;
    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 LevelUp uiLevelUp;
	public Result uiResult;
	public GameObject enemyCleaner;

    void Awake() {
		instance = this;
	}

	public void GameStart(int id) {
		playerId = id;
		health = maxHealth;

		player.gameObject.SetActive(true);
		uiLevelUp.Select(playerId % 2);
		Resume();
    }

	public void GameOver() {
		StartCoroutine(GameOverRoutine());
	}

	IEnumerator GameOverRoutine() {
		isLive = false;
		
		yield return new WaitForSeconds(0.5f);

		uiResult.gameObject.SetActive(true);
		uiResult.Lose();
		Stop();
	}

    public void GameVictory() {
        StartCoroutine(GameVictoryRoutine());
    }

    IEnumerator GameVictoryRoutine() {
        isLive = false;
		enemyCleaner.SetActive(true);

        yield return new WaitForSeconds(0.5f);

        uiResult.gameObject.SetActive(true);
        uiResult.Win();
        Stop();
    }

    public void GameRetry() {
		SceneManager.LoadScene(0);	//LoadScene() : 이름 혹은 인덱스로 장면을 새롭게 부르는 함수
	}

	void Update() {
		if (!isLive)
			return;

		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
			GameVictory();
		}
	}

	public void GetExp() {
		if (!isLive)	//EnemyCleaner로 경험치를 못얻게 하기 위함
			return;

		exp++;

		if (exp == nextExp[Mathf.Min(level, nextExp.Length - 1)]) {
			level++;
			exp = 0;
			uiLevelUp.Show();
		}
	}

	public void Stop() {
		isLive = false;
		Time.timeScale = 0;
	}

    public void Resume() {
        isLive = true;
        Time.timeScale = 1;	//값이 1보다 크면 그만큼 시간이 빠르게 흐름. 모바일 게임에서 시간 가속하는 것이 이것..
    }
}
//Player Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour {
    public Vector2 inputVec;   //키보드 입력 값 변수
	public float speed; //속도 관리 변수
    public Scanner scanner;
    public Hand[] hands;
    public RuntimeAnimatorController[] animCon;

    Rigidbody2D rigid;
    SpriteRenderer spriter;
    Animator anim;

    void Awake() {
        rigid = GetComponent<Rigidbody2D>();
		spriter = GetComponent<SpriteRenderer>();
        anim = GetComponent<Animator>();
        scanner = GetComponent<Scanner>();
        hands = GetComponentsInChildren<Hand>(true);    //인자의 true를 통해서 비활성화된 오브젝트도 가능
	}

    void OnEnable() {
        anim.runtimeAnimatorController = animCon[GameManager.instance.playerId];
    }

    void FixedUpdate() {
        if (!GameManager.instance.isLive)
            return;

		//위치 이동
		Vector2 nextVec = inputVec * speed * Time.fixedDeltaTime;
		rigid.MovePosition(rigid.position + nextVec);
    }

    void OnMove(InputValue value) {
        inputVec = value.Get<Vector2>();
    }

    void LateUpdate() {
        if (!GameManager.instance.isLive)
            return;

        anim.SetFloat("Speed", inputVec.magnitude); //Magnitude : 벡터의 순수한 크기 값
        
        if (inputVec.x != 0) {
            spriter.flipX = inputVec.x < 0;
        }
    }

    void OnCollisionStay2D(Collision2D collision) {
        if (!GameManager.instance.isLive)
            return;

        GameManager.instance.health -= Time.deltaTime * 10;

        if (GameManager.instance.health < 0) {
            for (int index = 2; index < transform.childCount; index++) {
                transform.GetChild(index).gameObject.SetActive(false);
            }

            anim.SetTrigger("Dead");
            GameManager.instance.GameOver();
        }
    }
}

캐릭터 선택을 위해 Player 오브젝트 비활성화 및 Animation Controller 변수 할당
GameStart 함수에 매개변수를 추가했으므로, Character 0과 Character 1 버튼의 OnClick 이벤트도 다시 설정(각 변수에 맞게)
테스트 실행. 게임 실행시 나온 캐릭터 선택지.
벼농부를 선택한 모습.
보리농부를 선택한 모습. 무기가 다르다.


3. 캐릭터 특성 로직

캐릭터 특성 반영을 위한 스크립트 파일 Character 생성

//Character Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Character : MonoBehaviour {
    public static float Speed {
        get { return GameManager.instance.playerId == 0 ? 1.1f : 1f; }
    }

    public static float WeaponSpeed {
        get { return GameManager.instance.playerId == 1 ? 1.1f : 1f; }
    }

    public static float WeaponRate {
        get { return GameManager.instance.playerId == 1 ? 0.9f : 1f; }
    }

    public static float Damage {
        get { return GameManager.instance.playerId == 2 ? 1.2f : 1f; }
    }

    public static int Count {
        get { return GameManager.instance.playerId == 3 ? 1 : 0; }
    }
}
//Player Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour {
    public Vector2 inputVec;   //키보드 입력 값 변수
	public float speed; //속도 관리 변수
    public Scanner scanner;
    public Hand[] hands;
    public RuntimeAnimatorController[] animCon;

    Rigidbody2D rigid;
    SpriteRenderer spriter;
    Animator anim;

    void Awake() {
        rigid = GetComponent<Rigidbody2D>();
		spriter = GetComponent<SpriteRenderer>();
        anim = GetComponent<Animator>();
        scanner = GetComponent<Scanner>();
        hands = GetComponentsInChildren<Hand>(true);    //인자의 true를 통해서 비활성화된 오브젝트도 가능
	}

    void OnEnable() {
        speed *= Character.Speed;
        anim.runtimeAnimatorController = animCon[GameManager.instance.playerId];
    }

    void FixedUpdate() {
        if (!GameManager.instance.isLive)
            return;

		//위치 이동
		Vector2 nextVec = inputVec * speed * Time.fixedDeltaTime;
		rigid.MovePosition(rigid.position + nextVec);
    }

    void OnMove(InputValue value) {
        inputVec = value.Get<Vector2>();
    }

    void LateUpdate() {
        if (!GameManager.instance.isLive)
            return;

        anim.SetFloat("Speed", inputVec.magnitude); //Magnitude : 벡터의 순수한 크기 값
        
        if (inputVec.x != 0) {
            spriter.flipX = inputVec.x < 0;
        }
    }

    void OnCollisionStay2D(Collision2D collision) {
        if (!GameManager.instance.isLive)
            return;

        GameManager.instance.health -= Time.deltaTime * 10;

        if (GameManager.instance.health < 0) {
            for (int index = 2; index < transform.childCount; index++) {
                transform.GetChild(index).gameObject.SetActive(false);
            }

            anim.SetTrigger("Dead");
            GameManager.instance.GameOver();
        }
    }
}
//Gear Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Gear : MonoBehaviour {
    public ItemData.ItemType type;
    public float rate;

    public void Init(ItemData data) {
        // Basic Set
        name = "Gear " + data.itemId;
        transform.parent = GameManager.instance.player.transform;
        transform.localPosition = Vector3.zero;

        // Property Set
        type = data.itemType;
        rate = data.damages[0];
        ApplyGear();
    }

    public void LevelUp(float rate) {
        this.rate = rate;
        ApplyGear();
    }

    void ApplyGear () { //장비가 새롭게 추가되거나 레벨업 할때 호출
        switch (type) {
            case ItemData.ItemType.Glove:
                RateUp();
                break;
            case ItemData.ItemType.Shoe:
                SpeedUp();
                break;
        }
    }

    void RateUp() {
        Weapon[] weapons = transform.parent.GetComponentsInChildren<Weapon>();

        foreach (Weapon weapon in weapons) {
            switch (weapon.id) {
                case 0:
                    float speed = 150 * Character.WeaponSpeed;
                    weapon.speed = speed + (speed * rate);
                    break;
                default:
                    speed = 0.5f * Character.WeaponRate;
                    weapon.speed = speed * (1f - rate);
                    break;
            }
        }
    }

    void SpeedUp() {
        float speed = 3 * Character.Speed;
        GameManager.instance.player.speed = speed + speed * rate;
    }
}
//Weapon Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Weapon : MonoBehaviour {
    public int id;
    public int prefabId;
    public float damage;
    public int count;
    public float speed;

    float timer;
    Player player;

    void Awake() {
        player = GameManager.instance.player;
    }

    void Update() {
        if (!GameManager.instance.isLive)
            return;

        switch (id) {
            case 0:
                transform.Rotate(Vector3.back * speed * Time.deltaTime);
                break;
            default:
                timer += Time.deltaTime;

                if (timer > speed) {
                    timer = 0f;
                    Fire();
                }
                break;
        }

        // .. Test Code ..
        if (Input.GetButtonDown("Jump")) {
            LevelUp(10, 1);
        }
    }
    
    public void LevelUp(float damage, int count) {
        this.damage = damage * Character.Damage;
        this.count += count;

        if (id == 0)
            Batch();

        player.BroadcastMessage("ApplyGear", SendMessageOptions.DontRequireReceiver);
    }

    public void Init(ItemData data) {
        // Basic Set
        name = "Weapon " + data.itemId;
        transform.parent = player.transform;
        transform.localPosition = Vector3.zero;

        // Property Set
        id = data.itemId;
        damage = data.baseDamage * Character.Damage;
        count = data.baseCount + Character.Count;

        for (int index = 0; index < GameManager.instance.pool.prefabs.Length; index++) {
            if (data.projectile == GameManager.instance.pool.prefabs[index]) {  //프리펩이 같은건지 확인
                prefabId = index;
                break;
            }
        }

        switch (id) {
            case 0:
                speed = 150 * Character.WeaponSpeed;
                Batch();
                break;
            default:
                speed = 0.5f * Character.WeaponRate;
                break;
        }

        // Hand Set
        Hand hand = player.hands[(int)data.itemType];
        hand.spriter.sprite = data.hand;
        hand.gameObject.SetActive(true);

        //플레이어가 가지고 있는 모든 Gear에 대해 ApplyGear를 실행하게 함. 나중에 추가된 무기에도 영향을 주기 위함.
        player.BroadcastMessage("ApplyGear", SendMessageOptions.DontRequireReceiver);
    }

    void Batch() {  //생성된 무기를 배치하는 함수
        for (int index = 0; index < count; index++) {
            Transform bullet;

            if (index < transform.childCount) {
                bullet = transform.GetChild(index); //기존 오브젝트가 있으면 먼저 활용
            }
            else {
                bullet = GameManager.instance.pool.Get(prefabId).transform; //모자라면 풀링에서 가져옴
                bullet.parent = transform;
            }

            bullet.localPosition = Vector3.zero;        //무기 위치 초기화
            bullet.localRotation = Quaternion.identity; //무기 회전값 초기화

            Vector3 rotVec = Vector3.forward * 360 * index / count; //개수에 따라 360도 나누기
            bullet.Rotate(rotVec);
            bullet.Translate(bullet.up * 1.5f, Space.World); //무기 위쪽으로 이동
            bullet.GetComponent<Bullet>().Init(damage, -1, Vector3.zero); //-1 is Infinity Per. 무한 관통.
        }
    }

    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);   //FromToRotation(지정된 축을 중심으로 목표를 향해 회전하는 함수
        bullet.GetComponent<Bullet>().Init(damage, count, dir);
    }
}

테스트 실행. Player의 Speed가 3으로 설정되어있다.
벼농부를 선택하자 Speed가 3.3으로 10% 증가했다.
보리 농부 비교를 위해서 레벨업을 해 엽총을 얻었다. 엽총의 기본 연사 속도는 0.5.
보리 농부를 선택해서 무기를 확인해보니 0.45로 10% 빠른것을 확인할 수 있다.