활과 화살의 게임 오브젝트 만들기

활과 화살의 이미지 에셋을 씬뷰에 끌어놔 게임오브젝트를 만들고 Sprite Renderer의 Order in Layer값을 3으로 설정합니다.

활 : 이름을 Bow로 변경하고 위치는 0으로 합니다.

화살 : 이름을 Arrow라는 태그와 레이어를 만들어 설정합니다. 접촉판정에 사용됩니다.

활과 화살을 Player폴더로 끌어서 프리팹으로 만들고 씬뷰의 활과 화살은 제거합니다.

 

화살을 발사하는 스크립트

ArrowShoot이라는 스크립트를 Player폴더에 만들고 Player에 어태치합니다.

ArrowShoot.cs
0.00MB

변수

public float shootSpeed = 12.0f;    //화살 속도
public float shootDelay = 0.25f;    //발사 간격
public GameObject bowPrefab;        //활의 프리펩
public GameObject arrowPrefab;      //호살의 프리펩

bool inAttack = false;               //공격 중 여부
GameObject bowObj;                   //활의 게임 오브젝트

Start()

bow프리팹을 이용해 오브젝트를 만들고 플레이어의 자식으로 만듭니다.

void Start(){
    //활을 플레이어 캐릭터에 배치
    Vector3 pos = transform.position;
    bowObj = Instantiate(bowPrefab, pos, Quaternion.identity);
    bowObj.transform.SetParent(transform);//활의 부모로 플레이어 캐릭터를 설정
}

Update()

활을 캐릭터에 맞게 회전시키고 위치시킵니다.

z값을 조정해 캐릭터보다 앞으로 놓습니다. 

공격키가 눌리면 Attack()를 실행합니다.

void Update() {
    if ((Input.GetButtonDown("Fire3"))) {
        //공격 키가 눌림
        Attack();
    }
    //활의 회전과 우선순위
    float bowZ = -1;    //활의 Z값(캐릭터보다 앞으로 설정)
    PlayerController plmv = GetComponent<PlayerController>();
    if (plmv.angleZ > 30 && plmv.angleZ < 150)  {
        //위 방향
        bowZ = 1;       //활의 Z값(캐릭터 보다 뒤로 설정)
    }
    //활의 회전
    bowObj.transform.rotation = Quaternion.Euler(0, 0, plmv.angleZ);
    //활의 우선순위
    bowObj.transform.position = new Vector3(transform.position.x,
                                transform.position.y, bowZ);
}

Attack()

화살을 쏘는 처리를 합니다 . ItemKeepr.hasArrows를 하나 줄입니다. static이므로 참조없이 바로 접근이 가능합니다.

public void Attack()
{
    //화살을 가지고 있음 & 공격중이 아님
    if (ItemKeeper.hasArrows > 0 && inAttack == false)
    {
        ItemKeeper.hasArrows -= 1;	//화살을 소모
        inAttack = true;		//공격 중으로 설정
        //화살 발사
        PlayerController playerCnt = GetComponent<PlayerController>();
        float angleZ = playerCnt.angleZ; //회전 각도
        //화살의 게임 오브젝트 만들기(진행 방향으로 회전)
        Quaternion r = Quaternion.Euler(0, 0, angleZ);
        GameObject arrowObj = Instantiate(arrowPrefab, transform.position, r);
        //화살을 발사할 벡터 생성
        float x = Mathf.Cos(angleZ * Mathf.Deg2Rad);
        float y = Mathf.Sin(angleZ * Mathf.Deg2Rad);
        Vector3 v = new Vector3(x, y) * shootSpeed;
        //화살에 힘들 가하기
        Rigidbody2D body = arrowObj.GetComponent<Rigidbody2D>();
        body.AddForce(v, ForceMode2D.Impulse);
        //공격 중이 아님으로 설정
        Invoke("StopAttack", shootDelay);
    }
}

화살을 제어하는 스크립트 만들기

발사된 화살을 제어하는 스크립트를 만들어봅니다. ArrowController라는 스크립트를 Player폴더에 만들고 Player에 어태치 합니다.(이미 어태치되어있으면 두번할 필요는 없습니다.)  발사된 화살은 접촉된 물체의 자식이 되어 붙어 있다가 deleteTime후 제거됩니다.

ArrowController.cs
0.00MB

변수

public float deleteTime = 2;  //제거 시간

Start()

void Start()  {
    Destroy(gameObject, deleteTime); //이정 시간후 제거하기
}

OnCollisionEnter2D()

private void OnCollisionEnter2D(Collision2D collision)
{
    //접촉한 게임 오브젝트의 자식으로하기
    transform.SetParent(collision.transform);
    //충돌 판정을 비활성
    GetComponent<CircleCollider2D>().enabled = false;
    //물리 시뮬레이션을 비활성
    GetComponent<Rigidbody2D>().simulated = false;
}

레이어의 접촉 설정 편집하기

화살은 발사되면서 Player와 접촉하기 때문에 나가지도 못하고 멈춥니다. 이걸 막기위해

