BossController.cs로 스크립트를 만들어 Boss 폴더에 저장합고 Boss에 어태치합니다.

BossController.cs
0.00MB

변수

hp나 반응거리는 적캐릭터와 같습니다. 발사하는 총알의 속성도 같습니다. inAttack변수는 공격중인지 여부를 판단하는 값입니다.

// 체력
public int hp = 10;
// 반응 거리
public float reactionDistance = 7.0f;

public GameObject bulletPrefab;     //총알
public float shootSpeed = 5.0f;     //총알 속도

//공격중인지 여부
bool inAttack = false;

Update()

"Player" Tag로 Player를 찾아서 공격범위이면 inAttack=true, 공격 애니메이션 Play("BossAttack")등 공격처리를 합니다. 

void Update() {
    if (hp > 0)  {
        //Player 게임 오브젝트 가져오기
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player != null) {
            //플레이어와의 거리 확인
            Vector3 plpos = player.transform.position;
            float dist = Vector2.Distance(transform.position, plpos);
            if (dist <= reactionDistance && inAttack == false)  {
                //범위 안 & 공격 중이 아니면 공격 애니메이션
                inAttack = true;
                // 애니메이션 변경
                GetComponent<Animator>().Play("BossAttack");
            }  else if (dist > reactionDistance && inAttack) {
                inAttack = false;
                // 애니메이션 변경
                GetComponent<Animator>().Play("BossIdle");
            }
        } else {
            inAttack = false;
            // 애니메이션 변경
            GetComponent<Animator>().Play("BossIdle");
        }
    }
}

OnCollisionEnter2D()

화살과 충돌하면 hp--처리하고 사망했다면 Play("BossDead")후 Destry()로 지우는 사망처리를 합니다.

private void OnCollisionEnter2D(Collision2D collision) {
    if (collision.gameObject.tag == "Arrow")  {
        //데미지
        hp--;
        if (hp <= 0)  {
            //사망!
            //충돌 판정 비활성
            GetComponent<CircleCollider2D>().enabled = false;
            // 애니메이션 변경
            GetComponent<Animator>().Play("BossDead");
            //1초 뒤에 제거
            Destroy(gameObject, 1);
        }
    }
}

Attack()

플레이어와의 각도를 구해 총알을 발사합니다.

void Attack()  {
    //발사 위치 오브젝트 가져오기
    Transform tr = transform.Find("gate");
    GameObject gate = tr.gameObject;
    //총알을 발사할 벡터 만들기
    GameObject player = GameObject.FindGameObjectWithTag("Player");
    if (player != null) {
        float dx = player.transform.position.x - gate.transform.position.x;
        float dy = player.transform.position.y - gate.transform.position.y;
        //아크 탄젠트2 함수로 각도(라디안) 구하기
        float rad = Mathf.Atan2(dy, dx);
        //라디안을 각도로 변환
        float angle = rad * Mathf.Rad2Deg;
        //Prefab으로 총알 오브젝트 만들기(진행 방향으로 회전)
        Quaternion r = Quaternion.Euler(0, 0, angle);
        GameObject bullet = Instantiate(bulletPrefab, gate.transform.position, r);
        float x = Mathf.Cos(rad);
        float y = Mathf.Sin(rad);
        Vector3 v = new Vector3(x, y) * shootSpeed;
        //발사
        Rigidbody2D rbody = bullet.GetComponent<Rigidbody2D>();
        rbody.AddForce(v, ForceMode2D.Impulse);
    }
}

 

애니메이션 이벤트 설정하기

씬뷰의 보스 캐릭터를 선택하고 애니메이션 창을 엽니다.  팝업메뉴에서 BossAttack를 선택하고 타임라인을 클릭후 이벤트추가▮+를 클릭하면  이벤트가 추가되고 추가된 이벤트를 선택하면 게임오브젝트에 어태치된 메서드 Attack()을 선택할 수 있습니다.

이제 애니메이션이 해당 프레임에 도달하면 Attack()메소드가 호출되면서 총알이 발사 됩니다.

Boss오브젝트  BossController.cs 컴포넌트의 Bullet Prefab에 프로젝트뷰 Boss폴더의 Bullet Prefab을 끌어다 놓습니다.

 

게임 실행하기

보스캐릭터가 플레이어 캐릭터를 향해 총알을 발사 합니다. 총알속도 반응 거리는 스크립트에서 설정할수 있습니다.

보스캐릭터를 Boss 폴더로 끌어다 프리팹으로 만듭니다.

 

BulletController.cs
0.00MB

보스 캐릭터가 발사하는 총알을 만들어봅니다. 기본적으로 포탄이나 화살과 같습니다.

Images폴더의 Bullet 이미지를 선택하고 Pixel Per Unit:32, Filter Mode: Point(no filter)로 설정합니다.

씬뷰로 끌어서 게임오브젝트를 만듭니다. 

Tag:Enemy, Layer:Bullet으로 설정합니다. 없으면 만들어서 합니다.

Order in Layer를 3으로 설정합니다. Rigidbody2D를 어태치하고 Gravity Scale을 0으로 합니다 CircleCollider2D를 어태치하고 EditCollider를 눌 범위를 조정합니다.

 총알의 접촉 설정

보스 캐릭터와 총알이 접촉하지 않도록 Proejct Settings > Physics2D탭의 LayerCollisionMatrix에서 Enemy와 Bullet이 교차하는 부분의 체크를 해제합니다. Bullet과 Bullet도 체크해제 합니다.

총알을 제어하기 위한 스크립트 만들기

스크립트를 하나 만들고 BulletController로 이름 변경하고 bullet에 어태치합니다.

스크립트는 간단합니다. 발사후 일정시간후 자신을 제거하거나 어딘가 충돌해도 제거합니다.

 

Start()

설정된 시간후 자신을 제거합니다.

void Start() {
    Destroy(gameObject, deleteTime);    //제거 설정
}

OnCollision2D()

접촉이 발생하면  자신을 제거합니다.

private void OnCollisionEnter2D(Collision2D collision)  {
    Destroy(gameObject);   //접촉이 발생하면 즉시 제거
}

 

스테이지 마지막에 싸울 보스 캐릭터와 보스 스테이지를 위한 씬을 만들어봅시다. 보스 캐릭터는 다음과 같이 동작합니다.

