Unity3D/TowerDefence 2014.07.15 06:43


강의글을 다시 검토하는중 몇가지 빠진 부분이 있어서 추가했습니다.
혹시 이전에 글을 보시고 따라하시는중에 에러가 난다면,
3. 컴포넌트 연결하기 의 윗부분을 한번더 봐주세요.
제보해주신, 레벨제로 카페에 '캐츠아이'님 감사합니다.


두번째 시간입니다.

오늘은 tk2d를 이용해서 4방향으로 움직이는 기본유닛을 만들고,

맵에 배치해서 이동하는것까지 구현해보겠습니다.


unit.zip


1. 리소스 준비

우선 기본유닛을 하나 만들도록 하겠습니다.

미리 준비해놓은 스타크레프트의 마린 스프라이트를 프로젝트 폴더에 넣습니다.



스프라이트는 상하좌우 로 걸어가는 애니메이션 이미지들이 있고, 죽었을때 애니메이션 이미지가 들어있습니다.


새로운 스프라이트 컬랙션(unit_marine_sc)을 만들어서 마린폴더를 통째로 드래그 해놓습니다.



마린 이미지의 중심점을 발쪽으로 옮기겠습니다.

모든스프라이트들이 선택된 상태에서 anchor를 "Lower Center" 로 변경합니다.

아래쪽에 그림처럼 apply버튼이 생기는데 버튼을 누르면 선택된 스프라이트가 같은설정으로 변경됩니다.



anchor를 변경한뒤 commit버튼을 꼭 눌러주세요.


다음으론 스프라이트 애니메이션 파일을 만들겠습니다.

SpriteCollection을 만들때처럼

Project텝에서 create->tk2d->SpriteAnimation을 클릭하면,

SpriteAnimation prefab이 생성 됩니다.

"unit_marine_sa" 라고 이름을 변경해준뒤 인스팩터에서 "Open Editor" 버튼을 클릭하면 Sprite Animation편집창이 뜹니다.


왼쪽 위에 create 버튼을 을 클릭하고, clip을 선택합니다.



아래 그림처럼 Name : "walk_up", frame_rate : 20, Collection:unit_marine_sc, sprite:"walk_up_00"

으로 선택후에 "Autofill 1..9"버튼을 클릭하면 walk_up으로시작하는 파일을 순서대로 배치해줍니다.



다시 create버튼을 누른뒤 동일한 방법으로,

walk_down, walk_left, walk_right, destroy 클립들도 만들어줍니다.

이때, destroy 클립만 LoopMode를 "Once"로 바꿔줍니다.

모든설정이 끝나면 commit버튼을 눌러주세요.


지금까지 만든 스프라이트 애니메이션을 화면에 올려보겠습니다.

하이라키 창에서 create -> tk2d -> Sprite With Animator 를 누르면 새로운 "AnimatedSprite" 라는 게임오브젝트가 생깁니다.



이름을 "unit_marine"라고 바꾸고, 인스팩터를 보면 다음과 같은데,


Anim Lib를 눌러보면 샘플에서 제공되는 animation파일과, 우리가 방금 만든 "unit_marine_sa"파일이 목록에 보입니다.

"unit_marine_sa"를 선택하고 Clip을 누르면, 위에서 작업한 "walk_up", "walk_down", "walk_left", "walk_right", "destroy" 로 클립이름들이 표시됩니다.

아래 "Play automatically"를 체크해주면 씬을 시작할때, 설정된 애니메이션을 자동으로 플레이 해주게 됩니다.

(해제 되어있다면 멈춰있는 스프라이트로 보입니다.)

"Play automatically"를 체크해주고 유니티 에디터의 "Play버튼"을 눌러보면,

"Game화면"에 제자리걸음을 하고 있는 마린을 볼수 있습니다.(안보인다면 마린오브젝트의 좌표를 확인해보세요.)


이번 스크립트 작업은

타일맵상의 좌표를 편하게 표시하기위한 "Point.cs" 와, 

유닛을 방향에 맞게 걸어다니게 하고, 유닛의 속도를 정할수 있는 "UnitBase.cs",

게임의 전반적인 관리를 맞게 될 "GameManager.cs"를 만들도록 하겠습니다.

Point.cs

using System;
namespace common
{
		public class Point
		{
		public int x = 0;
		public int y = 0;
		public Point(int _x, int _y)
			{
				x = _x;
				y = _y;
			}
		}
}


"Point.cs" 는 타일맵에서 간단히 x,y 좌표를 가지는 객체입니다.