Edit>Project Setting에서 Layer Collision Matrix를 열어 Player와 Arrow가 교차하는 점을 꺼줍니다.

게임 실행하기

활의 발사를 확인해 봅니다. 테스트를 위해 임시로 ItemKeeper의 hasArrow를 0 이상의 숫자로 해줍니다. 

PlayerController.cs 의 Start()에  ItemKeeper.hasArrows = 10; 적어도 된다.

[Shift]키를 누르면 이동 방향으로 화살이 발사됩니다. 벽에 닿으면 화살은 그자리에 멈추고 잠시뒤 사라집니다.

 

플레이어가 돌아다니다가 열쇠와 화살같은 아이템을 얻을수 있는 시스템을 만듭니다. 

대미지 회복 아이템인 하트는 소유아이템이 아니므로 제외합니다.

소유아이템은 씬이 바뀌어도 계속 가지고 있어야 합니다.

ItemKeeper 스크립트를 만들어 Player에 어태치 합니다. 전 player프리팹을 사용했기 때문에 이미 어태치되어 있습니다.

변수

코드는 static int 타입의 hgsKeys와 hasArrows 변수 두개 뿐입니다.

ItemKeeper.cs
0.00MB

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

public class ItemKeeper : MonoBehaviour
{
    public static int hasKeys = 0;          //열쇠 수
    public static int hasArrows = 0;        //화살 소지수
    // Start is called before the first frame update
    void Start() { }

    // Update is called once per frame
    void Update() { }
    //아이템 저장하기
    public static void SaveItem()  {  }
}

 

플레이어 캐릭터는 활을 겨누면서 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 스크립트가 어태치되어 있어 활도 표시되었습니다 이건 뒤에서 공부합니다.

 

새로운 프로젝트를 만듭니다. 2D템플릿을 선택하고 프로젝트 이름은 UniTopGame 이라고 합니다.

WorldMap이라는 이름으로 씬을 저장하고 빌드에 추가합니다.

예제의 UniTopGame_chap8>Assets폴더의 Iamges폴더 Map폴더와 Sound폴더를  프로젝트의 Assets폴더로 복사합니다.

Map.zip
0.04MB
Images.zip
2.56MB
Sounds.zip
12.10MB

8.3 타일맵으로 게임 화면 만들기

타일맵 알아보기

타일맵이란 동일한 크기의 이미지를 여러개 나열해 게임화면을 만드는 방법입니다.

멀티스프라이트 만들기

Map폴더에서 TileMap을 선택합니다. 한장의 그림안에 32x32픽셀규격의 여러 타일맵을 가진 도트이미지 입니다. 

유니티는 한 장의 이미지를 여러개로 잘라 사용할 수 있는 기능이 있습니다.

 TileMap을 선택하고 인스펙터뷰를 봅니다.

Sprite Mode의 Single->Multiple로 변경합니다. 그러면 그림을 분할해서 사용할수 있습니다.

Pixel Per Unit는 기본값 100에서 타일맵사이즈인 32으로 변경합니다.

FilterMode를 Point(no filter)로 변경해 도트이미지의 가장자리가 선명하게 보이도록 합니다.

Apply를 클릭해 변경된 설정을 저장합니다.

Sprite Editor로 편집하기

[Sprite Editor]버튼을 눌러 Sprite Editor창을 엽니다. 한장의 그림을 여러 개로 나누는 편집창입니다.

[Slice] 탭버튼을 클릭해 이미지를 자동으로 분활하기 위한 설정을 합니다.

 

먼저 기준점을 설정하는 Pivot항목을 살펴봅니다. Multiple모드에서 분할할때 피봇을 설정해야 합니다. 하지만 이번에는 특별히 변경할 것이 없으므로 변경하지 않습니다.

다음은 Type 항목입니다.

Automatic은 이미지의 투명한 부분을 고려해 자동으로 분할 합니다.

Grid By Cell Size는 설정된 X Y 사이즈로 분할합니다.

Grid By Cell Count는 C R로 설정된 개수로 분할 합니다.

예제에 사용된 타일맵은 32x32로 제작되었으므로 Grid By Cell Size를 선택해 x:32 y:32로 설정해줍니다. 하단의 Slice를 클릭하면 이미지가 분할됩니다.

SpriteEditor 위쪽중간의 Apply를 클릭하면 0~29넘버링을 갖는 총 30개의 스프라이트가 프로젝트뷰에 생성됩니다. 

프로젝트뷰에서 TileMap오른쪽 화살표▶를 클릭하면 확인할 수 있습니다.

타일에셋만들기

타일팔레트로 타일에셋을 만듭니다. Window>2D>Tile Palette를 선택하면 Tile Palette창이 열립니다.

Tile Palette창은  타일맵으로 배치할 타일을 선택하는 창입니다. 

왼쪽위에 위치한 Create New Pallet를 클릭합니다. 이름을 StageMap으로 한후 Create버튼을 클릭하고 Map폴더에 저장합니다.