플레이어 캐릭터와 접촉하면 플레이어가 대미지를 받는다.

플레이어 캐릭터가 접근하면 총알을 발사해 공격한다.

HP를 설정할 수 있다. 화살을 일정량 맞으면 쓰러진다.

보스 캐릭터와 관련된 데이터는  Boss폴더를 만들고 저장합니다. 사용할 이미지도 Boss 폴더로 옮겨둡니다. 지금 바로 Boss폴더를 만듭니다.

 

보스 캐릭터 게임 오브젝트 만들기

먼저 보스 캐릭터의 이미지 에셋을 준비합니다. Boss가 보스 캐릭터용 이미지입니다.

Boss_0~1:대기, Boss_2~3:공격 ,Boss_4: 사망 애니메이션입니다.

보스 캐릭터의 애니메이션 만들기

대기애니메이션 만들기

Boss_0~1을 씬뷰에 끌어다 놓으면 애니메이션 데이터를 만듭니다. 클립 이름을 BossIdel로 합니다. 배치한 보스 캐릭터 게임 오브젝트의 이름은 Boss, 애니메이션 컨트롤러의 이름은 BossAnime로 이름을 변경합니다.

Sprite Renderer의 Order in Layer를 2로 설정하고, Circle Collider2D를 어태치 합니다. 보스 캐릭터는 움직이지 않으니 Rigidbody2D는 쓰지 않습니다. 보스 캐릭터에 Enemy태그와 Enemy Layer를 만들어 적용합니다. 나중에 보스가 발사하는 총알의 충돌 판정에 사용합니다.

CreateEmpty를 하나만들어 gate로 이름 바꾸고 Boss의 자식으로 하고 가슴쯤에 배치합니다. 씬뷰에서 구별하기 쉽게 칼라아이콘을 설정합니다. 총알이 발사될 위치로 사용합니다.

BossIdle 애니메이션을 열고 하이라키의 Boss를 선택한후 Samples를 2로 설정합니다. ▶클릭해서 확인해봅니다.

공격 애니메이션

Animation창에서 BossIdel을 클릭하고 Create New Clip을 선택하고 창이 뜨면 이름은 BossAttack으로 합니다.

Boss_2~3을 끌어 씬 뷰에 끌어다 애니메이션 클립을 만듭니다. 

Sample을 4로 합니다. 약간 포즈를 취한뒤 공격을 하기 위해 0프레임의 키프레임을 카피해 그 다음 프레임어 넣어줍니다.

 

사망 애니메이션

Boss_4 이미지를 이용해 사망애니메이션을 만들고 이름을 BossDead로 합니다. Sprite Renderer.Color Property를 추가하가 마지막 프레임은 알파값이 0이 되도록 저장합니다.

 

애니메이션 컨트롤러

BossAnime을 열고 3개의 애니메이션이 다 있는지 확인합니다. 없으면 끌어다 놓습니다.

 

 

9.1 씬에서 씬으로 이동하기

레벨의 출입구와 연동해서 레벨을 이동하도록 합니다.   RoomManager라는 폴더를 만들어 관련 리소스를 저장합니다.

 

출입구 게임 오브젝트와 스크립트 만들기

플레이어가 접촉하면 씬을 이동하기 위한 게임 오브젝트와 스크립트를 만듭니다.

WorldMap 씬을 로딩합니다.

플레이어 프리팹을 배치합니다.

계층 뷰의 + > Create Empty를 클릭하고 이름을 Exit로 하고 알기쉽게 칼라아이콘을 붙입니다. Exit라는 Tag도 만들어 적용합니다.

CircleCollider2D 컴포넌트를 추가하고 타일에 맞게 크기를 조금 작게 합니다. 크기는 EditCollider를 클릭후 원위의 점을 끌면 됩니다.  IsTrigger를 체크합니다. 출입구 타일 위에 배치합니다. 칼라아이콘을 적용해 아이콘위에 이름이 보여 쉽게 식별이 가능합니다.

RoomManager폴더에 다음과 같이 Exit 스크립트를 만들어 저장합니다.

Exit.cs
0.00MB

enum ExitDirection

ExitDirection라는 enum Type 에 4개의 방향을 정의했습니다. 방향을 0,1,2,3같은 숫자로 사용하는 것보다 직관적입니다.

//출입구 위치
public enum ExitDirection
{
    right,  //오른쪽
    left,   //왼쪽
    down,   //아래쪽
    up,     //위쪽
}

변수

이동할 씬, 씬의 문번호, 캐릭터가 바라볼 방향을 뜻합니다.

public string sceneName = "";   //이동할 씬 이름
public int doorNumber = 0;      //캐릭터가 위치할 문 번호
public ExitDirection direction = ExitDirection.down;//문에서 나온 캐릭터가 바라보는 방향

OnTriggerEnter2D()

충돌이 일어나고 Tag가 Player라면 정해진 씬과 doorNumber를 전달하고 ChangeScene()을 호출한다.

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.gameObject.tag == "Player")
    {
        RoomManager.ChangeScene(sceneName, doorNumber);
    }
}

 

방을 관리하는 게임 오브젝트 만들기

방을 관리하는 게임오브젝트와 클래스를 만듭니다. 빈오브젝트를 씬에 배치하고 이름을 RoomManager로 합니다.

RoomManager 스크립트를 RoomManager 폴더에 만들고 씬뷰의 RoomManager오브젝트에 어태치합니다.

RoomManager.cs
0.00MB

선언

씬을 처리하기 위해 선언합니다.

using UnityEngine.SceneManagement;

변수

doorNumber를 static으로 선업합니다. 그래야 씬이 바뀌어도 유지됩니다.

// static 변수
public static int doorNumber = 0;   //문 번

Start()

Exit 라는 Tag를 전부찾아 enters라는 배열에 저장한다

c#에서는 Type[] name; 으로 배열을 선언한다.

배열에서 GameObject를 꺼내 Exit 컴포넌트의 doorNumber와 static doorNumber를 비교해서 찾은 door의 위치에 플레이어를 배치하고 도어의 뚫린 방향으로 이동시켜준다. 그래야 조금 나와 있는것 처럼 보이니까.