UnitBase.cs


using UnityEngine;
using System.Collections;
using common;

public class UnitBase : MonoBehaviour {

	public float moveSpeed = 1.0f;
	public tk2dSpriteAnimator spr;
	private enum CHAR_ANI {UP, DOWN, LEFT, RIGHT, DESTROY};
	private string[] charAniStr = new string[]{"walk_up","walk_down","walk_left","walk_right","destroy"};
	// Use this for initialization
	void Start () {
        spr = this.GetComponent();
	}
	private bool isMoveAble = false;
	// Update is called once per frame
	void Update () {
		if(!isMoveAble)return;
		float _speed = GameManager.instance.cellSize * Time.deltaTime * moveSpeed;
		int tx = nextPoint.x - startPoint.x;
		int ty = nextPoint.y - startPoint.y;

		float dx = _speed * tx;
		float dy = -_speed * ty;
		float rx = (nextPoint.x * GameManager.instance.cellSize + GameManager.instance.cellSize/2.0f) - this.transform.localPosition.x;
		float ry = (-nextPoint.y * GameManager.instance.cellSize - GameManager.instance.cellSize/2.0f) - this.transform.localPosition.y;
		bool isCloseX = false;
		bool isCloseY = false;
		if(Mathf.Abs(dx)>Mathf.Abs(rx)||dx==0){dx = rx;isCloseX = true;}
		if(Mathf.Abs(dy)>Mathf.Abs(ry)||dy==0){dy = ry;isCloseY = true;}
		this.transform.localPosition += new Vector3(dx , dy , 0);
		if(isCloseX && isCloseY)
		{
			if(pathArr.Length <= pathIndex + 1)
			{
				isMoveAble = false;
				return;
			}
			setNextPoint();
		}
	}
	private Point[] pathArr;
	private Point startPoint;
	private Point nextPoint;
	private int pathIndex =0;
	public void setStartPoint(Point p)
	{
		startPoint = p;
		getPath();
		nextPoint = pathArr[pathIndex];
		showCharDir();
		isMoveAble = true;
	}
	public void getPath()
	{
		pathArr = new Point[]{new Point(startPoint.x + 1, startPoint.y), new Point(startPoint.x + 2, startPoint.y), new Point(startPoint.x + 2, startPoint.y+1), new Point(startPoint.x + 2, startPoint.y)};
		pathIndex = 0;
	}
	private void setNextPoint()
	{
		startPoint = nextPoint;
		pathIndex++;
		nextPoint = pathArr[pathIndex];
		showCharDir();
	}
	private void showCharDir()
	{

		if(startPoint.x<nextPoint.x)
			spr.Play (charAniStr[(int)CHAR_ANI.RIGHT]);
		else if(startPoint.x>nextPoint.x)
			spr.Play (charAniStr[(int)CHAR_ANI.LEFT]);
		else if(startPoint.y>nextPoint.y)
			spr.Play (charAniStr[(int)CHAR_ANI.UP]);
		else if(startPoint.y<nextPoint.y)
			spr.Play (charAniStr[(int)CHAR_ANI.DOWN]);
		spr.ClipFps *= moveSpeed;
	}
}

실제로 유닛들이 맵위에서 정해진 속도와 방향으로 움직이게 해주는 스크립트 입니다.


변수들

private Point[] pathArr; //유닛이 지나갈 경로입니다.차후에 pathfinder를 통해서 path경로를 미리 받아두고,                          //맵이 변경될때마다 새로 갱신하게 됩니다.

private Point startPoint; //현재 유닛이 속해있는 타일맵 좌표입니다.
private Point nextPoint; //유닛이 가야할 다음 타일의 좌표입니다.


함수들

public void setStartPoint(Point p)

최초로 유닛이 생성될때 유닛의 StartPoint를 지정해주고,유닛이 움직여야할경로등을 생성해줍니다.

public void getPath()

현재 위치부터 최종목적지까지의 지나갈수 있는경로를 계산합니다. (지금은 패스파인더를 적용하지 않고, 테스트용으로 몇몇 좌표를 미리 넣어놨습니다.)

private void setNextPoint()

유닛이 다음타일에 도착했을때, 가야할 nextPoint를 새로 셋팅해줍니다.

private void showCharDir()

startPoint와, nextPoint를 계산해 유닛Sprite의 방향을 정해줍니다.



Update()매소드 안을 보면,

float _speed = GameManager.instance.cellSize * Time.deltaTime * moveSpeed;