Tile Palette 탭을 잡고 드래그해서 인스펙터뷰 아래쪽에 적당해 놓습니다. 이후 TileMap이미지를 프로젝트부에서 끌어다 놓습니다. 타일 에셋 파일의 저장 경로 를 선택하는 창이 표시되는 Map폴더를 선택합니다.

타일 팔레트에 타일이 표시되고 타일 에셋 파일이 저장됐습니다.

 

타일맵 만들기

하이라키에서 + > 2DObjec t> Tilemap > Rectangular를 선택합니다. Grid와 자식으로 Tilemap게임 오브젝트가 추가됩니다.

계층뷰에서 Grid나 Tilemap을 선택하면 씬에 눈금이 표시됩니다. 타일맵에 배치될 타일의 위치입니다.

하이라키에서 타일맵을 선택하면 인스펙터뷰에서 Timemap Renderer 컴포넌트에서 Order in Layer가 있습니다. 타일맵이 여러개일때 레이어를 조정해 우선순위를 변경할 수 있습니다.

타일 에셋을 타일맵에 배치하기

타일 팔레트를 사용해 맵에 타일을 배치해보겠습니다. 먼저 월드맵을 만들어 봅니다.

Tile Pallet 창에 나열된 타일의 가장 왼쪽에 있는 바다 타일을 선택하고 툴 바의 왼쪽에서 세번째브러시 툴을 선택합니다. 신뷰에서 마우스를 클릭할 상태에서 드래그하면 선택한 타일이 그리드에 배치됩니다.

 

Tile Palette 툴바

Tile Palette 창의 상단에는 타일맵에 타일 에셋을 배치하기 위한 툴 바가 있습니다. 이 툴들을 사용해 타일맵을 배치합니다.

타일 회전 배치

키보드의 중괄호 [ , ]  를 누르면 타일을 90도씩 회전시킬수 있습니다. 

 

맵만들기

타일로 맵을 만들어 봅니다. 씬을 두개 만듭니다.  교재에서는 바다 위에 떠 있는 섬을 월드맵으로 해 던전 맵을 만들었습니다. 현재이름이 WorldMap입니다.

뒤에서 입구로 씬을 이동하는 기능을 추가할 것이니 적당한 위치에 입구를 하나 만듭니다. 이건 dungeon1으로 저장합니다.

WorldMap.unity
0.46MB
dungeon1.unity
0.05MB

타일맵에 충돌 판정 설정하기

캐릭터와 접촉했을때 충돌판정을 만들기 위해 하이라키에서 TileMap을 선택한후 Tilemap collider 2D 컴포넌트를 추가합니다. (첨부파일에는 이미 추가되어져 있습니다.)

타일맵에 충돌 판정이 추가됐습니다. 필요없는 지면 부분에도 충돌 판정이 생기니 다음과 같이 조정합니다.

프로젝트뷰에서 타일에셋을 선택하면 인스펙터뷰에서 Collider Type이라는 풀다운 메뉴를 선택할 수 있습니다.

이 값으로 충돌 타입을 선택할 수 있습니다.

None: 충돌판정이 없습니다.

Sprite: 스프라이트의 모양대로 충돌 판정을 합니다. 투명한 부분에는 충돌 판정이 없습니다.

Grid:타일의 사각형 모양으로 충돌 판정을 합니다.

예제에서 충돌 판정을 없앨 타일은 모래, 풀밭, 짙은 풀밭, 다리, 던전 바닥입니다. 해당 타일의 ColliderType을 None으로 설정합니다.  첨부된 파일에는 이미 설정이 되어져 있습니다.

 

자유로운 모양의 충돌 판정

Collider Type이 Sprite인 타일을 선택하고 [Sprite Editor]버튼을 누르면 Sprite Editor창이 열립니다. 이번에는 던전 출입구인 TipeMap_9을 선택했습니다.

 

Sprite Editor창의 왼족 위에 풀다운 메뉴에서 Custom Physics Shape를 선택하면 충돌 판정을 패스로 설절할 수 있어 플레이어가 출입구로 진입하는 방향을 제한할 수 있습니다. 스프라이트를 선택하고 Generate를 선택하면 해당 스프라이트의 주변에 사각형 포인터가 배치되고 흰선으로 테두리가 만들어집니다. 하얀테두리 영역이 충돌판정 범위가 됩니다.

네모서리의 사각형을 마우스로 드래그해 흰 테두리의 모양을 바꿀수 잇고 선을 클릭해 새로운 포인트를 만들고 delete키로 삭제가능합니다. 완료후 Apply로 저장합니다.  

던전 입구 뿐 아니라 다섯 종류의 계단 타일도 충돌 판정을 똑같이 설정합니다. 캐릭터가 계단에 진입할 곳외는 블록킹 처리 해줍니다.

 

2부에서는 횡스크롤방식의 게임이었지만 3부에서는 위에서 보는 탑뷰방식의 게임을 만들어 보겠습니다.

다음과 같은 제작방법을 배워보겠습니다.

  • 타일맵 제작방법
  • 멀티 스프라이트
  • 슈팅요소
  • 여러 씬에 걸치 데이터 처리