void Start() {
    //플레이어 캐릭터 위치
    //출입구를 배열로 얻기
    GameObject[] enters = GameObject.FindGameObjectsWithTag("Exit");
    for (int i = 0; i < enters.Length; i++)  {
        GameObject doorObj = enters[i]; //배열에서 꺼내기
        Exit exit = doorObj.GetComponent<Exit>();   //Exit 클래스 변수
        if (doorNumber == exit.doorNumber)  {
            //==== 같은 문  번호 ====
            //플레이어 캐릭터를 출입구로 이동
            float x = doorObj.transform.position.x;
            float y = doorObj.transform.position.y;
            if (exit.direction == ExitDirection.up)  {
                y += 1;
            }  else if (exit.direction == ExitDirection.right)  {
                x += 1;
            } else if (exit.direction == ExitDirection.down) {
                y -= 1;
            }  else if (exit.direction == ExitDirection.left) {
                x -= 1;
            }
            GameObject player = GameObject.FindGameObjectWithTag("Player");
            player.transform.position = new Vector3(x, y);
            break;  //반복문 빠나오기
        }
    }
}

public static void ChangeScene()

doornum을 doorNumber에 저장하고 scenename의 씬을 호출합니다. 

ChangeScene()은  외부에서 static doorNumber를 변경시키므로 public static 으로 선언됩니다.

public static void ChangeScene(string scnename, int doornum)
{
    doorNumber = doornum;   //문 번호를 static 변수에 저장

    //string nowScene = PlayerPrefs.GetString("LastScene");
    //if (nowScene != "")
    //{
    //    SaveDataManager.saveArrangeData(nowScene);      //配置データを保存
    //}
    //PlayerPrefs.SetString("LastScene", scnename);   //シーン名を保存
    //PlayerPrefs.SetInt("LastDoor", doornum);        //ドア番号保存
    //ItemKeeper.SaveItem();                          //アイテムを保存

    SceneManager.LoadScene(scnename);   //シーン移動
}

여기까지 완료한 RoomManager와 Exit를 RoomManager폴더로 끌어 프리팹으로 만듭니다. (예제폴더를 사용하시면 이미 만들어져 있습니다.)

 

출입구 배치하기

레벨의 출입구에 Exit프리팹을 배치하고 스크립트에 SceneName을 적고 Door Number를 순서대로 0,1,2... 으로 적습니다. direction은 플레이어가 나갈 방향을 정합니다.

 

문 만들기

출입구를 막는 문을 만듭니다. 열쇠가 있다면 열립니다. 문은 나중에 만들 Item과 비스하므로 Item폴더를 만들고 저장합니다.

Item.zip
0.02MB

문 이미지는 Door입니다. Door 이미지도 Item폴더로 옮깁니다. 32x32픽셀의 도트 이미지이므로 Pixel Per Unit를 32로 하고 Filter Mode를 Point로 합니다.

이미지 설정후 Door를 씬뷰로 끌어다 놓고 BoxCollider2D와 Order In Layer 1로 하고 Door Tag를 만들어 적용합니다.

열쇠를 가지고 있으면 열수 있도록 스크립트를 만들어 봅니다.

Door 스크립트를 Item폴더에 저장하고 Door오브젝트에 어태치합니다.

Door.cs
0.00MB

변수

public int arrangeId = 0;       //식별에 사용하기 위한 값

OnCollisionEnter2D(Collision2D collision)

"Player"와 충돌이 있으면 열쇠를 하나 감소 시키고 문을 열어 줍니다.

void OnCollisionEnter2D(Collision2D collision)
{
    if (collision.gameObject.tag == "Player")
    {
        //열쇠를 가지고있으면
        if (ItemKeeper.hasKeys > 0)
        {
            ItemKeeper.hasKeys--;       //열쇠를 하나 감소
            Destroy(this.gameObject);   //문 열기
        }
    }
}

 

Door를 배치합니다.

Door를 배치하면 키가 없으면 들어갈수 없습니다. 테스트로 ItemKeeper 스크립트의 변수를 바꿔주면

public static int hasKeys = 1;          //열쇠 수
public static int hasArrows = 10;        //화살 소지수

처음 한번은 통과하지만 dungeon1으로 갔다 다시 와서 들어가려고 하면 키가 없어 못들어가는걸 확인할 수 있습니다.

 

dungeon1씬에도 Exit배치

dungeon1 씬에도  Exit를 배치하고 스크립트에 WorldMap과 방향을 down으로 하면 dungeon1에서 WorldMap으로도 이동할 수 있습니다.

플레이어 캐릭터는 활을 겨누면서 360도 방향을 자유롭게 이동합니다. 활은 진행하는 방향을 향하게 되고 진행 방향으로 발사할수 있습니다. 발사된 활은 물체에 닿으면 그 자리에 박힌후 잠시 뒤 사라집니다.

 

멀티 스프라이트로 캐릭터 이미지 만들기

프로젝트뷰에 Player폴더를 만듭니다.  교재를 다운 받으면 이미 폴더가 만들어져 있습니다.

Player.zip
0.03MB

Player폴더의 PlayerImages를 엽니다. 패턴은 32x32 픽셀의 도트이미지 입니다. 타일맵과 동일하게 멀티 스프라이트로 만듭니다. Sprite Mode:Multiple, Pixels Per Unit: 32, Filter Mode: Point 아까 멀티맵처럼 설정합니다.

Sprite Editor를 클릭해 [Slice]탭을 선택하고 Pixel Size:32x32 Pivot:Custom, Custom Pivot x:0.5 Y:0.2로 약간 아래로 내립니다.

화살과 활은 아래와 같이 이동중심으로 옮겨 줍니다.

화살은 Center로 합니다.

설정이 완료되면 위쪽의 APPLY를 클릭합니다.

 

플레이어 캐릭터의 게임 오브젝트 만들기

플레이어의 상하좌우 이동 애니메이션을 만들어야 합니다. 우선 분할된 이미지의 앞 2개를 선택해서 씬뷰로 끌어줍니다.

이름을 PlayerDown 으로 Player 폴더에 저장합니다.

배경 타일맵 보다 위에 보이도록 Order in Layer값을 3으로 설정합니다. 

여기까지 작업후 저장된 애니메이터 컨트롤러 이름을 PlayerAnime으로 합니다. 게임오브젝트의 이름도 Player로 합니다.

* 저는 그냥 Player폴더에 있는 player프리팹을 끌어다 놨습니다. 모든게 이미 설정되어져 있습니다.