"GameManager.instance.cellSize" 는 타일 한칸의 크기를 저장하고 있습니다.

"moveSpeed"는 float으로, 기본값은 1일경우 타일 1칸을 1초에 움직이게 해줍니다.

2일경우 1칸움직이는데 0.5초가 걸립니다.


int tx = nextPoint.x - startPoint.x;

다음목적지 포인트와 현재 포인트의 차이를 계산해서,

float dx = _speed * tx;
 float rx = (nextPoint.x * GameManager.instance.cellSize + GameManager.instance.cellSize/2.0f) - this.transform.localPosition.x;

목적지까지 유닛이 움직이는 거리를 계산하고,

bool isCloseX = false;
 if(Mathf.Abs(dx)>Mathf.Abs(rx)||dx==0){dx = rx;isCloseX = true;}

목적지 근처까지 왔는지 체크합니다.

if(isCloseX && isCloseY)
{
     if(pathArr.Length <= pathIndex + 1)
     {
         isMoveAble = false; return;
     }
     setNextPoint();
}

x,y좌표가 모두 목적지 근처까지 왔다면, 목적지와 현재 좌표를 새로 갱신해줘야하는데,

더이상 목적지가 없다면 더이상 움직이지 않게 합니다.


GameManager.cs


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

public class GameManager : MonoBehaviour {
	private static GameManager _instance = null;
	public static GameManager instance
	{
		get{
			if(_instance == null)Debug.LogError("GameManager is NULL");
			return _instance;
		}
	}

	void Awake()
	{
		_instance = this;
	}
	//=========================================
	public float cellSize = 40.0f;
	public int[,] wallMap = 
	{{1, 1, 1, 1, 1, 11, 11, 11, 1, 1, 1, 1, 1},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		{1, 1, 1, 1, 1, 111, 111, 111, 1, 1, 1, 1, 1}};
	[HideInInspector]
	public List<UnitBase> unitList = new List<UnitBase>();
	public Transform unit_field;
	public UnitBase unit_marine;

	private void init()
	{
		unitList.Clear();
	}
	void addUnit()
	{
		UnitBase unit = Instantiate(unit_marine) as UnitBase;
		unit.transform.parent = unit_field;
		Point startPoint = getStartPoint();
		unit.transform.localPosition = new Vector3(startPoint.x * cellSize + cellSize/2.0f, -startPoint.y * cellSize - cellSize/2.0f);
		unit.setStartPoint(startPoint);
               unitList.Add(unit);
	}
	Point getStartPoint()
	{
		//check startPoints
		List<Point> startPointList = new List<Point>();
		int _w = wallMap.GetLength(0);
		int _h = wallMap.GetLength(1);
		int x,y;
		for (x = 0; x < _w; x++)
		{
			for(y = 0; y < _h; y++)
			{
				if(wallMap[x,y] >= 10 && wallMap[x,y] <= 100)
				{
					startPointList.Add(new Point(x,y));
				}
			}
		}
		if(startPointList.Count == 0)
		{
			Debug.LogError("Not Found Start Position");
			return null;
		}
		int ranIdx = Random.Range(0,startPointList.Count);
		return startPointList[ranIdx];
	}

	void OnGUI()
	{
		if(GUI.Button( new Rect( 10, 10, 100, 40), "Add Unit"))
		{
			addUnit();
		}
	}
}

"GameManager.cs"는 게임의 전체를 관리하는 클레스입니다. 게임시작, 종료, 점수 합산, 초기화 등등 거의 모든것들이 이 "GameManager"를 통해서 이루어지게 됩니다.

"GameManager"는 싱글톤 형태로 만들어졌습니다.(instance가 null인경우 생성해주는부분만 없습니다.)

게임내에 어디서든 GameManager.instance로 GameManager에 접근할수 있습니다.


변수들

public float cellSize = 40.0f; //타일맵 한칸당 크기 public int[,] wallMap = {...}; //이전 BgCellDisplayer.cs에 있던 맵정보                                //GameManager에서 가지고 있도록 변경함. public List<UnitBase> unitList = new List<UnitBase>(); //화면에 있는 유닛들. public Transform unit_field; //유닛들을 올려놓을 부모 Transform public UnitBase unit_marine; //화면에 추가할 유닛 prefab

함수들

private void init()

초기화 함수. 아직은 별다른일을 하진 않습니다.

void addUnit()

unit_marine에 연결된 prefab을 화면에 인스턴스화 시키고, unit_field아래로 위치시켜줍니다.

Point getStartPoint()