CHAPTER 08 탑뷰액션게임의 기본 시스템 만들기

8.1 예제 게임 실행해보기

생략

8.2 탑뷰게임에 대해 알아보기

위에서 바라보는 시점의 게임 스테이지를 의미합니다. 캐릭터가 상하좌우 또는 360도 임의의 방향으로 이동할 수 있습니다.

  • 게임의 무대인 맵은 세계지도와 던전내부를 위에서 바라본 시점으로 구성됩니다. 타일맵 시스템을 활용해 만듭니다.

플레이어캐릭터

  • 활을쏘아 적을 공격할수 있고 공격을 받으면 HP가 감소합니다.

아이템(열쇠와 화살)

  • 맵 위에 아이템을 배치해 플레이어가 획득할 수 있도록 합니다. 아이템은 화살과 열쇠 생명 세가지를 만듭니다. 

문과입구

  • 맵의 입구가 배치된 장소에서 게임을 시작합니다. 입구는 막혀있어 열쇠로 열 수 있도록 합니다.

적 캐릭터

  • 플레이어가 일정 거리 이내로 접근하면 추적을 시작합니다. 플레이어에 1의 대미지를 줄 수 있고 플레이어의 화살을 맞으면 죽습니다.

 

 

지금까지는 키보드로 플레이했습니다. 스마트폰에서 플레이할 경우 터치스크린 조작이 필요합니다.

 

스마트폰의 UI 생각해보기

이게임은 횡 스크롤 게임으로 조작은 좌우 이동과 점프입니다. 터치스크린에서 다음과 같이 구현합니다.

 

좌우 이동

화면 왼쪽 아래 버추얼 패드를표시하고 이것을 손가락으로 움직여 좌우로 이동할 수 있습니다.

점프 화면 오른쪽 점프 버튼을 배치합니다. 버튼을 누르면 점프하도록 구현합니다.

 

PlayerController스크립트 변경하기

PlayerController.cs
0.01MB

변수

isMoving이 true이면 키보드 입력처리를 무시하고 터치로 처리하도록 합니다.

// 터치 스크린 조작
bool isMoving = false;

Update() 

isMoving=false (키보드처리) 면 키보드 입력을 읽어 옵니다.

if (isMoving == false) {
// 수평 방향의 입력 확인
    axisH = Input.GetAxisRaw("Horizontal");
}

SetAxis()

버추얼 패드에서 불리는 메서드 입니다.  키보드 입력대신 axisH에 값을 입력합니다. float v는 이번에는 사용안하지만 다음을 위한 매개변수입니다.

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

 

GameManager 클래스 수정

게임이 종료되었을대 UI를 숨기는 처리를 Update메서드에 추가합니다. Jump 메서드를 추가하고 그 안에서 PalyerController의 Jump메서드를 호출합니다. GameManager에 집약시켜 이후 조작과 관련된 UI에 할당할 클래스를 GameManager로 한정해 작업을 단순화하기 위함입니다.

GameManager.cs
0.01MB

 

변수

가상패드와 연결할 변수 inputUI를 마련합니다.

// +++ 플레이어 조작 +++
public GameObject inputUI; // 조작 UI 패널

Update() 