플레이어 캐릭터에 Rigidbody2D를 어태치하고 Gravity를 0, Freeze Rotation Z를 체크,

Circle Collider2D를 어태치합니다. 범위를 전체의 반정도로 조정합니다.  Player Tag를 설정하고 Player 레이어를 만들어 설정합니다.  

PlayerDown 애니메이션을 열어 Samples를 4로 설정합니다.

같은 방법으로 PlayerUp(Image2~3), PlayerLeft(Image4~5), PlayerRight(Image6~7)를 만듭니다.

생성된 게임오브젝트는 애니메이션 클립을 만들기 위한 것이므로 실제 사용하지 않습니다. 애니메이션 클립을 저장한 후에는 PlayerUp, PlayerLeft, PlayerRight 애니메이션 컨트롤러와 게임오브젝트는 제거됩니다.

방금만든 PlayerUp, PlayerLeft, PlayerRight 애니메이션을 Animator에 끌어다 놓습니다.

게임오버 애니메이션 만들기

이제 게임오버 애니메이션을 만듭니다.

하이라키에서 Player를 선택하고 애니메이션 창을 엽니다. Crate NewClip을 선택해 PlayerDead라는 이름을 애니메이션 클립을 추가합니다.

[Add Propety] 버튼을 클릭하고 Sprite Renderer > Sprite를 선택해 Sprite를 추가합니다. 빨간색 기록버튼을 누르고 키 프레임으로 등록된 이미지 두개를 PlayerImage_8로 변경합니다. 

Sprite Renderer.Color를 추가하고 마지막 프레임의 Color.a를 0으로 설정합니다.

플레이어 이동 스크립트 만들기

PlayerController라는 스크립트를 Player폴더에 만들고 씬뷰의 Player에 어태치합니다. 7장에서 만든 버추얼패드의 스크립트를 다시사용하고자 이름을 똑같이 했습니다. 

PlayerController.cs
0.01MB

변수

과거 애니메이션과 현재 애니메이션의 이름이 다르면 애니메이션을 변경하는 처리를 합니다.

public float speed = 3.0f;     //이동 속도
//애니메이션 이름
public string upAnime = "PlayerUp";         // 위
public string downAnime = "PlayerDown";     // 아래
public string rightAnime = "PlayerRight";   // 오른쪽
public string leftAnime = "PlayerLeft";     // 왼쪽 
public string deadAnime = "PlayerDead";     // 사망

string nowAnimation = ""; //현재 애니메이션
string oldAnimation = ""; //이전 애니메이션

float axisH;                    //가로 축 값(-1.0 〜 0.0 〜 1.0)
float axisV;                    //세로 축 값(-1.0 〜 0.0 〜 1.0)
public float angleZ = -90.0f;   //회전 각

Rigidbody2D rbody;              //Rigidbody 2D
bool isMoving = false;          //이동 중인지 여부

//데미지 처리
public static int hp = 3;           //플레이어 HP
public static string gameState;     //게임 상태
bool inDamage = false;              //데미지 받는 중인지 여부

Start()

Rigidbody2D 컴포넌트 참조를 저장하고, oldAnimation을 downAnime으로 하고 gameState를 "playing"으로 설정합니다.

// Use this for initialization
void Start()
{
    //Rigidbody2D 가져오기
    rbody = GetComponent<Rigidbody2D>();
    //애니메이션
    oldAnimation = downAnime;
    //게임 상태를 플레이 중으로 변경
    gameState = "playing";
}

Update()

키입력으로 방향을 정하고 애니메이션을 결정

// Update is called once per frame
void Update()  {
    //게임중이 아니거나 데미지 받는 중엔 아무것도 하지 않음
    if (gameState != "playing" || inDamage)  {
        return;
    }
    if (isMoving == false)  {
        axisH = Input.GetAxisRaw("Horizontal"); //좌우 키 입력
        axisV = Input.GetAxisRaw("Vertical");   //상하 키 입력
    }
    //키 입력으로 이동 각도 구하기
    Vector2 fromPt = transform.position;
    Vector2 toPt = new Vector2(fromPt.x + axisH, fromPt.y + axisV);
    angleZ = GetAngle(fromPt, toPt);
    //이동 각도에서 방향과 애니메이션 변경
    if (angleZ >= -45 && angleZ < 45)   {
        //오른쪽
        nowAnimation = rightAnime;
    }  else if (angleZ >= 45 && angleZ <= 135)   {
        //위쪽
        nowAnimation = upAnime;
    }   else if (angleZ >= -135 && angleZ <= -45)  {
        //아래쪽
        nowAnimation = downAnime;
    }  else {
        //왼쪽
        nowAnimation = leftAnime;
    }
    // 애니메이션 변경하
    if (nowAnimation != oldAnimation) {
        oldAnimation = nowAnimation;
        GetComponent<Animator>().Play(nowAnimation);
    }
}

FixedUpdate()

 inDamage상태이면 SpriteRendere.enable = true, false로 변경하면서 점멸시킨다. 이동속도도 변경시킨다.

void FixedUpdate()  {
    //게임 중이 아니면 아무것도 하지 않음
    if (gameState != "playing")   {
        return;
    }
    if (inDamage)  {
        //데미지 받는 중엔 점멸 시키기
        float val = Mathf.Sin(Time.time * 50);
        Debug.Log(val);
        if (val > 0)   {
            //스프라이트 표시
            gameObject.GetComponent<SpriteRenderer>().enabled = true;
        } else {
            //스프라이트 비표시
            gameObject.GetComponent<SpriteRenderer>().enabled = false;
        }
        return; //데미지 받는 중엔 조작을 할수 없게하기
    }
    //이동 속도 변경하기
    rbody.velocity = new Vector2(axisH, axisV) * speed;
}

SetAxis() 

버추얼패드에서 호출해서 키보드대신 axisH와 axisV를 설정해줍니다.

public void SetAxis(float h, float v) {
    axisH = h;
    axisV = v;
    if (axisH == 0 && axisV == 0)   {
        isMoving = false;
    }   else {
        isMoving = true;
    }
}

GetAngle()

p1,p2사이의 각도를 Atan2를 이용해 구합니다.