wallMap에서 스타팅포인트를 검색(10이상 100 이하인값)해서 그중하나의 포인트를 반환해줌.

void OnGUI()

유니티에서 제공하는 UI를 쓸수 있는 함수. 기본적인 버튼이나, 라벨등을 쉽게 만들수 있어서 테스트 할때 좋습니다. 여기서 하는일은 화면에 "Add Unit" 이라는 버튼을 만들고 버튼을 누를때마다 유닛을 하나씩 추가합니다.


BgCellDisplayer.cs


using UnityEngine;
using System.Collections;

public class BgCellDisplayer : MonoBehaviour {
	public BgCell bgcell;

	private GameManager gm;
	// Use this for initialization
	void Start () {
		gm = GameManager.instance;
		showBgCells();

	}
	public void showBgCells()
	{
		int _w = gm.wallMap.GetLength(0);
		int _h = gm.wallMap.GetLength(1);
		int x,y;
		for (x = 0; x > _w; x++)
		{
			for(y = 0; y > _h; y++)
			{
				BgCell bc = Instantiate (bgcell) as BgCell;
				bc.transform.parent = this.transform;
				bc.transform.localPosition = new Vector3( 40 * x, -40 * y, 0);
				if(gm.wallMap[x,y] == 1)
				{
					bc.isVisible = false;
				}
				else if(gm.wallMap[x,y] >= 10 && gm.wallMap[x,y] >= 100)
				{
					bc.isVisible = true;
					bc.setStart();
				}
				else if(gm.wallMap[x,y] >= 110)
				{
					bc.isVisible = true;
					bc.setGoal();
				}
				else
				{
					bc.isVisible = true;
					bool isBlack = ((x + (y%2))%2 == 1);
					bc.setBlack(isBlack);
				}
			}
		}
	}
}



BgCellDisplayer.cs는 이전과 크게 바뀌지 않습니다.
wallMap 데이터가 GameManager로 옮겨지면서 같은 클레스 안에 있던 wallMap을 지우고 GameManager의 값을 가져다 쓰도록 변경하였습니다.

private GameManager gm; // GameManager 객체를 선언합니다.


void Start () {

gm = GameManager.instance;
}

Start함수의 제일처음에서 gm객체에 GameManager.instance를 가저와서 넣어줍니다.
앞으로는 gm.wallMap과 같이 바로바로 GameManager의 public 함수, 변수를 사용할수 있습니다.


3. 컴포넌트 연결하기.

이제 만든 스크립트들을 GameObject들과 연결하고, 각각 컴포넌트끼리 연결해서 결과물을 보도록 하겠습니다.

먼저 위에서 만들었던 "unit_marine" 이란 게임오브젝트를 프리팹으로 변경하는 방법입니다.

여러가지 방법이 있지만, 가장 간단하게, 하이라키에 있는 오브젝트를 프로젝트 텝에 적당한 경로를 만들고 드래그 해놓습니다.

새로 만들어진 "unit_marine" 프리팹을 선택하고 인스펙터의 UnitBase컴포넌트의 Spr 란에 바로 위에있는 Tk2dSpriteAnimator 컴포넌트를 드래그 해 놓습니다.




그리고, 다음과 같이 하이라키 뷰에 "GameManager", "gameField" 라는 이름으로 빈 GameObject들을 만들어 줍니다.



"GameManager"의 transform은 특별히 상관 없지만,"gameField"는 "BG"의 transform과 동일하게 맞춰주세요.(z 값은 "BG"보다 약간 작게 넣어주세요.)

하이라키뷰에서 "GameManager"를 선택한후, 위에서 만든 "GameManager.cs" 파일을 "GameManager" 추가 해줍니다.(drag&drop)

"GameManager" 인스팩터창에서 아래 그림처럼 [unitField]에는 "gameField" 오브젝트를, [unit]에는 "unit_marine" prefab을 각각 드래그해서 연결해줍니다. 

유니티 에디터의 "play" 버튼을 눌러 보면 그림처럼 좌측상단에 "add unit" 이란 버튼이 보이게 됩니다.



버튼을 누르게 되면 녹색칸중 랜덤으로 마린이 추가 되서, "우측->우측-> 아래 -> 위" 순서로 걸어가게 됩니다.



다음 시간엔 마린들이 출발점에서 도착점까지 경로를 찾아서 갈수 있는 pathFinder와, 임시로 벽을 만들어 경로를 수정할수 있도록 해보겠습니다.




신고
posted by andwhy

티스토리 툴바