게임클리어나 게임오버시 가상패드 UI를 숨깁니다.

 void Update()
    {
        if (PlayerController.gameState == "gameclear")
        {
            // 게임 클리어
            // +++ 점수 추가 +++
            // 사운드 재생
            // +++ 플레어이 조작 +++
            inputUI.SetActive(false);   // 조작 UI 숨기기
        }
        else if (PlayerController.gameState == "gameover")
        {
            // 게임 오버
             // +++ 시간 제한 추가 +++
            // +++ 사운드 재생 추가 +++
            // +++ 플레어이 조작 +++
            inputUI.SetActive(false);   // 조작 UI 숨기기
        }
        else if (PlayerController.gameState == "playing")
        {
			~생략

Jump()

Player오브젝트의 PlayerController컴포넌트의 Jump()메서드를 실행합니다.

    // +++ 플레이어 조작 +++ 
    // 점프
      public void Jump() {
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        PlayerController playerCnt = player.GetComponent<PlayerController>();
        playerCnt.Jump();
    }

 

점프버튼 배치하기

이제 Canvas에 점프 버튼을 배치하고 버튼이 눌리면 플레이어가 점프하도록 해봅시다.

Canvas 프리팹을 편집모드로 엽니다.

하이라키 +>UI>Legacy>Button을 선택한 후 Canvas의 프리팹에 새로운 버튼 오브젝트를 배치하고 이름을 jumpbutton으로 합니다. Images폴더의 JumpButton Sprite를 끌어 JumpButton 오브젝트의 Image컴포넌트의 Source Image로 끌어 넣고

preserve Aspect 체크 SetNativeSize 클릭후 Canvas 우하로 배치시킵니다 .Ankor Preset +ALT 배치할수 있습니다.

이미지만 표시할 것이므로 하이라키 JumpButton을 선택한후 자식 Text는 제거합니다.

점프버튼의 이벤트 설정하기

버튼이 눌리면 플레이어 캐릭터가 점프할 수 있도록 이벤트를 연결해 보겠습니다.

Button의 OnClikc() 컴포넌트는 버튼이 눌렸다 떨어질때 반응하는데 점프버튼은 눌렸을때 바로 반응해야 하므로 Event Trigger라는 컴포넌트를 추가하여 사용합니다.  추가하면 Add New Event Type을 눌러 Pointer Down을 선택합니다.

OnClick()처럼 +를 눌러 리스트를 추가하고 None Object에 GameManager를 끌어다 연결합니다. GameManager는 Canvas오브젝트에 어태치되어 있으므로 저는 Canvas를 끌어다 연결했습니다. 이후 No Function을 클릭해 GameManager의 Jump()를 선택해줍니다.

이제 버튼을 누르면 누르면 캐릭터가 점프합니다.

 

버추얼 패드 만들기

이제 캐릭터를 좌우로 이동시킬 버추얼 패드를 만들겠습니다. 계층뷰의 +>UI>Image를 선택후 Canvas에 새로운 이미지 오브젝트를 배치합니다. 이름은 VirtualPadBase로 한 후 자식으로 Image 오브젝트를 하나 더 비치하고 이름을 VirtualPadBaseTab으로 합니다.

Images폴더에서 VirtualPad2D는  VirtualPadBase로 VirtualPadTab이미지는 자식인 VirtualPadBaseTab의 Image컴포넌트 Image Source에 끌어 놓고 Preserve Perspective 체크, Set Native Size 클릭해줍니다.

하이라키에서 +>UI>Panel을 선택해 Canvas에 Panel을 배치하고 이름을 InputUI로 변경합니다. 사이즈를 가상패드와 점프버튼이 들어갈수 있게 그림처럼 줄입니다.

 JumpButton과 VirtualPadBase를 끌어다 패널의 자식으로 배치합니다.InputUI는 Color값을 변경해 투명하게 만듭니다.

버추얼 패드의 스크립트 만들기

VirtualPad.cs 스크립트를 만들고  VirtualPadTab에 어태치합니다. 책에서는 원형이미지에 어태치하라는데 Image폴더에 어태치해봤는데 안되서 VirtualPadBasdTab에 어태치했습니다.

 

VirtualPad.cs
0.00MB

변수

public float MaxLength = 70;    // 탭이 움직이는 최대 거리
public bool is4DPad = false;    // 상하좌우로 움직이는지 여부
GameObject player;              // 조작 할 플레이어 GameObject
Vector2 defPos;                 // 탭의 초기 좌표
Vector2 downPos;                // 터치 위치

 

Start()

void Start(){
    // 플레이어 캐릭터 가져오기
    player = GameObject.FindGameObjectWithTag("Player");
    // 탭의 초기 좌표
    defPos = GetComponent<RectTransform>().localPosition;
}

PadDown()

마우스 클릭시 스크린좌표를 저장합니다.

public void PadDown()  {
    // 마우스 포인트의 스크린 좌표
    downPos = Input.mousePosition;
}

PadDrag()

마우스를 드래그하면 localPosition을 이동시킵니다. 플레이어 캐릭터도 이동시킵니다. MaxLength를 넘기지 못하게 합니다.

public void PadDrag()  {
    // 마우스 포인트의 스크린 좌표
    Vector2 mousePosition = Input.mousePosition;
    // 새로운 탭 위치 구하기
    Vector2 newTabPos = mousePosition - downPos;// 마우스 다운 위치로 부터의 이동 거리
    if (is4DPad == false)  {
        newTabPos.y = 0; // 횡스크롤 일 때는  Y 값을 0 으로 한다.
    }
    // 이동 벡터 계산하기
    Vector2 axis = newTabPos.normalized; // 벡터를 정규화
    // 두 점의 거리 구하기
    float len = Vector2.Distance(defPos, newTabPos);
    if (len > MaxLength)  {
        // 한계거리를 넘겼기 때문에 한계 좌표로 설정
        newTabPos.x = axis.x * MaxLength;
        newTabPos.y = axis.y * MaxLength;
    }
    // 탭 이동 시키기
    GetComponent<RectTransform>().localPosition = newTabPos;
    // 플레이어 캐릭터 이동 시키기
    PlayerController plcnt = player.GetComponent<PlayerController>();
    plcnt.SetAxis(axis.x, axis.y);
}

PlayerController의 SetAxis()

매개변수를 받아 axisH를 설정합니다.

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

 

PadUp()

마우스 클릭이 업되면 패드의 위치를 초기화하고 캐릭터를 정지시킵니다.

public void PadUp() {
    // 탭 위치의 최기화 
    GetComponent<RectTransform>().localPosition = defPos;
    // 플레이어 캐릭터 정지 시키기
    PlayerController plcnt = player.GetComponent<PlayerController>();
    plcnt.SetAxis(0, 0);
}

컴포넌트 어태치

VirtualPadBase의 자식으로 배치했던 VirtualPadBaseTab에 컴포넌트를 몇가지 어태치합니다.

Event>Evnt Trigger를 선택해 Event Trigger컴포넌트를 추가합니다.

Add New Event Type 버튼을 클릭한 후 Pointer Down, Drag, Point Up 세가지를 추가합니다.

각각의  Event의 +버튼을 클릭해 리스트를 추가하고 이벤트를 추가합니다. None(Object)에 하이라키의 VirtualPadBaseTab을 끌어다 놓고 None Function 풀다운 메뉴에서 VirtualPad> PadDown(), PadDrag(), PadUP()을 선택합니다.

추가 스테이지는 여러분이 여태까지의 과정을 복습하면서 만들어 보시기 바랍니다. 

BGM 재생하기

Cavas 프리팸을 편집모드로 연후 인스펙트뷰의 Audio source 컴포넌트를 추가합니다.

Audio Source 폴더의 AudioClip에 프로젝트 뷰의 BGM_game_00을 드래그 앤 드롭합니다. 이때 Play On Awake와  Loop를 체크합니다.  이제 게임을 시작함과 동시에 BGM_game_00이 반복해서 재생됩니다.

Sounds.zip
2.45MB

 

스크립트로 사운드 재생및 정지시키기

게임을 클리어 했을때와 게임 오버일때 game_BGM_00을 멈추고 다른 사운드를 플레이합니다.

GameManager스크립트에 해당기능을 추가합니다.

 

변수

게임오버 클리어용 오디오클립 참조 변수를 마련합니다.

// +++ 사운드 재생 추가 +++
public AudioClip meGameOver;        // 게임 오버
public AudioClip meGameClear;       // 게임 클리어

Update()

오디오 컨트롤을 위해서 Audiosource컴포넌트를 이용합니다.

AudioSource soundPlayer = GetComponent<AudioSource>();


정지를 위해서 Stop(), 한번 플레이를 위해서 PlayOneShot()메서드를 이용합니다.

soundPlayer.Stop(); 
soundPlayer.PlayOneShot(meGameClear);

Update()변경부분

 void Update()
    {
        if (PlayerController.gameState == "gameclear")
        {
            // 게임 클리어
            ~중략

             // +++ 사운드 재생 추가 +++
            // 사운드 재생
            AudioSource soundPlayer = GetComponent<AudioSource>();
            if (soundPlayer != null)
            {
                // BGM 정지
                soundPlayer.Stop(); soundPlayer.PlayOneShot(meGameClear);
            }
            // +++ 플레어이 조작 +++
            inputUI.SetActive(false);   // 조작 UI 숨기기
        }
         else if (PlayerController.gameState == "gameover")
        {
            // 게임 오버
           ~중략
            // +++ 사운드 재생 추가 +++
            // 사운드 재생
            AudioSource soundPlayer = GetComponent<AudioSource>();
            if (soundPlayer != null)
            {
                // BGM 정지
                soundPlayer.Stop();
                soundPlayer.PlayOneShot(meGameOver);
            }
            // +++ 플레어이 조작 +++
            inputUI.SetActive(false);   // 조작 UI 숨기기
        }

GameManager.cs
0.01MB

스크립트를 캔버스 프리팹에 어태치하고 오디오 클립2개를 연결해줍니다.

 

이제 적 캐릭터를 만듭니다. 움직이는 데미지 블록과 비슷합니다.

적 캐릭터는 다음과 같이 동작합니다.

플레이어가 접촉하면 게임오버가 됩니다. (Dead Tag)

일정 범위를 왔다갔다한다. (MovingBlock)

벽에 접촉하면 180도 방향을 바꾼다.

이번 예제에서 만들 적캐릭터 데이터는 Empty폴더를 만들어 저장합니다.

 

적 캐릭터 만들기

적 캐릭터는 4장에서 애니메이션이 되도록 이미지를 준비했습니다.

images폴더에서 enemy1~4를 한꺼번에 선택해 씬뷰에 끌어다 애니메이션을 만듭니다. 게임오브젝트와 애니메이션 이름을 Enemy로 변경합니다.

태그를 Dead로 설정합니다. Sprite Renderer 컴포넌트의 Order in Layer는 2로 합니다. 어태치할 컴포넌트는 RigidBody 2D, Circle Collider 2D, Box Collider 2D입니다.  어태치한 후 Rigidbody 2D의 Freeze Rotation에서 Z를 체크해 회전하지 않도록 합니다.

Circle Collider 2D와 Box Collider 2D의 위치는 다음 그림과 같이 설정합니다. Collider영역은 Edit Collider를 누르면 나타나는 영역과 점을 이용해 조정합니다.

Box Collider가 Circle Collider보다 약간 크고 isTrigger를 체크해서 충돌을 담당합니다.

Player라 접촉하기면 죽일수 있게 Enemy game object는 Dead Tag를 지정하였습니다. Player가 충돌하면 죽는 DeadZone이 함정, 니들, 적으로 늘어났습니다. Circle Collider2D는 지면과의 물리적 충돌을 처리하기위해 약간 아래로 내립니다. 원이 박스보다 접촉이 적어 저항이 적습니다. 

 

적 캐릭터의 스크립트 만들기

스크립트를 하나 만들고 EnemyController라고 이름을 바꿉니다.  스크립트를 만들고 어태치합니다. 플레이어 캐릭터를 움직이는 PlayerController의 간소한 버전입니다.

 

변수

public float speed = 3.0f;          // 이동 속도
public string direction = "left";   // 방향 right or left 
public float range = 0.0f;          // 움직이는 범위
Vector3 defPos;                     // 시작 위치

Start()

변수 direction이 "right"일 경우 스프라이트 localScale을  x축으로 반전시켜 줍니다. 현재위치를 디폴트위치로 합니다.

void Start()  {
    if (direction == "right") {
        transform.localScale = new Vector2(-1, 1);// 방향 변경
    }
    defPos = transform.position;  // 시작 위
}

Update()

적캐릭터가 이동범위를 벗어나면 direction="right"<->"left"로 방향을 바꿔준다. range가 0일 경우 범위 체크는 안하고 벽을 부딪쳐야만 반전한다.

void Update() {
    if (range > 0.0f)  {
        if (transform.position.x < defPos.x - (range / 2)) {
            direction = "right";
            transform.localScale = new Vector2(-1, 1);// 방향 변경
            if (transform.position.x > defPos.x + (range / 2)){
                direction = "left";
                transform.localScale = new Vector2(1, 1);// 방향 변경
            }
        }
    }
}

FixedUpdate()

direction방향에 따라 velocity를 가속해준다

void FixedUpdate()   {
    // 속도 갱신
    // Rigidbody2D 가져오기
    Rigidbody2D rbody = GetComponent<Rigidbody2D>();
    if (direction == "right")  {
        rbody.velocity = new Vector2(speed, rbody.velocity.y);
    } else  {
        rbody.velocity = new Vector2(-speed, rbody.velocity.y);
    }
}

OnTriggerEnter2d()

벽과 충돌하면 direction="right"<-> "left"를 반전시켜 방향을 바꿔준다. 충돌체크를 위해서는  BoxCollider 2D isTrigger가 체크되어 있어야 합니다.

private void OnTriggerEnter2D(Collider2D collision) {
    if (direction == "right")  {
        direction = "left";
        transform.localScale = new Vector2(1, 1); // 방향 변경
    }  else {
        direction = "right";
        transform.localScale = new Vector2(-1, 1); // 방향 변경
    }
}

EnemyController.cs
0.00MB

스크립트를 적캐릭터에 어태치하고 플레이한다.

Range가 0일 경우 무대 전체를 왔다갔다하면서 벽에 부딪치면 반대방향으로 달립니다. Range를 4로 하면 4만큼의 범위내에서만 움직입니다.

 

고정 포대 게임 오브젝트 만들기

images폴더의 cannon을 씬 뷰에 드래그 앤 드롭해 게임오브젝트를 만듭니다. Order in Layer를 2로 하고 Box Collider 2D를 추가합니다. 자식오브젝트로 Create Empty를 추가하고 이름을 gate로 합니다. 위치를 포대 왼쪽으로 설정합니다. Empty라 보이지 않으므로 컬러 아이콘을 추가합니다

 

고정 포대 스크립트 만들기

스크립트를 하나 만들고 이름을 CanonController라고 합니다. cannon에 어태치 합니다.

 

변수

public GameObject objPrefab;        // 발생시키는 Prefab 데이터 
public float delayTime = 3.0f;      // 지연시간
public float fireSpeedX = -4.0f;    // 발사 벡터  X
public float fireSpeedY = 0.0f;     // 발사 벡터 Y
public float length = 8.0f;

GameObject player;                  // 플레이어
GameObject gateObj;                 // 발사구
float passedTimes = 0;              // 경과 시간

Start()

cannon의 자식오브젝트 gate와 Player를 찾아 연결해 줍니다.

void Start()
{
    // 발사구 오브젝트 얻기
    Transform tr = transform.Find("gate");  //자식오브젝트 transform 찾기
    gateObj = tr.gameObject;  //자식오브젝트 가져오기
    // 플레이어
    player = GameObject.FindGameObjectWithTag("Player");
}

Update()

passedTimes를 갱신하면서 delayTime보다 크면 포탄을 발사해줍니다. 발사위치는 gate의 위치인 gateObj이 됩니다. Instatiate(objPrefab)으로 포탄을 만듭니다. 스크립트로 런타임으로 프리팹을 이용 게임오브젝트를 만들어 줍니다.  rigidbody.AddForce()를 이용해 포탄을 발사하게 만듭니다.

 void Update()
{
    // 발사 시간 판정
    passedTimes += Time.deltaTime;
    // 거리 확인
    if (CheckLength(player.transform.position))
    {
        if (passedTimes > delayTime)
        {
            // 발사!!
            passedTimes = 0;
            // 발사 위치
            Vector3 pos = new Vector3(gateObj.transform.position.x,
                                      gateObj.transform.position.y,
                                      transform.position.z);
            // Prefab 으로 GameObject 만들기
            GameObject obj = Instantiate(objPrefab, pos, Quaternion.identity);
            // 발사 방향
            Rigidbody2D rbody = obj.GetComponent<Rigidbody2D>();
            Vector2 v = new Vector2(fireSpeedX, fireSpeedY);
            rbody.AddForce(v, ForceMode2D.Impulse);
        }
    }
}

CheckLength()

자신과 targetPos를 측정해 length 보다 크면 true 작으면 false를 리턴하는 함수입니다.

bool CheckLength(Vector2 targetPos)
{
    bool ret = false;
    float d = Vector2.Distance(transform.position, targetPos);
    if (length >= d)
    {
        ret = true;
    }
    return ret;
}

포탄 만들기

Images폴더의 shell을 끌어다 씬뷰에 놓습니다. 

Tag는 Dead를 선택하고 Layer를 Shell을 새로 만들어 지정합니다.

Circle Collider 2D를 추가하고 IsTrigger를 체크합니다.

Rigidbody2D를 추가하고 Gravity Scale를 0로 설정합니다.

포탄의 스크립트 만들기

ShellController 라는 이름의 스크립트를 만든후  shell 게임오브젝트에 추가합니다.

포탄이 발사되면 일정시간(deleteTime=3)후 제거되며 무언가 접촉해도 제거됩니다.

 

변수

public float deleteTime = 3.0f; // 제거할 시간 지정

Start()

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

OnTriggerEnter2D()

void OnTriggerEnter2D(Collider2D collision) {
    Destroy(gameObject); // 무언가에 접촉하면 제거
}

레이어의 접촉 설정 수정하기

포탄은 무언가 접촉하면 사라지기 때문에 포탑과 접촉해도 사라집니다. Edit>ProjectSettings>Physics2D 에서 Ground-Shell간의 매트릭스를 꺼준다.

이제 씬뷰에서 shell을  프로젝트뷰로 끌어다 프리팹을 만들어주고 씬뷰의 shell을 지워준다

cannon오브젝트의 스크립트 컴포넌트에 shell을 끌어다 적용하고 cannon역시 프로젝트뷰로 끌어다 프리팹으로 만든다.

플레이어가 cannon에 접근하면 포탄이 발사 된다.

스위치 게임 오브젝트 만들기

Lever_off 이미지 에셋을 씬 뷰에 배치해 게임 오브젝트를 만들고 이름을 Switch로 합니다. 구분을 위해 Switch 태그를 추가하고 설정합니다. Order in Layer를 2로 하고 Box Collider2D를 추가하고 isTrigger를 체크합니다.

스위치 스크립트 작성하기

스크립트명은 SwitchAction으로 하고 Switch 게임오브젝트에 어태치합니다.

변수

public GameObject targetMoveBlock;
public Sprite imageOn;
public Sprite imageOff;
public bool on = false; // 스위치의 상태 (true:눌린 상태 false:눌리지 않은 상태)

Start()

스크립트 글로벌 변수 on이 true면 imageOn, off면 imageOff를 지정합니다.

void Start()  {
    if (on) {
        GetComponent<SpriteRenderer>().sprite = imageOn;
    } else{
        GetComponent<SpriteRenderer>().sprite = imageOff;
    }
}

OnTriggerEnter2D()

플레이어가 switch와 접촉하면 on<->off 토글시켜줍니다. movBlock도 Stop() Move()시켜줍니다.

void OnTriggerEnter2D(Collider2D col){
    if (col.gameObject.tag == "Player"){
        if (on) {
            on = false;
            GetComponent<SpriteRenderer>().sprite = imageOff;
            MovingBlock movBlock = targetMoveBlock.GetComponent<MovingBlock>();
            movBlock.Stop();
        } else{
            on = true;
            GetComponent<SpriteRenderer>().sprite = imageOn;
            MovingBlock movBlock = targetMoveBlock.GetComponent<MovingBlock>();
            movBlock.Move();
        }
    }
}

SwitchAction.cs
0.00MB

스크립트를 Switch게임오브젝트에 어태치해주고 변수를 설정해줍니다. 

Image폴더의 ImageOn ImageOff스프라이트를 끌어다 연결해줍니다. TargetMoveBlock은 프리팹을 만든후 설정하겠습니다.

여기까지 만들고 프로젝트뷰로 끌어 프리팹을 만들어 줍니다.

MovingBlock Prefab을 끌어다 씬뷰에 배치하고 변수를 다음과 같이 설정합니다.

하이라키의 MovingBlock을 끌어다 Switch 스크립트컴포넌트 변수에 연결해줍니다.

 

'유니티2D게임 > 게임에 장치 추가' 카테고리의 다른 글

7.5 돌아다니는 적 캐릭터 만들기  (0) 2023.05.20
고정 포대 만들기  (0) 2023.05.19
이동 블록 만들기  (0) 2023.05.19
7.1 대미지 블록 만들기  (0) 2023.05.17
결과 화면 추가하기  (2) 2023.05.17

+ Recent posts