float GetAngle(Vector2 p1, Vector2 p2) {
    float angle;
    if (axisH != 0 || axisV != 0) {
        //이동 중이면 각도를 변경
        //p1과 p2차이 구하기(원점을 0으로 하기 위해)
        float dx = p2.x - p1.x;
        float dy = p2.y - p1.y;
        //아크 탄젠트 함수로 각도(라디안) 구하기
        float rad = Mathf.Atan2(dy, dx);
        //라디안 각으로 변환하여 반환
        angle = rad * Mathf.Rad2Deg;
    }   else {
        //정지중이면 이전 각도를 유지
        angle = angleZ;
    }
    return angle;
}

OnCollisionEnter2D() 

Enemy와 충돌하면 대미지처리를 합니다.

private void OnCollisionEnter2D(Collision2D collision)
{
    if (collision.gameObject.tag == "Enemy")
    {
        GetDamage(collision.gameObject);
    }
}

GameOver()

플레이어의 hp가 0이되면 GameOver()처리를 합니다.

void GameOver()
{
    Debug.Log("게임 오버!");
    gameState = "gameover";
    //=====================
    //게임 오버 연출
    //=====================
    //플레이어 충돌 판정 비활성
    GetComponent<CircleCollider2D>().enabled = false;
    //이동 중지
    rbody.velocity = new Vector2(0, 0);
    //중력을 적용하여 플레이어를 위로 튀어오르게하는 연출
    rbody.gravityScale = 1;
    rbody.AddForce(new Vector2(0, 5), ForceMode2D.Impulse);
    // 애니메이션 변경하기
    GetComponent<Animator>().Play(deadAnime);
    //1초후에 플레이어 캐릭터 제거하기
    Destroy(gameObject, 1.0f);
}

Player에 어태치해서 실행해보면 WASD키를 누르면 상하좌우로 잘 움직입니다.  교재에는 활을 표시해주는  Arrow Shoot 스크립트가 어태치되어 있어 활도 표시되었습니다 이건 뒤에서 공부합니다.

 

밝으면 죽는 대미지 블록을 만듭니다. 새로운 게임오브젝트를 만들어 "Dead" Tag를 붙여주면 됩니다.

  • 대미지 블록의 외관은 바늘
  • 양 옆에서 접축하면 대미지 없이 충돌
  • 위에서 접촉시만 대미지 (게임오버)

 

바늘 대미지 블록 만들기

대미지 블록을 위한 바늘 이미지는 에셋인 needle을 씬뷰나 하이라키고 드래그 드롭으로 추가합니다.

Sprite Render컴포넌트의 Order in Layer값을 2로 합니다.

인스펙터 뷰에서 Add Component>Physics 2D를 선택한 후 Box Collider 2D를 두개 어태치해 양옆으로 벽처럼 배치합니다.  플레이어의 진행을 벽처럼 막을 목적이므로 isTrigger는 체크하지 않습니다.(체크하면 통과해버립니다.)

하이라키뷰에서 needle의 자식으로 Create Empty한후 이름을 DeadZone으로 하고 BoxCollider 2D를 추가하고 isTrigger를 체크합니다. 범위는 가운데쪽으로 해서 위에서 떨어지는 접촉을 감지합니다.

needle 을 Prefab폴더로 끌어다 prefab으로 만듭니다.

 

낙하하는 대미지 블록 만들기

images폴더에서 block1x1을 씬뷰에 끌어다 이름을 GimmickBlock으로 변경합니다. Sprite Renderer컴포넌트의 Order in Layuers는 2로 변경, Layer값은 Ground로 해서 캐릭터가 올라탈수 있도록 합니다.

Box Collider 2D를 추가하고 아래쪽을 살짝 올려줍니다.

지면과의 접촉은 CircleCollider2D를 추가합니다. 지면과의 마찰을 줄여 매끄럽게 이동하기 위해서입니다.   플레이어가 밀수 있게 Rigidbody2D를 추가하고 넘어지지 않게 Freeze Rotation Z를 체크하고 25로 설정합니다.

떨어지다가 플레이어와의 접촉을 담당할 새로운 빈오브젝트를 만들어 이름을 DeadZone으로 바꾸고 GimmickBlock의 자식으로 만들고 Tag를 Dead로 설정합니다.  Box Collider 2D를 추가하고 위치를 아래로 약간 내리고 IsTrigger를 체크해서 플레이어의 검출을 담당합니다.

장애물 블록과 식별을 용이하게 하기 위해 컬러아이콘을 붙입니다. 인스펙터뷰 맨위좌측을 눌러보면 아이콘을 선택할 수 있습니다. 부탁후 씬쀼에서 이름을 확인할 수 있습니다. 게임중에는 표시되지 않습니다.

공중에 배치하면 낙하하고 플레이어와 충돌하면 게임 오버가 되면 지면에 배치하면 플레이어가 밀어서 움직일 수 있고 올라탈수 있는 블록이 완성되었습니다.

 

대미지 블록을 낙하시키는 스크립트 만들기

GimmickBlock에 다음 기능을 추가하는 스크립트를 추가해봅니다.

플레이어가 일정 거리 안에 들어가면 떨어진다.

낙하 후에는 점점 사라진다.(변수로 시간을 정의할 수 있음)

GimmickBlock.cs의 코드 입니다.

변수 : 낙하와 페이드아웃 시간을 정의하고 있습니다.

public float length = 0.0f;     // 자동 낙하 탐지 거리
public bool isDelete = false;   // 낙하 후 제거할지 여부

bool isFell = false;            // 낙하 플래그
float fadeTime = 0.5f;          // 페이드 아웃 시간

Start()  : 게임 시작시 낙하할 블록을 Static으로 만들어 못 움직이게 만듭니다.

void Start() {
    // Rigidbody2D 물리연동 정지
    Rigidbody2D rbody = GetComponent<Rigidbody2D>();
    rbody.bodyType = RigidbodyType2D.Static;
}

Update() : 플레이어와의 거리를 계산해서 bodyType을 Dynamic으로 바꿔 떨어지게 만듭니다.

블록이 바닥에 떨어져 있는 상태라면 faceTiem을 카운트다운하면서 서서히 투명도를 바꿔줍니다. 0이 되면 제거합니다.

void Update(){
        GameObject player = GameObject.FindGameObjectWithTag("Player"); // 플레이어 찾기
        if (player != null) {
            // 플레이어와의 거리 계산
            float d = Vector2.Distance(transform.position, player.transform.position);
            if (length >= d) {
                Rigidbody2D rbody = GetComponent<Rigidbody2D>();
                if (rbody.bodyType == RigidbodyType2D.Static) {
                    // Rigidbody2D물리 연동 시작
                    rbody.bodyType = RigidbodyType2D.Dynamic;
                }
            }
        }
        if (isFell){
            // 낙하
            // 투명도를 변경해여 페이드 아웃 효과
            fadeTime -= Time.deltaTime; // 이전 프레이과의 차이만큼 시간 차감
            Color col = GetComponent<SpriteRenderer>().color;   // 컬러 값 가져오기
            col.a = fadeTime;   // 투명도 변경
            GetComponent<SpriteRenderer>().color = col; // 컬러 값을 재설정
            if (fadeTime <= 0.0f){            
                Destroy(gameObject); // 0보다 작으면(투명)제거
            }
        }
    }

OnCollisionEnter2D() 충돌은 바닥과 생기므로 오브젝트 검사는 하지 않습니다. 바닥에 떨어지면 isFell이 true로 바꿔줍니다.

void OnCollisionEnter2D(Collision2D collision) {
    if (isDelete){
        isFell = true; // 낙하 플래그 true
    }
}

스크립트를 GimmickBlock에 어태치하고 Lengh를 2정도로 해줍니다. 플레이어와 블록의 거리가 2이하이면 떨어지게 만들어 줍니다. isDelete도 체크해줘 땅에 떨어지면 사라지게도 해봅니다. 이걸 안하면 화면이 스크롤 되면서 블록이 왼쪽에 부디치면 어색하게 밀립니다.

블록이 2이상 높이 배치되어 있을 경우 블록이 떨어지지 않습니다. 거리d를 x만 계산해서 높이가 높아도 x축으로 다가가면  떨어지게 만들어 봤습니다. 이건 게임의 상황에 따라 적절히 조절해보시면 됩니다.

float d = Mathf.Abs( transform.position.x- player.transform.position.x);

완성된 GimmickBlock을 프리팹으로 만들어 씬에 몇개더 배치해 봅니다. 

게임 스테이지를 전부 클리어했을때 표시할 엔딩장면을 만들어 봅시다. Result 라는 이름의 씬을 만들어 봅니다.

결과화면  UI 만들기 

배경 이미지 배치

+UI>Image를 추가하고 자동추가된 Canvas 컴포넌트의 Render Mode값을 ScreenSpace-Camera로 설정하고 Render Camara Main Camera를 선택해 줍니다.  tile_back이미지를 Image컴포넌트에 끌어다 놓습니다. 이미지 크기를 화면에 맞게 조정합니다. 하이라키에서 image를 선택하고Anchor Presets를 열어 ALT를 누른상태에서 stetch를 선택해도 됩니다.

 

타이틀로 돌아가는 버튼 배치

하이라키의+를 클릭해 UI>Legacy>Button을 추가합니다. 프로젝트뷰 Image폴더의 Button스프라이트를 Image컴포넌트의  Source에 끌어다 놓습니다. Preserve Aspect / SetNativeSize후 적당히 위치를 잡습니다.

자식으로 Text도 추가하고 타이틀로 돌아가기를 입력해줍니다. 사이즈는 48정도 입니다.

Button에 ChangeScene 스크립트를 끌어다 추가해주고 Scene Name은 Title로 설정해줍니다.

OnClick()에 +를 눌러 리스트를 하나 만들고 하이라키의 Button을 끌어다 연결후 None Function을 눌러 ChangeScene>Load()를 선택해줍니다.

 

점수표시용 UI만들기

하이라키의+를 클릭해 UI>Image을 추가합니다. 프로젝트뷰 Image폴더의 ScoreBoard스프라이트를 Image컴포넌트의  Source에 끌어다 놓습니다. Preserve Aspect / SetNativeSize후 적당히 위치를 잡습니다. 이름을 ScoreBoard 로 바꿉니다.

자식으로 Text도 추가하고 타이틀로 돌아가기를 입력해줍니다. 사이즈는 48정도 입니다. 이름을 ScoreText로 바꿉니다.

전체 점수를 표시할 스크립트 만들기

ResultManager스크립트를 만들어 Canvas에 어태치합니다.

ResultManager.cs
0.00MB

실행되면 GameManager의 totalScore를 ScoreBoard자식인 scoreText에 표시하는게 다입니다.

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

public class ResultManager : MonoBehaviour
{
    public GameObject scoreText;

    // Start is called before the first frame update
    void Start() {
        scoreText.GetComponent<Text>().text = GameManager.totalScore.ToString();
    }
}

 Canvas에 어태치된 ResultManager스크립트 컴포넌트의  scoreText변수에 ScoreText를 끌어다 스크립트에 연결해 줍니다.

시간처리를 위한 변수를 추가한다.

// +++ 시간 제한 추가 +++
public GameObject timeBar;          // 시간 표시 이미지 
public GameObject timeText;         // 시간 텍스트
TimeController timeCnt;             // TimeController 스크립트 참조

 

점수처를 위한 변수를 추가한다.

// +++ 점수 추가 +++
public GameObject scoreText;        // 점수 텍스트  
public static int totalScore;       // 점수 총합
public int stageScore = 0;          // 스테이지 점수

싱글톤정적변수인 gameState를 체크해 게임중이라면 player게임오브젝트의 playerController스크립트의 score변수가 0이 아니라면 stageScore를 추가해주고 0으로 변경한후 점수를 처리하기 위한 UpdateScore()를 호출한다.

else if (PlayerController.gameState == "playing")
    {
        // 게임 중
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        // PlayerController 가져오기
        PlayerController playerCnt = player.GetComponent<PlayerController>();
        // +++ 시간 제한 추가 +++
        // 시간 갱신
        if(timeCnt != null)
        {
			~시간 종료 처리
        }
        // +++ 점수 추가 +++
        if (playerCnt.score != 0)
        {
            stageScore += playerCnt.score;
            playerCnt.score = 0;
            UpdateScore();
        }
    }

UpdateScore()에서는 public으로 연결된 scoreText게임오브젝트의 Text컴포넌트를 갱신해준다. 재미있는건 하이라키의 모든건 게임오브젝트라 Text마저도 GameObject이다. Text 내용을 변경하기 위해서는 귀찮아도 다시 GetComponent<Text>를 통해 test에 접근해야 한다 

void UpdateScore()
{
    int score = stageScore + totalScore;
    scoreText.GetComponent<Text>().text = score.ToString();
}

using UnityEngine.UI;  간 선언되어 있기 때문에  Text타입을 이용해 좀더 편하게 사용할 수도 있다.

public Text ScoreText;
ScoreText.text =  score.ToString();

시간은 TimeController에서 관리되여 있고 여기서는 시간을 표시하고 게임을 관리하기 위해서 사용한다. 소스를 읽어보면 이해할수 있으리라 믿습니다.

GameManager.cs
0.00MB

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI を使うのに必要

public class GameManager : MonoBehaviour
{
    public GameObject mainImage;        // 이미지를 담아두는 GameObject
    public Sprite gameOverSpr;          // GAME OVER 이미지
    public Sprite gameClearSpr;         // GAME CLEAR 이미지
    public GameObject panel;            // 패널
    public GameObject restartButton;    // RESTART 버튼
    public GameObject nextButton;       // NEXT 버튼
    Image titleImage;                   // 이미지를 표시하고있는 Image 컴포넌트

    // +++ 시간 제한 추가 +++
    public GameObject timeBar;          // 시간 표시 이미지 
    public GameObject timeText;         // 시간 텍스트
    TimeController timeCnt;             // TimeController

    // +++ 점수 추가 +++
    public GameObject scoreText;        // 점수 텍스트  
    public static int totalScore;       // 점수 총합
    public int stageScore = 0;          // 스테이지 점수

    // Start is called before the first frame update
    void Start()
    {
        // 이미지 숨기기
        Invoke("InactiveImage", 1.0f);
        // 버튼(패널)을 숨기기
        panel.SetActive(false);

        // +++ 시간 제한 추가 +++
        // TimeController 가져옴
        timeCnt = GetComponent<TimeController>();
        if(timeCnt != null)
        {
            if(timeCnt.gameTime == 0.0f)
            {
                timeBar.SetActive(false);   // 시간 제한이 없으면 숨김
            }
        }

        // +++ 점수 추가 +++
        UpdateScore();
    }

    // Update is called once per frame
    void Update()
    {
        if (PlayerController.gameState == "gameclear")
        {
            // 게임 클리어
            mainImage.SetActive(true); // 이미지 표시
            panel.SetActive(true); // 버튼(패널)을 표시
            // RESTART 버튼 무효화 
            Button bt = restartButton.GetComponent<Button>();
            bt.interactable = false;
            mainImage.GetComponent<Image>().sprite = gameClearSpr;
            PlayerController.gameState = "gameend";

            // +++ 시간 제한 추가 +++
            if (timeCnt != null)
            {
                timeCnt.isTimeOver = true; // 시간 카운트 중지

                // +++ 점수 추가 +++
                // 정수에 할당하여 소숫점 버림
                int time = (int)timeCnt.displayTime;
                totalScore += time * 10;        // 남은 시간을 점수에 더한다.
            }
            // +++ 점수 추가 +++
            totalScore += stageScore;
            stageScore = 0;
            UpdateScore();// 점수 갱신
        }
        else if (PlayerController.gameState == "gameover")
        {
            // 게임 오버
            mainImage.SetActive(true);      // 이미지 표시하기
            panel.SetActive(true);          // 버튼(패널)표시하기
            // NEXT 버튼 비활성
            Button bt = nextButton.GetComponent<Button>();
            bt.interactable = false;
            mainImage.GetComponent<Image>().sprite = gameOverSpr;
            PlayerController.gameState = "gameend";

            // +++ 시간 제한 추가 +++
            if (timeCnt != null)
            {
                timeCnt.isTimeOver = true; // 시간 카운트 중지
            }
        }
        else if (PlayerController.gameState == "playing")
        {
            // 게임 중
            GameObject player = GameObject.FindGameObjectWithTag("Player");
            // PlayerController 가져오기
            PlayerController playerCnt = player.GetComponent<PlayerController>();
            // +++ 시간 제한 추가 +++
            // 시간 갱신
            if(timeCnt != null)
            {
                if (timeCnt.gameTime > 0.0f)
                {
                    // 정수에 할당하여 소수점 이하를 버림
                    int time = (int)timeCnt.displayTime;
                    // 시간 갱신
                    timeText.GetComponent<Text>().text = time.ToString();
                    // 타임 오버
                    if (time == 0)
                    {
                        playerCnt.GameOver();   // 게임 오버
                    }
                }
            }
            // +++ 점수 추가 +++
            if (playerCnt.score != 0)
            {
                stageScore += playerCnt.score;
                playerCnt.score = 0;
                UpdateScore();
            }
        }
    }
    // 이미지 숨김
    void InactiveImage()
    {
        mainImage.SetActive(false);
    }
    // +++ 점수 추가 +++
    void UpdateScore()
    {
        int score = stageScore + totalScore;
        scoreText.GetComponent<Text>().text = score.ToString();
    }
}

 

시간을 계측하는 스크립트 만들기

카운트다운과 카운트업을 모두 할수 있는 스크립트를 만들고 이름을 TimeController로 합니다.

프로젝트뷰의 Prefab폴더에서 Canvs 프리팹을 더블클릭후 편집모드 상태에서 TimeController 스크립트를 어태치합니다.

분석을  해보면

변수

times가 시계다.

public bool isCountDown = true; // true= 카운트 다운으로 시간 측정
public float gameTime = 0;      // 게엠의 최대 시간
public bool isTimeOver = false; // true= 타이머 정지
public float displayTime = 0;   // 표시 시간
float times = 0;                // 현재 시간

Start()

최대게임시간을 displayTime으로 지정한다.

void Start(){
    if (isCountDown){
        // 카운트다운
        displayTime = gameTime;
    }
}

Update()

times를 업데이트해주고 이시간을 이용해 displayTime을 갱신한다.

void Update(){
    if (isTimeOver == false){
        times += Time.deltaTime;
        if (isCountDown){
            // 카운트다은
            displayTime = gameTime - times;
            }
        }else{
            // 카운트업
            displayTime = times;
        }
    }
}

스크립트를 Canvas에 어태치한후 인스펙트뷰의 GameTime값을 60으로 IsCount Down을 체크합니다.

TimeController.cs
0.00MB

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

public class TimeController : MonoBehaviour{
    public bool isCountDown = true; // true= 카운트 다운으로 시간 측정
    public float gameTime = 0;      // 게엠의 최대 시간
    public bool isTimeOver = false; // true= 타이머 정지
    public float displayTime = 0;   // 표시 시간

    float times = 0;                // 현재 시간

    // Start is called before the first frame update
    void Start(){
        if (isCountDown){
            // 카운트다운
            displayTime = gameTime;
        }
    }

    // Update is called once per frame
    void Update(){
        if (isTimeOver == false){
            times += Time.deltaTime;
            if (isCountDown){
                // 카운트다은
                displayTime = gameTime - times;
                if (displayTime <= 0.0f){
                    displayTime = 0.0f;
                    isTimeOver = true;
                }
            }else{
                // 카운트업
                displayTime = times;
                if (displayTime >= gameTime){
                    displayTime = gameTime;
                    isTimeOver = true;
                }
            }
            Debug.Log("TIMES: " + displayTime);
        }
    }
}

 

게임에 시간제한 UI 추가하기

Prefabs폴더의 Canvas를 선택하고 인스펙트뷰의 Open을 클릭한다.

하이라키의 +를 클릭해서 UI>Image를 2개 추가하고 이름을 TimeBar, Score로 한다 

 Images폴더에서 TimeBar, ScoreBoard를 선택해 인스펙트뷰의 Image컴포넌트의 Source Image에 끌어다 놓는다.

Preserve Aspect를 체크하고 Set NativeSize를 클릭후 적당히 사이즈를 조정한다.

위치는 Transform 의 Anchor Presets를 누르고 ALT를 눌러 상중과 상우를 선택해 적당해 배치한다.

TimeBar, Score Image자식으로 UI>Legacy>Text를 추가한다. 이름은 TimeText, ScoreText로 바꿔준다. Text는 000 Font Size는 48~64로 설정한다.

 

실행시 Text가 안보이는건 Text박스가 작아서이다. 하이라키에서 두 Text를 순서대로 골라서 박스사이즈를 맞춰준다.

아이템과 점수 만들기

Images폴더에 Item_color 스프라이트가 있다. 하나를 끌어다 씬뷰에 놓는다.

인스펙트뷰에서 CircleCollider2D를 추가해준다. Trigger를 체크해준다. 플레이어와 접촉을 감지하기 위해서다

Tag에서 ScoreItem를 추가로 만들어주고 지정해준다. 플레이어에서 Trigger가 발생했을때 Item인지 체크하기 위해서다.

ItemData.cs 스크립트를 작성에 어태치해준다. 단순히 플레이어가 먹었을때 올라가는 점수값 변수만 있다.

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

public class ItemData : MonoBehaviour {
    public int value = 0;       // 정수값을 설정할 수 있다
}

이제 이걸 프로젝트뷰 프리팹폴더로 끌어다  놓는다.

Ctrl-D로 3개 더 만든후 이름을 Item_blue, Item_green, Item_red, Item_white로 바꾸고

itemData 컴포넌트의 value를 blue:30, green:10, red:50, white:100으로 설정한다.

스프라이트 렌더러컴포넌트의 Sprite 검색◉을 누르고 이름에 맞는 스프라이트를 선택해준다.

실험삼아 아이템들을 씬뷰에 배치한다.

 

아이템 획득 스크립트

방금만든 아이템을 획등하는 스크립트를 PlayerController에 추가합니다.

Collider에서 Trigger가 발생시 OnTriggerEnter2D()에서 처리하므로 Tag가 "ScoreItem"이라면 충돌 게임오브젝트의 ItemData컴포넌트(스크립트)의 변수인 value값을 score로 설정한다. 이값은 나중에 GameManager에서 불리워져 점수로 추가된다.

PlayerController.cs
0.01MB

// 접촉 시작
private void OnTriggerEnter2D(Collider2D collision)
{
   if(collision.gameObject.tag == "Goal")  {
        Goal();        // 골
    }
    else if (collision.gameObject.tag == "Dead") {
        GameOver();     // 게임 오버
    }
    else if (collision.gameObject.tag == "ScoreItem"){
        // 점수 아이템
        // ItemData 가져오기
        ItemData item = collision.gameObject.GetComponent<ItemData>(); // 점수 얻기
        score = item.value;
        // 아이템 제거
        Destroy(collision.gameObject);
    }
}

 

 

이제 애니메이션 전화을 위한 PlayerController 스크립트를 수정합니다.

이번에 제일 중요한 부분은 충돌 판정입니다. Goal과 Dead존에 들어가면 알맞은 함수를 실행합니다.

  private void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "Goal") {
            Goal();
        } else if(collision.gameObject.tag =="Dead") {
            GameOver();
        }
    }

FixedUpdate()에서는 이동중이면 PlayerMove, 정지시 PlayerStop, 하늘에 떠 있다면 PlayerJump 애니메이션을 실행합니다.

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

public class PlayerController : MonoBehaviour{
		// 생략
		//애니메이션처리
    Animator animator;  //애니메이터 컨트롤러 참조
    public string stopAnim = "PlayerStop";
    public string moveAnim = "PlayerMove";
    public string jumpAnim = "PlayerJump";
    public string goalAnim = "PlayerGoal";
    public string deadAnim = "PlayerOver";
    string nowAnim = "";
    string oldAnim = "";
    void Start(){
        rbody = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        nowAnim = stopAnim;
        oldAnim = stopAnim;
    }

    // Update is called once per frame
    void Update(){
 		// 생략 
    }
    private void FixedUpdate() {
     	//생략
        if(onGround) {
            if(axisH == 0) {
                nowAnim = stopAnim;
            } else {
                nowAnim = moveAnim;
            }
        } else {
            nowAnim = jumpAnim;
        }
        if(nowAnim != oldAnim) {
            oldAnim = nowAnim;
            animator.Play(nowAnim);
        }
    }
    public void Jump() {
        goJump = true;
    }
    private void OnTriggerEnter2D(Collider2D collision) {
        if (collision.gameObject.tag == "Goal") {
            Goal();
        } else if(collision.gameObject.tag =="Dead") {
            GameOver();
        }
    }
    public void Goal() {
        animator.Play(goalAnim);
    }
    public void GameOver() {
        animator.Play(deadAnim);
    }
}

PlayerController.cs
0.00MB

 

+ Recent posts