드디어 타워디펜스 게임 만들기 강좌가 끝났습니다.

처음생각했던것과 달라진부분도 많고 만들지 못한것도 많았네요.

최대한 읽는분들이 이해하기 쉽게 하려고 했었는데, 그래도 많이 복잡하게 느껴졌을것 같습니다.

실제로 누군가를 앞에서 알려드리진 못했지만, 이렇게나마 누군가에게 설명을하려니까 어려운부분이 상당히 많네요.

개인적으로도 많은공부를 하게 된것 같습니다.

혼자 만들때는 그냥그냥 되는대로 뚝딱뚝딱 만들면 끝났지만 다시 하나씩 설명하려고 하니 정말 많이 준비하고, 정말 많이 생각해야하는군요.

다른 강좌나, 책쓰시는분들이 너무나도 대단해보입니다.

허접하게 나마 게임하나 강좌를 끝내보니 이제서야 어떻게 해야할지 조금 느껴지는것 같습니다.

앞으로 기회 되면 또다른 강좌를 좀더 많이 준비해서 만들어보도록 하겠습니다.


그동안 지루한 강좌 따라와 주셔서 감사하고, 고맙습니다.

특히 글올리면 바로바로 피드백 주신 "캣츠아이"님, "순순"님 정말 정말 감사드립니다.


마지막으로 지금까지 함께 만든 타워디펜스 게임 올리고 인사드리겠습니다.
(waveData와 벨런싱으좀 맞춰봤습니다...저는 11스테이지 이후론 못가봤네요;;)



감사합니다.







신고
Posted by andwhy

마지막 시간이네요.

오늘 할건 지금까지 한거에 비하면 정말 간단한것입니다.

점수 보여주기와, GameOver, 그리고 Game 재시작.

크게 이 세가지입니다.


1. Score

타워 디펜스 게임에서는 점수가 크게 의미는 없을것 같습니다. 퍼즐게임이나 기록갱신 게임들에서는 얼마나 많은점수를 내느냐가 가장 중요한점이지만, 디펜스게임의 경우는 스테이지를 몇까지 클리어 하는지가 더 중요하니까요...

하지만, 점수가 없으면 뭔가 좀 허전해서...넣어보겠습니다.(타워디펜스도 기획에따라 점수가 정말 중요해질수도 있으니까요..)

우선 저희는 유닛을 죽이고 얻는 골드를 점수로 간주하도록 하겠습니다. 유닛마다 점수를 따로 설정하고, 타워를 짓거나 업그레이드 할때도 점수를 추가해줄수 있겠지만, 가장 간단한 방법으로 얻어지는 골드를 점수로 환산하도록 하겠습니다.


먼저 게임 점수를 저장할 gameScore변수를 만듭니다.

GameManager.cs

외부에서 게임점수를 추가할수 있도록 addScore()라는 함수도 만들어줍니다.

GameManager.cs

게임이 시작될때는 점수를 0점으로 초기화 시켜줍니다.

GameManager.cs

이제 화면에 점수를 표시해주기위해서, OnGUI() 안에 Gold를 표기한 다음줄에 표시하도록 코드를 수정해줍니다.

GameManager.cs

GameManager.cs 에서 해야할일은 끝났고, 이젠 유닛을 죽일때 GameManager addScore를 호출해주기만 하면됩니다.

처음에 말한것처럼, 유닛을 죽이고 골드를 획득할때(addGold) 획득한골드만큼 점수를 올려줄 것입니다. UnitBase에서 addGold해주는 부분을 찾아서 바로 밑에 addScore도 해주면 됩니다.

UnitBase.cs


이제 화면을 보면 아래처럼 Gold표시 아래에 Score도 표시됩니다.


2. GameOver

이번엔 GameOver를 만들어보겠습니다.
우리게임은 유닛이 목표점까지 못오도록 막는게 목적입니다. 하지만 유닛을 막지 못하고, 목표점까지 유닛이 침범한다면, 베이스캠프(?) 의 체력을 하나씩 줄어들고, 체력이 0이되면 게임은 끝납니다.

먼저 뭐부터 해야할까요?

베이스캠프(?) 의 최대 체력과 현재 체력 값을 만들어줍니다. 인스펙터에서 손쉽게 수정가능하도록 public 으로 선언하겠습니다.
그리고 현재 게임오버 상태인지 아닌지를 체크하는 isGameOver 값도 하나 추가 하겠습니다.

GameManager.cs

다음으론 유닛이 도착했을때, 현재 체력을 하나씩 깎고, 체력이 0인지 체크하는 reachUnit() 함수를 추가합니다.

GameManager.cs

기존에는 유닛이 목적지에 도착하면 자기 자신을 Destroy시키고 GameManagerremoveUnit() 를 호출해줬는데, 이제는 현재 체력을 하나씩 깍고, 체력이 0이 되면 게임 오버상태로 변경하기 위해서 reachUnit() 이란 함수를 새로 만들었습니다.

isGameOverisGameStart만 으로 게임진행상태를 판단하기 어려울 경우를 위해서 추가하였습니다.
GameOver 상태에선 새로운 타워를 짓거나, 타워를 제거하는일을 할수는 없습니다. 하지만, wave를 시작하기전에 미리 몇몇 타워를 짓고 싶다면 isGameStart 라는 변수 하나만으론 판단하기 힘듭니다.


다음으론 게임시작시, 베이스캠프의 체력을 초기화 시켜주고, isGameOverfalse로 변경시켜줍니다.

GameManager.cs


이제 게임이 시작되면, 게임상태는 게임은 스타트 되었고 게임오버는 되지 않은 상태가 됩니다.
그리고 게임점수는 0점 베이스캠프의 체력은 최대 채력(10) 이 되게 될겁니다.

이번엔 다시 UnitBase.cs 로 넘어가서, 유닛이 목적지 까지 도착할때 GameManagerreachUnit()함수를 호출하도록 하겠습니다.

UnitBase.cs

위의 그림처럼 Update함수안에 목적지에 도착했는지 판단하는 부분이 있는데, 기존의 removeUnit()을 호출하는것을 reachUnit()을 호출하도록 변경해줍니다.


이상태로 게임을 한번 진행해서, 정상적으로 게임오버가 되는지 돌려보겠습니다.
정상적으로 게임오버가 된다면, 10마리 이상의 유닛을 놓치면, GAMEOVER란 메시지가 표시되고 더이상 게임이 진행되면 안됩니다.




실제로 돌려보면.....
GAMEOVER 로그가 여러번 찍히고, Life는 0 이 되어도 계속 내려갑니다.

이문제는 게임이 끝났지만, 유닛과, 타워가 계속 동작하는게 문제네요.

UnitBase.csTowerBase.cs 파일에 GameOver인지를 검사하는 코드를 넣어보도록 하겠습니다.

UnitBase.cs

Update중에 GameManagerisGameOver  상태이면 스프라이트 애니메이션을 멈추고 더이상 아무것도 하지 않습니다.
만일 spr.Stop()구문을 빼버리면 유닛이 이동하진 않지만 제자리 걸음을 하고 있을껍니다.

TowerBase.cs


TowerBase 에서도, GameOver상태이면 바로 리턴시켜 버립니다. 이러면 유닛을 바라보거나, 공격하는 행동은 더이상 하지 않습니다.


그리고 한가지 놓친곳이 있는데, GameManager 에서 다음 Wave를 체크하는 부분과, 현재 WaveData들을 업데이트 해주는부분도, GameOver상태일때는 동작해선 안됩니다.

아래 그림처럼 각각 함수 윗부분에 게임중이 아닐경우엔 return 시켜 더이상 동작하지 않도록 합니다.

GameManager.cs


이제 게임을 테스트 해보면 게임오버까지 정상적으로 동작되는것을 볼수 있습니다.


3. 게임재시작.

게임이 끝난뒤 게임을 다시 시작할수 있게 만들어주면 됩니다.
게임의 점수나, 획득 골드를 처음상태로 바꿔주고, 게임화면에 나와있는 유닛들과, 타워들을 제거해준뒤 다시 게임을 시작하면됩니다.

화면의 유닛과, 타워를 모두 제거하는 함수를 만듭니다.

GameManager.cs


다음으론 게임초기화 함수를 만들겠습니다.

GameManager.cs

gameInit()은 방금 만든 clearTowers, clearUnits 함수와 현재 wave데이터를 초기화 하고, 기타 그외의 점수, 골드 등의 게임데이터들을 초기화 해줍니다.
그리고 기존의 startGame()isGameStart를 제외한 다른 초기화코드를 제거했는데,
이는 게임이 종료되었을때, "확인"버튼을 누르면 게임이 초기화 되고, 이상태에서 미리 타워들을 배치할수 있도록 하기위해서 gameInit()때만 초기화를 해줍니다.
(startGame()에서도 초기화 시키게되면 gold점수등이 잘못초기화 될수도 있습니다.)

마지막으로 게임오버시에 화면중앙에 "GameOver" 라는 버튼을 만들고, 버튼을 누르면 초기화 시키도록 OnGUI에 코드를 추가해줍니다.

GameManager.cs


이제 게임오버가 되면 다음과 같이 화면 중앙에 GameOver 버튼이 보이고, 버튼을 누르면 기존의 유닛,타워들이 사라지고, 초기화 되는것을 볼수 있습니다.



이번 강의는 여기까지입니다.

그동안 힘등 강의 따라와주셔서 정말 감사합니다.

마지막으로 게임코드 공유 하고 끝내도록 하겠습니다.


4. 게임코드


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;
		init();
	}
	//=========================================
	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>();
	[HideInInspector]
	public Dictionary<Point, TowerBase> towerDic = new Dictionary<Point, TowerBase>();
	public BgCellDisplayer bgGrid;
	public Transform unit_field;
	public UnitBase unit_marine;
	public TowerBase tower;
	public TowerUIMenu towerMenu;
	
	public int startedGold = 100;
	public int currentGold = 0;
	public int maxLife = 10;
	public int currentLife = 10;
	public bool isGameOver = false;

	public Camera mainCam;

	private int gameScore = 0;
	private bool isGameStart = false;
	public int waveIndex = 0;
	//-----------waveData
	public WaveData[] waveDataArray;
	private List<WaveData> currentWaveDataList = new List<WaveData>();
	private List<WaveData> removeWaveDataList = new List<WaveData>();
	private float nextWaveDelay = 0f;
	private float nextWaveDelayCount = 0;
	
	public void addWaveData(WaveData wd)
	{
		currentWaveDataList.Add(wd);
	}
	public void removeWaveData(WaveData wd)
	{
		removeWaveDataList.Add (wd);
	}


	private void init()
	{
		unitList.Clear();
		towerDic.Clear();
		initPathFinder();

		currentGold = startedGold;
	}

	public void addUnit(float hp, float speed, int gold)
	{
		UnitBase unit = Instantiate(unit_marine) as UnitBase;
		unitList.Add(unit);
		unit.setUnit(hp, speed, gold);
		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);
		
	}

	void addTower(Point p)
	{
		
		TowerBase tw = Instantiate(tower) as TowerBase;
		tw.transform.parent = unit_field;
		tw.transform.localPosition = new Vector3(p.x * cellSize + cellSize/2.0f, -p.y * cellSize - cellSize);
		towerDic.Add(p, tw);
		
		useGold(tw.buildPrice);
	}
	public void addGold(int g)
	{
		currentGold += g;
	}
	public void addScore(int s)
	{
		gameScore += s;
	}
	public void useGold(int g)
	{
		currentGold -= g;
	}
	public bool checkGold(int g)
	{
		return (currentGold>=g);
	}
	public void reachUnit(UnitBase ub)
	{
		currentLife --;
		removeUnit(ub);
		if(currentLife<=0)
		{
			Debug.Log("GAMEOVER");
			isGameStart = false;
			isGameOver = true;
		}
	}
	public void removeUnit(UnitBase ub)
	{
		unitList.Remove(ub);
	}
	void researchPathUnits()
	{
		foreach(UnitBase ub in unitList)
		{
			if(ub!=null)
			{
				ub.getPath();
			}
		}
	}
	Point getStartPoint()
	{
		List<Point> startPointList = getStartPointList();
		if(startPointList.Count == 0)
		{
			Debug.LogError("Not Found Start Position");
			return null;
		}
		int ranIdx = Random.Range(0,startPointList.Count);
		return startPointList[ranIdx];
	}
	List<Point> getStartPointList()
	{
		//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));
				}
			}
		}
		return startPointList;
	}
	public void initPathFinder()
	{
		PathFinder.instance.setMapData(wallMap);
	}
	void OnGUI()
	{
		if(isGameOver)
		{
			if(GUI.Button( new Rect( (Screen.width - 200)/2.0f, (Screen.height - 40)/2.0f, 100, 40), "GameOver"))
			{
				
				gameInit();
			}
		}
		else if(!isGameStart)
		{
			if(GUI.Button( new Rect( 10, 10, 100, 40), "Start Game"))
			{
				startGame();
			}
		}
		else 
		{
			if(GUI.Button( new Rect( 10, 10, 100, 40), "nextWave!!"))
			{
				nextWaveDelay = nextWaveDelayCount;
			}
		}
		GUI.Label(new Rect( (Screen.width - 100)/2.0f, 10, 100, 50), "GOLD : "+currentGold+" \n"+"Score : "+gameScore);
		if(waveIndex < waveDataArray.Length)
			GUI.Label(new Rect( (Screen.width - 200), 50, 200, 50), 
			          string.Format("NextWave [{0}] : {1:F2}",waveDataArray[waveIndex].name,(nextWaveDelay-nextWaveDelayCount)));
		GUI.Label(new Rect( (Screen.width - 100), 10, 100, 50), string.Format("Life {0} / {1}",currentLife, maxLife));
	}
	void Update()
	{
		checkNextWave();
		//updateWaveDatas
		updateWaveDataList();

		if(Input.GetMouseButtonDown(0) && !towerMenu.isShow) 
		{
			Vector2 pos = Input.mousePosition;
			Vector3 mouseP = mainCam.ScreenToWorldPoint(pos) - unit_field.TransformPoint(Vector3.zero);
			Point myPos = new Point((int)(mouseP.x/cellSize), -(int)(mouseP.y/cellSize));
			if(towerDic.ContainsKey(myPos))
			{
				showMenu(towerDic[myPos]);
			}
			else 
			{
				buildTower(myPos);
			}
		}
	}
	void gameInit()
	{
		gameScore = 0;
		waveIndex = 0;
		currentGold = startedGold;
		isGameOver = false;
		currentLife = maxLife;
		nextWaveDelay = 0f;
		nextWaveDelayCount = 0;

		clearTowers();
		clearUnits();
		currentWaveDataList.Clear();

	}
	void startGame()
	{
		isGameStart = true;
	}
	private void clearTowers()
	{
		foreach(Point keyP in towerDic.Keys)
		{
			if(keyP.x<0||keyP.y<0 || keyP.x >= wallMap.GetLength(0) || keyP.y >= wallMap.GetLength(1))return;
			wallMap[keyP.x, keyP.y] = 0;
			Destroy(towerDic[keyP].gameObject);
		}
		towerDic.Clear();
		bgGrid.refreshDisplay();
		PathFinder.instance.setPath();
		PathFinder.instance.setCheckMode(false);
	}
	private void clearUnits()
	{
		foreach(UnitBase unit in unitList)
		{
			Destroy( unit.gameObject);
		}
		unitList.Clear();
	}
	void checkNextWave()
	{
		if(!isGameStart||isGameOver)return;
		if(nextWaveDelay<0)return;
		nextWaveDelayCount += Time.deltaTime;
		if(nextWaveDelay<nextWaveDelayCount)
		{
			nextWaveDelayCount = 0;
			if(waveIndex >= waveDataArray.Length)
			{
				nextWaveDelay = -1;
			}
			else
			{
				addWaveData(waveDataArray[waveIndex].clone());
				nextWaveDelay = waveDataArray[waveIndex].nextWaveDelay;
				waveIndex++;
			}
		}
	}

	void updateWaveDataList()
	{
		if(!isGameStart||isGameOver)return;
		foreach( WaveData wd in currentWaveDataList)
		{
			wd.update();
		}
		foreach( WaveData wd in removeWaveDataList)
		{
			if(currentWaveDataList.Contains(wd))currentWaveDataList.Remove(wd);
		}
		removeWaveDataList.Clear();
	}

	bool checkReachAble()
	{
		PathFinder.instance.setCheckMode(true);
		foreach(Point sp in getStartPointList())
		{
			if(PathFinder.instance.getPath(sp,100+wallMap[sp.x,sp.y]) == null)
			{
				Debug.Log("StartPoint Path NULL");
				return false;
			}
		}
		foreach(UnitBase unit in unitList)
		{
			if(!unit.getPath())
			{
				Debug.Log("Unit Path NULL");
				return false;
			}
		}
		PathFinder.instance.setPath();
		return true;
	}
	void buildTower(Point p)
	{
		if(p.x<0||p.y<0 || p.x >= wallMap.GetLength(0) || p.y >= wallMap.GetLength(1))return;
		int prevIndex = wallMap[p.x, p.y];
		
		if(wallMap[p.x, p.y] == 0)wallMap[p.x, p.y] = 2;
		else return;
			
		if(checkGold(tower.buildPrice) && checkReachAble())
		{
			bgGrid.refreshDisplay();
			researchPathUnits();
			addTower(p);
		}
		else 
		{
			wallMap[p.x, p.y] = prevIndex;
		}
		PathFinder.instance.setCheckMode(false);
	}
	public void sellTower(TowerBase tw)
	{
		if(!towerDic.ContainsValue(tw))return;
		addGold(tw.totalPrice/2);
		removeTower(tw);
	}
	public void removeTower(TowerBase tw)
	{
		if(!towerDic.ContainsValue(tw))return;
		foreach(Point keyP in towerDic.Keys)
		{
			if(towerDic[keyP] == tw)
			{
				if(keyP.x<0||keyP.y<0 || keyP.x >= wallMap.GetLength(0) || keyP.y >= wallMap.GetLength(1))return;
				wallMap[keyP.x, keyP.y] = 0;
				towerDic.Remove(keyP);
				break;
			}
		}
		Destroy(tw.gameObject);
		bgGrid.refreshDisplay();
		checkReachAble();
		PathFinder.instance.setPath();
		PathFinder.instance.setCheckMode(false);
	}
	void showMenu(TowerBase tw)
	{
		towerMenu.showMenu(tw);
	}
}



UnitBase.cs


using UnityEngine;
using System.Collections;
using common;

public class UnitBase : MonoBehaviour {
	public float maxHp = 100;
	public float curHp = 100;
	public int gainGold = 5;

	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 () {
		curHp = maxHp;
	}
	private bool isMoveAble = false;
	// Update is called once per frame
	void Update () {
		if(!isMoveAble)return;
		if(GameManager.instance.isGameOver){spr.Stop();return;}
		float cellSize = GameManager.instance.cellSize;
		float _speed = cellSize * Time.deltaTime * moveSpeed;
		float rx = (nextPoint.x * cellSize + cellSize/2.0f) - this.transform.localPosition.x;
		float ry = (-nextPoint.y * cellSize - cellSize/2.0f) - this.transform.localPosition.y;
		float dx = _speed * makeNomal(rx);
		float dy = _speed * makeNomal(ry);
		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);
		spr.Sprite.SortingOrder = -(int)this.transform.localPosition.y;
		if(isCloseX && isCloseY)
		{
			if(pathArr.Length <= pathIndex + 1)
			{
				isMoveAble = false;
				//GameManager.instance.removeUnit(this);
				GameManager.instance.reachUnit(this);
				Destroy(this.gameObject);
				return;
			}
			setNextPoint();
		}
	}

	public void setUnit(float hp, float speed, int gold) {
		curHp = maxHp = hp;
		moveSpeed = speed;
		gainGold = gold;
	}

	int makeNomal(float f)
	{
		float k = 0.1f;
		if(f>k)return 1;
		else if(f<-k)return -1;
		else return 0;
	}
	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 bool getPath()
	{
		float cellSize = GameManager.instance.cellSize;
		startPoint = new Point((int)(this.transform.localPosition.x/cellSize),-(int)(this.transform.localPosition.y/cellSize) );
		int wallMapIndex = GameManager.instance.wallMap[startPoint.x,startPoint.y];
		if(wallMapIndex > 0 && wallMapIndex < 10)return true;

		Point[] pArr = PathFinder.instance.getPath(startPoint, 111);
		if(pArr == null){Debug.Log("NULL path");return false;}
		pathArr = pArr;

		if(nextPoint != null && pathArr.Length > 1 && nextPoint.isEqual(pathArr[1]))pathIndex = 1;
		else pathIndex = 0;
		nextPoint = pathArr[pathIndex];
		showCharDir();
		return true;
	}
	public void attackMe(float dmg)
	{
		curHp -= dmg;
		if(curHp < 0)
		{
			isMoveAble = false;
			spr.Play ("destroy");
			GameManager.instance.addGold(gainGold);
			GameManager.instance.addScore(gainGold);
			GameManager.instance.removeUnit(this);
			spr.AnimationCompleted = unitDestoryAniComplete;
		}
	}
	private void unitDestoryAniComplete(tk2dSpriteAnimator sprite, tk2dSpriteAnimationClip clip)
	{
		Destroy(this.gameObject);
	}
	private void setNextPoint()
	{
		startPoint = nextPoint;
		pathIndex++;
		nextPoint = pathArr[pathIndex];
		showCharDir();
	}
	private void showCharDir()
	{
		
		float cellSize = GameManager.instance.cellSize;
		float nx = (nextPoint.x * cellSize + cellSize/2.0f);
		float ny = (-nextPoint.y * cellSize - cellSize/2.0f);
		if(this.transform.localPosition.x<nx)
			spr.Play (charAniStr[(int)CHAR_ANI.RIGHT]);
		else if(this.transform.localPosition.x>nx)
			spr.Play (charAniStr[(int)CHAR_ANI.LEFT]);
		else if(this.transform.localPosition.y<ny)
			spr.Play (charAniStr[(int)CHAR_ANI.UP]);
		else if(this.transform.localPosition.y>ny)
			spr.Play (charAniStr[(int)CHAR_ANI.DOWN]);
		spr.ClipFps *= moveSpeed;
	}
}


TowerBase.cs


using UnityEngine;
using System.Collections;

public class TowerBase : MonoBehaviour {
	public UnitBase target;
	public float attack = 10.0f;
	private string spriteAtkNameFormat = "tower_a_{0:d2}";
	private string spriteNormalNameFormat = "tower_n_{0:d2}";
	public float range = 100;
	public float shotDelay = 3.0f;
	private float reloadTime = 0;
	private tk2dSprite spr;

	public int totalPrice = 0;
	public int buildPrice = 50;
	private int atkLv = 1;
	private int spdLv = 1;
	private int rngLv = 1;

	public UpgradeInfo[] attackUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 20), new UpgradeInfo(2, 10, 25), new UpgradeInfo(3, 20, 30), new UpgradeInfo(4, 30, 35), new UpgradeInfo(5, 40, 40)
	};
	public UpgradeInfo[] speedUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 2.5f), new UpgradeInfo(2, 10, 2.1f), new UpgradeInfo(3, 20, 1.7f), new UpgradeInfo(4, 30, 1.3f), new UpgradeInfo(5, 40, .9f)
	};
	public UpgradeInfo[] rangeUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 100), new UpgradeInfo(2, 10, 130), new UpgradeInfo(3, 20, 160), new UpgradeInfo(4, 30, 190), new UpgradeInfo(5, 40, 220)
	};
	private UpgradeInfo getInfo(int lv, UpgradeInfo[] infoArr)
	{
		foreach(UpgradeInfo ui in infoArr)
		{
			if(ui.level == lv)return ui;
		}
		return null;
	}
	public int getLvupAtkPrice() { return getLvupAtkPrice(atkLv + 1); }
	public int getLvupAtkPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, attackUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
	public int getLvupSpdPrice() { return getLvupSpdPrice(spdLv + 1); }
	public int getLvupSpdPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, speedUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
	public int getLvupRngPrice() { return getLvupRngPrice(rngLv + 1); }
	public int getLvupRngPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, rangeUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
	public void upgradeAtk(){upgradeAtk(atkLv+1);}
	public void upgradeAtk(int lv)
	{
		UpgradeInfo info = getInfo(lv, attackUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		atkLv = info.level;
		attack = info.value;
	}
	public void upgradeSpd(){upgradeSpd(spdLv+1);}
	public void upgradeSpd(int lv)
	{
		UpgradeInfo info = getInfo(lv, speedUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		spdLv = info.level;
		shotDelay = info.value;
	}
	public void upgradeRng(){upgradeRng(rngLv+1);}
	public void upgradeRng(int lv)
	{
		UpgradeInfo info = getInfo(lv, rangeUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		rngLv = info.level;
		range = info.value;
	}
	// Use this for initialization
	void Start () {
		spr = this.GetComponentInChildren<tk2dSprite>();
		init();
	}

	void init()
	{
		totalPrice = buildPrice;
		reloadTime = shotDelay;
		upgradeAtk(1);
		upgradeSpd(1);
		upgradeRng(1);
	}

	// Update is called once per frame
	void Update () {
		if(GameManager.instance.isGameOver)return;
		spr.SortingOrder = -(int)this.transform.localPosition.y;
		checkRangeTarget();
		lookAtTarget();
		autoShot();
		attackSprAnim();
		updateSprite();
	}

	void checkRangeTarget()
	{
		if(target != null)
		{
			if(range < Vector2.Distance((Vector2)target.transform.localPosition, (Vector2)this.transform.localPosition))target = null;
		}
		if(target == null)
		{
			foreach(UnitBase ub in GameManager.instance.unitList)
			{
				if(range > Vector2.Distance((Vector2)ub.transform.localPosition, (Vector2)this.transform.localPosition))
				{
					target = ub;
					return;
				}
			}
		}
		
	}

	void lookAtTarget()
	{
		if(target == null)return;
		float anglePI = Mathf.Atan2(this.transform.localPosition.y - target.transform.localPosition.y, target.transform.localPosition.x - this.transform.localPosition.x) + Mathf.PI/2.0f;
		int angle = (int)((anglePI/Mathf.PI * 18.0f) + 36)%36;
		spriteN = 18 -Mathf.Abs(angle - 18);
		int spriteDir = angle<18?1:-1;
		spr.scale = new Vector3(spriteDir, 1,1);
		
	}

	void autoShot()
	{
		reloadTime -= Time.deltaTime;
		if(target == null || reloadTime>0)return;
		attackTarget();
		
	}
	bool isAtkSpr = false;
	bool isAttacking = false;
	float attackTimeCount = 0f;
	float attackDuration = .1f;
	float attackDelay = .13f;
	int attackRepeat = 3;
	int attackedCount = 0;

	void attackTarget()
	{
		if(target == null || isAttacking)return;
		attackTimeCount = 0;
		attackedCount = 0;
		isAtkSpr = true;
		isAttacking = true;
		reloadTime = shotDelay;

		target.attackMe(attack);
	}

	void attackSprAnim()
	{
		if(!isAttacking)return;
		attackTimeCount += Time.deltaTime;
		if(isAtkSpr)
		{
			if(attackDuration<attackTimeCount)isAtkSpr = false;
		}
		else if(attackDelay<attackTimeCount)
		{
			attackedCount++;
			if(attackedCount<attackRepeat)
			{
				isAtkSpr = true;
				attackTimeCount -= attackDelay;
			}
			else isAttacking = false;
		}
	}

	private int spriteN = 0;
	void updateSprite()
	{
		string spriteName = string.Format(isAtkSpr?spriteAtkNameFormat:spriteNormalNameFormat, spriteN);
		spr.spriteId = spr.GetSpriteIdByName(spriteName);
	}

}
[System.Serializable]
public class UpgradeInfo
{
	public int level = 0;
	public int price = 0;
	public float value = 0;
	
	public UpgradeInfo(int _level, int _price, float _value)
	{
		level = _level;
		price = _price;
		value = _value;
	}
}






신고
Posted by andwhy
앞으로 2회 남았네요...

처음 계획한것과 약간 달라져서.. 좀 더 세분화 된것도 있고, 약식으로 넘어가버린것들도 있네요.
앞으로 2회분의 내용은.

-오늘 WaveData 작성관 5Wave까지 구현..(나머지는 직접 만들어보시면 됩니다.)

-게임 종료와 재시작.

이렇게 2회로 끝내겠습니다.

UI도 넣어서 좀더 게임처럼 보이고 싶었는데, 적당한 UI를 구하질 못했네요.
혹시라도 이글을 보시는 디자이너분중에.. UI작업을 도와주실분있으시면 언제라도 연락 바랍니다.

요즘 업무가 많아지고, 이것저것 신경쓸일이 많아져서, 우선은 다음회까지로 종료하겠습니다.
종료후에 지금까지 했던걸 좀더 다듬어서 재판??으로 만들고, 시간되는데로 좀더 보강하도록 하겠습니다.

//===============================================================================

오늘 작업할 내용은 WaveData 입니다.

WaveData는 한웨이브(스테이지) 에 나올 적들에 대한 데이터 입니다.

지금까지는 수동으로 버튼을 눌러서 적 유닛을 추가해줬지만,

WaveData를 통해서 나오는 적들의 속도, 체력, 나오는 간격...등등을 정해줍니다.

유닛리소스 종류도 정해주면 좋겠지만..지금껏 우리가 작업한 리소스는 유닛이 1종밖엔 없으니 할수 없겠네요.


1. WaveData.cs

웨이브 데이터는 한 웨이브당 출현할 유닛들에 대한 정보를 담고 있습니다.

웨이브에 출현할 유닛에 필요한 정보는

유닛의 HP
유닛 스피드
유닛을 제거할때 얻는 골드량
한번생성될때 몇마리의 유닛이 생성될지
유닛이 생성되고 다음생성때까지의 딜레이
유닛을 총 몇번 생성할것인지,
다음 웨이브까지의 딜레이


정도입니다.

WaveData.cs 파일을 만들고,

다음과 같이 코딩합니다.
(완료된 cs파일들은 마지막에 한번에 모두 공유하도록 하겠습니다.)

WaveData.cs

모든 public 변수들은 위에 설명한 정보들을 저장한것이고,
name 이란 변수와, generateCount 란 변수만 위에서 설명이 없는 변수인데 잠시후에 설명하겠습니다.

그림에 표시된 부분을 보면,메타 테그가 붙어있는데, 지난 타워 업데이트 시간에 UpdateData 클래스에도 동일한 메타테그를 붙였습니다.
기억나실지 모르겠지만, 저 태가가 붙어있으면 인스팩터상에서 해당 클래스의 변수들을 볼수 있게 시리얼라이즈화 시켜줍니다.

다음으로..name 이란 string변수는 크게 쓸모는 없는 변수 이지만, 
인스팩터에 현재 클래스가 표시될때 name에 정해진 이름으로 표시됩니다.

(게임상 크게 필요없지만 팁정도로 생각해주세요.)
우리는 이 name레벨 1~5까지 이름을 붙여 놓겠습니다.


2. Wave 동작 구현

먼저 우리가 이 WaveData를 어떻게 활용할껀지에 대해서 설명을 좀 하는편이 좋을것 같네요.

1. 우선 게임이 시작되면, GameManager 에서 waveDataIndex 를 '0'으로 셋팅합니다.

2. waveDataIndex번째 WaveData를 가져와서 현재 WaveDataList에 추가하고, 다음 wave까지의 딜레이를 셋팅해줍니다.

2-1. GameManagerUpdate때마다 WaveDataList에 추가되어있는 WaveData의 정보를 업데이트 해주고, WaveData는 각각 정보를 업데이트 하다가 유닛을 생성해야할 시간이 되면 GameManager에게 유닛을 추가하라고 요청합니다.

2-2. WaveData는 자기자신을 업데이트 며 총 생성할 유닛을 모두 생성하면 자신을 GameManager에게 자신을 제외하도록 요청합니다.

3. GameManager에서 다음 웨이브 시작시간이 되면, waveDataIndex를 증가 시키고, 2번작업으로 돌아갑니다.


위에 설명한것처럼 GameManager.cs 에 추가 작업을 하겠습니다.

GameManager.cs



그림처럼 GameManager.cs파일에 waveData에 대한 코딩을 추가해줍니다.
isGameStart 는 "게임시작" 버튼을 눌렀을때 true로 바뀌고, false일동안에는 시간체크를 하지않아서 다음 웨이브로 넘어가지 않습니다.

waveIndex는 현재 스테이지 레벨입니다.
waveDataArray는 스테이지 웨이브 데이터로, 각 스테이지의 난이도를 배열로 가지고 있습니다. 위에서 시리얼라이즈 화 시켰기때문에 인스팩터에서 게임을 하면서 난이도를 조절할수 있습니다.

currentWaveDataList는 현재 적용되고 있는 waveData들로, list로 만들어진 이유는, 난이도에 따라 동시에 여러 WaveData가 존재할수 있기때문입니다.
nextWaveDelay와, nextWaveDelayCount는 다음 웨이브데이터 추가를 위한 카운트 값입니다.


그리고, 다음과같이 startGame(), checkNextWave(), updateWaveDataList() 함수를 만들어줍니다.

GameManager.cs

startGame()은 현재는 isStarttrue로 변경하고, 인덱스만 초기화 합니다.

checkNextWave()는 시간을 체크해서 다음 웨이브 시작시간이 되면 웨이브 리스트에 다음웨이브데이터를 복사해서 넣어주고, 다음웨이브 시간 설정, 웨이브 인덱스를 하나 증가시킵니다.
웨이브 데이터를 복사해서 넣는 이유는 복사해서 넣지 않았을경우 게임을 재시작하면 게임도중 변형된 데이터(이미 사용된 데이터들) 이 들어가기때문입니다.

그리고 밑줄친 부분을 보면, 마지막 웨이브가 될경우 다음 웨이브 시간을 음수로 만들어버려서 더이상 갱신되지 않도록 처리해두었습니다.

updateWaveDataList()는 현재 등록되어있는 WaveData들을 갱신시켜줍니다.

OnGUI()에서 addUnit 버튼의 라벨과 기능을 startGame으로 변경합니다.

GameManager.cs


이제 업데이트때 checkNextWave()updateWaveDataList()를 호출해줍니다.

GameManager.cs

여기까지 하면 위에서 설명한 1번, 2번, 2-1번(일부), 3번 내용을 모두 구현한겁니다.

waveDataIndex 를 0으로 초기화 하고,(1번), 다음웨이브데이터를 넣어주고, 다음웨이브 시간을 설정해주고,(2번), updateWaveDataList() 에선 추가되어있는 모든 웨이브데이터를 업데이트 해주고,(2-1번) 다시 checkNextWave() 에선 다음웨이브 시간이 되면 새로운 웨이브 데이터를 추가해줍니다.(3번)

이젠 WaveData에 자신의 정보를 업데이트 시켜줄 update()함수와, 자신을 복제할 clone()함수를 만들겠습니다.

WaveData.cs


clone() 함수는 상당히 단순합니다. 자신과 같은 데이터 형을 새로 생성해주고, public 으로 선언된 변수에 자신의 변수와 동일하게 셋팅해주면 됩니다.

update()함수는 현재 WaveDataMonoBehaviour 를 상속받은게 아니기 떄문에, 자동으로 호출되지는 않고, 다른 클레스(GameManager.cs)에서 호출을 해줘야합니다.
update()의 마지막에 표시된 부분을 보면, 바로 위에서 repeatTime 을 하나씩 감소 시키며 더이상 반복할일이 없을때, GameManagerremoveWaveData를 호출해서 자기 자신을 제외시켜 버립니다.(3번)
그리고 윗쪽 표시를 보면, 생성시간이 되었을때, onceGenerateUnitCount만큼 유닛을 추가하려고 하는데,(지금은 주석으로만 달려있습니다.)

현재는 유닛을 추가할때, 별도로 hp나, 속도를 설정할만한 함수가 없어서 주석으로 표시 해두었습니다.
이제부터 추가하도록 하겠습니다.

먼저 UnitBase.cs를 열고, setUnit() 이란 함수를 만듭니다.

UnitBase.cs

이 함수는 유닛의 hp, speed, 획득 골드를 설정해줍니다.

GameManager.csaddUnit()함수도 다음과 같이 public 으로 변경하고, 파라미터를 받아올수 있게 수정합니다.

GameManager.cs

유닛을 추가한뒤엔 hp와, speed, 획득 Gold를 유닛에게 셋팅해주면 됩니다.

이제 다시 WaveData.cs 로 가서 update함수에 주석으로 되어있던 addUnit을 구현해보겠습니다.

WaveData.cs

사진처럼 GameManageraddUnit을 각각 파라미터를 넣고 호출해주면 끝납니다.

마지막으로 GameManager 의 인스펙터에 WaveData를 5개 정도 만들고 간단히 설정해서 테스트 해보겠습니다.
인스펙터의 WaveDataArray 항목을 열어서 size에 5 를 입력하고 엔터를 처줍니다.

생성된 5개의 WaveData에 다음처럼 값을 넣어줍니다.(값은 제가 임의로 넣은것이기에, 테스트 할때는 마음껏 변경해보시기 바랍니다.)

이제 플레이버튼을 눌러서 "start Game" 버튼을 눌러보면 마린들이 적당한 간격을 두고 나오는것을 볼수 있습니다.

하지만 잠시뒤엔 다음과 같은 에러문구 볼수 있습니다.

이 에러가 생기는 원인은 바로 GameManager의 removeWaveData() updateWaveDataList() 함수가 원입니다.
updateWaveDataList() 에서 foreach 로 
currentWaveDataList를 검색하는 중에 , removeWaveData가 동작되어 currentWaveDataList의 인덱스가 엉키면서 생기는문제입니다.
당장 보기엔 별문제 없어보이지만, Error는 Error 이니까 해결하고 가도록 하겠습니다.

이를 해결하기 위해서 바로 currentWaveDataList에서 WaveData를 삭제 하지 않고 다른방법을 사용하겠습니다.
currentWaveDataList에서 바로 삭제하지 않고 , 삭제할 WaveData를 따로 저장했다가 
currentWaveDataList 업데이트가 끝나면 지울 리스트를 한번에 삭제하도록 합니다.

먼저 지울 리스트를 생성하고, removeWaveData 함수에서 currentWaveDataList에서 삭제 대신에 지울 리스트에 추가합니다.

GameManager.cs

그리고 updateWaveDataList() 에서 모든 업데이트가 끝난뒤 아래처럼 제거 대상 데이터들을 삭제해줍니다.

GameManager.cs

이제 다시 플레이버튼을 누르고 테스트 해보면 더이상 에러는 보이지 않습니다.

3. 추가 정보 및 스킵 버튼

지금 상태로 게임을 해보면 다음 다음웨이브까지 얼마나 남았는지 알수가 없고, 미리 유닛을 모두 제거한 상태라면, 빨리 다음웨이브를 진행하고 싶어집니다.

그래서 이번엔 간단하게 다음웨이브까지 남은 시간을 표시해주고, skip버튼을 만들어보겠습니다.

먼저 다음 웨이브까지 시간을 표시하겠습니다.

GameManager.cs

처음 if문을 보면, isGameStart false인경우(아직 게임이 시작되지 않음)는 이전처럼 StartGame버튼을 노출합니다.
isGameStart true 가 될경우(이미 게임이 시작된경우)에는 nextWave라는 skip버튼을 노출합니다.
skip버튼에는 nextWaveDelaycount값을 동일하게 만들어주면,  checkNextWave() 에서 자연스럽게 다음 웨이브를 시작하게 됩니다.

아랫쪽 표시한부분을 보면, 아주 간단하게 다음 웨이브의 이름과, 남은시간을 표시해주게 되어있습니다.
크게 복잡한 내용은 없고 string.Format에서 "F2"라는 옵션은 "고정 소수점" 을 표시하는 옵션으로 뒤에 2 가 붙었으니 항상 소수점 2째 자리까지 표시해줍니다.

게임을 시작해보면 다음과 같이 정보가 보입니다.


4. 소스 파일

오늘 작업한 소스파일들을 올려드립니다.
잘안되거나 이해안가는 부분이 있으면 참고 하시기 바랍니다.


WaveData.cs


using System;
using UnityEngine;

[System.Serializable]
public class WaveData
{
	public string name = "level_";
	
	public float unitHP = 0;
	public float unitSpeed = 0;
	public int unitGold = 0;
	public int onceGenerateUnitCount = 1;
	public float generateDelay = 1.0f;
	private float generateCount = 0;
	
	public int repeatTime = 10;
	public float nextWaveDelay = 30.0f;
	public WaveData clone()
	{
		WaveData retWd = new WaveData();
		retWd.unitHP = unitHP;
		retWd.unitSpeed = unitSpeed;
		retWd.unitGold = unitGold;
		retWd.onceGenerateUnitCount = onceGenerateUnitCount;
		retWd.generateDelay = generateDelay;
		retWd.generateCount = generateDelay;
		retWd.repeatTime = repeatTime;
		retWd.nextWaveDelay = nextWaveDelay;
		return retWd;
	}
	public void update()
	{
		generateCount += Time.deltaTime;
		if(generateDelay <= generateCount)
		{
			generateCount -= generateDelay;
			for(int i = 0; i < onceGenerateUnitCount; i++)
			{
				GameManager.instance.addUnit(unitHP, unitSpeed, unitGold);
			}
			repeatTime--;
			if(repeatTime<=0)GameManager.instance.removeWaveData(this);
		}
	}
}


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;
		init();
	}
	//=========================================
	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>();
	[HideInInspector]
	public Dictionary<Point, TowerBase> towerDic = new Dictionary<Point, TowerBase>();
	public BgCellDisplayer bgGrid;
	public Transform unit_field;
	public UnitBase unit_marine;
	public TowerBase tower;
	public TowerUIMenu towerMenu;
	
	public int startedGold = 100;
	public int currentGold = 0;

	public Camera mainCam;

	private bool isGameStart = false;
	public int waveIndex = 0;
	//-----------waveData
	public WaveData[] waveDataArray;
	private List<WaveData> currentWaveDataList = new List<WaveData>();
	private List<WaveData> removeWaveDataList = new List<WaveData>();
	private float nextWaveDelay = 0f;
	private float nextWaveDelayCount = 0;
	
	public void addWaveData(WaveData wd)
	{
		currentWaveDataList.Add(wd);
	}
	public void removeWaveData(WaveData wd)
	{
		removeWaveDataList.Add (wd);
	}


	private void init()
	{
		unitList.Clear();
		towerDic.Clear();
		initPathFinder();

		currentGold = startedGold;
	}

	public void addUnit(float hp, float speed, int gold)
	{
		UnitBase unit = Instantiate(unit_marine) as UnitBase;
		unitList.Add(unit);
		unit.setUnit(hp, speed, gold);
		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);
		
	}

	void addTower(Point p)
	{
		
		TowerBase tw = Instantiate(tower) as TowerBase;
		tw.transform.parent = unit_field;
		tw.transform.localPosition = new Vector3(p.x * cellSize + cellSize/2.0f, -p.y * cellSize - cellSize);
		towerDic.Add(p, tw);
		
		useGold(tw.buildPrice);
	}
	public void addGold(int g)
	{
		currentGold += g;
	}
	public void useGold(int g)
	{
		currentGold -= g;
	}
	public bool checkGold(int g)
	{
		return (currentGold>=g);
	}
	public void removeUnit(UnitBase ub)
	{
		unitList.Remove(ub);
	}
	void researchPathUnits()
	{
		foreach(UnitBase ub in unitList)
		{
			if(ub!=null)
			{
				ub.getPath();
			}
		}
	}
	Point getStartPoint()
	{
		List<Point> startPointList = getStartPointList();
		if(startPointList.Count == 0)
		{
			Debug.LogError("Not Found Start Position");
			return null;
		}
		int ranIdx = Random.Range(0,startPointList.Count);
		return startPointList[ranIdx];
	}
	List<Point> getStartPointList()
	{
		//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));
				}
			}
		}
		return startPointList;
	}
	public void initPathFinder()
	{
		PathFinder.instance.setMapData(wallMap);
	}
	void OnGUI()
	{
		if(!isGameStart)
		{
			if(GUI.Button( new Rect( 10, 10, 100, 40), "Start Game"))
			{
				startGame();
			}
		}
		else 
		{
			if(GUI.Button( new Rect( 10, 10, 100, 40), "nextWave!!"))
			{
				nextWaveDelay = nextWaveDelayCount;
			}
		}
		GUI.Label(new Rect( (Screen.width - 200)/2.0f, 10, 200, 50), "GOLD : "+currentGold);
		if(waveIndex < waveDataArray.Length)
			GUI.Label(new Rect( (Screen.width - 200), 10, 200, 50), 
			          string.Format("NextWave [{0}] : {1:F2}",waveDataArray[waveIndex].name,(nextWaveDelay-nextWaveDelayCount)));
	}
	void Update()
	{
		checkNextWave();
		//updateWaveDatas
		updateWaveDataList();

		if(Input.GetMouseButtonDown(0) && !towerMenu.isShow) 
		{
			Vector2 pos = Input.mousePosition;
			Vector3 mouseP = mainCam.ScreenToWorldPoint(pos) - unit_field.TransformPoint(Vector3.zero);
			Point myPos = new Point((int)(mouseP.x/cellSize), -(int)(mouseP.y/cellSize));
			if(towerDic.ContainsKey(myPos))
			{
				showMenu(towerDic[myPos]);
			}
			else 
			{
				buildTower(myPos);
			}
		}
	}
	void startGame()
	{
		waveIndex = 0;
		isGameStart = true;
	}

	void checkNextWave()
	{
		if(!isGameStart)return;
		if(nextWaveDelay<0)return;
		nextWaveDelayCount += Time.deltaTime;
		if(nextWaveDelay<nextWaveDelayCount)
		{
			nextWaveDelayCount = 0;
			if(waveIndex >= waveDataArray.Length)
			{
				nextWaveDelay = -1;
			}
			else
			{
				addWaveData(waveDataArray[waveIndex].clone());
				nextWaveDelay = waveDataArray[waveIndex].nextWaveDelay;
				waveIndex++;
			}
		}
	}

	void updateWaveDataList()
	{
		foreach( WaveData wd in currentWaveDataList)
		{
			wd.update();
		}
		foreach( WaveData wd in removeWaveDataList)
		{
			if(currentWaveDataList.Contains(wd))currentWaveDataList.Remove(wd);
		}
		removeWaveDataList.Clear();
	}

	bool checkReachAble()
	{
		PathFinder.instance.setCheckMode(true);
		foreach(Point sp in getStartPointList())
		{
			if(PathFinder.instance.getPath(sp,100+wallMap[sp.x,sp.y]) == null)
			{
				Debug.Log("StartPoint Path NULL");
				return false;
			}
		}
		foreach(UnitBase unit in unitList)
		{
			if(!unit.getPath())
			{
				Debug.Log("Unit Path NULL");
				return false;
			}
		}
		PathFinder.instance.setPath();
		return true;
	}
	void buildTower(Point p)
	{
		if(p.x<0||p.y<0 || p.x >= wallMap.GetLength(0) || p.y >= wallMap.GetLength(1))return;
		int prevIndex = wallMap[p.x, p.y];
		
		if(wallMap[p.x, p.y] == 0)wallMap[p.x, p.y] = 2;
		else return;
			
		if(checkGold(tower.buildPrice) && checkReachAble())
		{
			bgGrid.refreshDisplay();
			researchPathUnits();
			addTower(p);
		}
		else 
		{
			wallMap[p.x, p.y] = prevIndex;
		}
		PathFinder.instance.setCheckMode(false);
	}
	public void sellTower(TowerBase tw)
	{
		if(!towerDic.ContainsValue(tw))return;
		addGold(tw.totalPrice/2);
		removeTower(tw);
	}
	public void removeTower(TowerBase tw)
	{
		if(!towerDic.ContainsValue(tw))return;
		foreach(Point keyP in towerDic.Keys)
		{
			if(towerDic[keyP] == tw)
			{
				if(keyP.x<0||keyP.y<0 || keyP.x >= wallMap.GetLength(0) || keyP.y >= wallMap.GetLength(1))return;
				wallMap[keyP.x, keyP.y] = 0;
				towerDic.Remove(keyP);
				break;
			}
		}
		Destroy(tw.gameObject);
		bgGrid.refreshDisplay();
		checkReachAble();
		PathFinder.instance.setPath();
		PathFinder.instance.setCheckMode(false);
	}
	void showMenu(TowerBase tw)
	{
		towerMenu.showMenu(tw);
	}
}



UnitBase.cs


using UnityEngine;
using System.Collections;
using common;

public class UnitBase : MonoBehaviour {
	public float maxHp = 100;
	public float curHp = 100;
	public int gainGold = 5;

	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 () {
		curHp = maxHp;
	}
	private bool isMoveAble = false;
	// Update is called once per frame
	void Update () {
		if(!isMoveAble)return;
		float cellSize = GameManager.instance.cellSize;
		float _speed = cellSize * Time.deltaTime * moveSpeed;
		float rx = (nextPoint.x * cellSize + cellSize/2.0f) - this.transform.localPosition.x;
		float ry = (-nextPoint.y * cellSize - cellSize/2.0f) - this.transform.localPosition.y;
		float dx = _speed * makeNomal(rx);
		float dy = _speed * makeNomal(ry);
		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);
		spr.Sprite.SortingOrder = -(int)this.transform.localPosition.y;
		if(isCloseX && isCloseY)
		{
			if(pathArr.Length <= pathIndex + 1)
			{
				isMoveAble = false;
				GameManager.instance.removeUnit(this);
				Destroy(this.gameObject);
				return;
			}
			setNextPoint();
		}
	}

	public void setUnit(float hp, float speed, int gold) {
		curHp = maxHp = hp;
		moveSpeed = speed;
		gainGold = gold;
	}

	int makeNomal(float f)
	{
		float k = 0.1f;
		if(f>k)return 1;
		else if(f<-k)return -1;
		else return 0;
	}
	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 bool getPath()
	{
		float cellSize = GameManager.instance.cellSize;
		startPoint = new Point((int)(this.transform.localPosition.x/cellSize),-(int)(this.transform.localPosition.y/cellSize) );
		int wallMapIndex = GameManager.instance.wallMap[startPoint.x,startPoint.y];
		if(wallMapIndex > 0 && wallMapIndex < 10)return true;

		Point[] pArr = PathFinder.instance.getPath(startPoint, 111);
		if(pArr == null){Debug.Log("NULL path");return false;}
		pathArr = pArr;

		if(nextPoint != null && pathArr.Length > 1 && nextPoint.isEqual(pathArr[1]))pathIndex = 1;
		else pathIndex = 0;
		nextPoint = pathArr[pathIndex];
		showCharDir();
		return true;
	}
	public void attackMe(float dmg)
	{
		curHp -= dmg;
		if(curHp < 0)
		{
			isMoveAble = false;
			spr.Play ("destroy");
			GameManager.instance.addGold(gainGold);
			GameManager.instance.removeUnit(this);
			spr.AnimationCompleted = unitDestoryAniComplete;
		}
	}
	private void unitDestoryAniComplete(tk2dSpriteAnimator sprite, tk2dSpriteAnimationClip clip)
	{
		Destroy(this.gameObject);
	}
	private void setNextPoint()
	{
		startPoint = nextPoint;
		pathIndex++;
		nextPoint = pathArr[pathIndex];
		showCharDir();
	}
	private void showCharDir()
	{
		
		float cellSize = GameManager.instance.cellSize;
		float nx = (nextPoint.x * cellSize + cellSize/2.0f);
		float ny = (-nextPoint.y * cellSize - cellSize/2.0f);
		if(this.transform.localPosition.x<nx)
			spr.Play (charAniStr[(int)CHAR_ANI.RIGHT]);
		else if(this.transform.localPosition.x>nx)
			spr.Play (charAniStr[(int)CHAR_ANI.LEFT]);
		else if(this.transform.localPosition.y<ny)
			spr.Play (charAniStr[(int)CHAR_ANI.UP]);
		else if(this.transform.localPosition.y>ny)
			spr.Play (charAniStr[(int)CHAR_ANI.DOWN]);
		spr.ClipFps *= moveSpeed;
	}
}






신고
Posted by andwhy

이번엔 저번 강의에 이어서, 타워 업그레이드를 구현하도록 하겠습니다.

업그레이드 기능은 끝났지만, 실제 게임상의 재화를 소비하거나, 얻는기능은 없었습니다.

우선 업그레이드 시스템을 완료하기 위해서 게임상의 재화를 구현하도록 합니다.

이 게임에선 "골드" 를 재화라고 칭하고, 계속 골드로 설명하겠습니다.


1. 골드 시스템.

게임 내에서 골드는 타워를 짓거나, 업그레이드를 할때 필요한 요소 입니다.골드가 부족하면 타워를 짓지 못하고, 업그레이드가 불가능해서 적 유닛을 막아낼수가 없습니다.

이 골드는 게임초기에 특정 금액이 주어지고, 유닛을 제거하거나, 타워를 판매할때 획득할수 있습니다.

먼저 게임이 시작되면 시작골드를 지급하고, 현재 보유 골드를 화면에 표시하도록 하겠습니다.

GameManager.cs 파일을 열고, 다음과 같이 startGold 와, currentGold 변수를 넣어줍니다.(float로 얻거나 소비될일은 없을것 같아서, int로 생성합니다.)

바로 아래의 init()함수 안에서는 currentGold에 startedGold 를 넣어줍니다. 게임이 시작하면 정해진 startedGold만큼 지급됩니다.

화면에 현재 골드가 얼마인지 GUI를 이용해서 표시해보겠습니다.

화면을 확인해보면 아래처럼 화면 중앙상단에 현재 골드량이 표시됩니다.



GameManager에서 골드를 컨트롤 할수있는 addGold, useGild, checkGold 함수를 만들어줍니다.


각각 골드를 추가, 사용, 골드 잔고가 얼마 이상인지 체크해주는 역할을 합니다.

이제 addTower 함수를 찾아서 마지막에 다음과 같이 useGold함수를 넣어주면, 타워를 지을때마다 현재 골드가 줄어들게 됩니다.

다시 플레이해서 타워를 지어보면, 타워가 생길때마다 50 골드씩 줄어드는걸 볼수 있습니다. 하지만 돈이 부족하더라도 골드가 마이너스가 될뿐 계속 타워는 지을수 있습니다.

이번엔 타워를 짓기전에 골드량을 체크해서 골드가 충분할때만 지어지도록 하겠습니다.

표시된것처럼 현재 골드가 towerbuildPrice보다 같거나 많을때만 타워를 추가하도록 변경합니다. 이젠 골드가 부족할경우 타워는 지어지지 않습니다.

2. 타워 되팔기

이젠 타워를 되팔때 보유 골드를 추가해주는 작업을 하겠습니다.

타워의 판매 가격은 타워를 처음 짓기위해 필요한 금액과, 업그레이드시 들어갔던 모든 비용의 총합의 50% 가격으로 판매하려합니다.

타워에 관련된 기능들이니 TowerBase.cs 에 작업을 추가해주겠습니다.

먼저 타워에 사용된 모든 가격을 누적시켜주는 totalPrice를 선언해주고, init()함수에서 totalPricebuildPrice로 맞춰줍니다.
(타워를 짓자마자 init()을 하게되니 여기서 totalPrice를 초기화해주면 됩니다.)

TowerBase.cs


업그레이드 시에 totalPrice에 누적이될수있도록, 각각의 upgrade 함수도 다음과 같이 코드를 넣어줍니다.


업그레이드에 필요한 priceGameManager.checkGold를 통해 골드가 충분한지 검사하고, 충분하다면, useGold를 이용해 골드를 차감해주고, totalPrice에 사용한만큼 누적시켜줍니다.
(공경력업그레이드 함수 뿐 아니라, 공속, 공격범위 업그레이드도 잊지말고 적용해주세요.)

실제로 Tower를 추가하거나 제거하는곳은 GameManager이니, GameManager.cssellTower()라는 함수를 만들고 여기서 골드를 추가해주겠습니다.

GameManager.cs

판매하면 타워를 삭제해야하기에 가장 아랫쪽엔 removeTower()함수도 호출해줍니다.

그리고 타워판매 버튼을 눌렀을때, 기존엔 removeTower()를 호출하게 되어있었지만, 이젠 sellTower로 연결을 해줍니다.

TowerUIMenu.cs

윗쪽 주석부분이 기존코드이고, 아래 표시된부분이 변경한코드입니다. 단순히 메소드 명만 변경되었습니다.

이대로 실행해서 테스트 해보면 타워를 판매할때 처음 비용의 50%가 다시 골드로 판매되어 현재 골드가 올라가는게 보입니다.

3.menuUI 정보 업데이트.

이번엔 타워선택시 표시되는 UI들의 정보를 업데이트 해주는 작업을 하겠습니다.

업데이트시 필요 골드와, 골드가 부족하면 버튼이 비활성화되게 하고, 업데이트에 쓰인골드도 타워 판매가격에 반영되도록 만들어줍니다.

우선 TowerMenuBtn.cs 을 좀더 보강해주겠습니다.


표시한것처럼 enable 값이 set될때 uiItem의 enabled 값도 동일하게 셋팅해줍니다.
그리고 금액을 표시할때 setLabel을 직접 써도 되지만, 약간 귀찮을것 같아서 가격만 적으면 가격을 적당히 포멧에 맞춰서 표시해주도록 setPrice함수를 추가합니다.

다음으로, TowerUIMenu.cs 파일을 수정해서, 업데이트 버튼을 누르거나, 다른 타워를 선택했을때 각각 상황에 맞는 정보를 표시해주도록 updateInfo() 함수를 추가합니다.

간단히 보면, sellBtn은 비활성화 될일이 없으니 항상 enable = true 로 활성화시켜줍니다.
가격표시는  선택된 타워의 totalPrice의 50% 이니 반으로 나눠주면 됩니다.

atkUpgradeBtn은 업그레이드 비용이 -1(업그레이드 단계가 max일경우 -1로 나옵니다.)이거나, 돈이 부족하면 비활성화로 표시되고,
업그레이드 레벨이 최고라면 "MAX"로 표시 아니라면, 각각 레벨업에 필요한 비용을 표시해주도록 되어있습니다.

다른 버튼들도 같은방법으로 되어있으니 금방 알아보실수 있습니다.

각각 upgrade 함수들 마지막에 updateInfo() 를 한번씩 호출하고, showMenu시에도 updateInfo()를 호출해주시면 됩니다.

이제 게임을 실행해보면,


버튼을 누를때마다 정보도 변하고, 판매금액도 바로바로 적용되고, 타워판매시 GOLD도 정확히 올라가고 있습니다.


이젠 마지막으로 유닛을 파괴했을때 골드를 획득할수 있도록 UnitBase.cs 파일을 약간 수정하겠습니다.

먼저 유닛을 파괴할때 지급할 gold값을 만들어주고, 

                          

유닛이 죽을때 GameManageraddGold를 통해 골드를 증가시켜주면됩니다.

 


이제 게임을 실행해서 유닛을 죽여보면 죽일때마다 설정한 골드가 지급되는걸 볼수 있습니다.
(초기 자본금이 적어서..죽이기 힘들어서 자본금과 공격력, 유닛의 체력등을 인스펙터에서 조절하면 확인할수있습니다.)

오늘은 여기까지 하고, 소스파일들을 공유 하겠습니다. 혹시 잘안되는게 있으면 한번씩 비교해보세요.

다음시간엔, 유닛들을 웨이브에따라 자동으로 등장하는걸 만들어보겠습니다.


4.소스코드

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;
		init();
	}
	//=========================================
	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>();
	[HideInInspector]
	public Dictionary<Point, TowerBase> towerDic = new Dictionary<Point, TowerBase>();
	public BgCellDisplayer bgGrid;
	public Transform unit_field;
	public UnitBase unit_marine;
	public TowerBase tower;
	public TowerUIMenu towerMenu;
	
	public int startedGold = 100;
	public int currentGold = 0;

	public Camera mainCam;

	private void init()
	{
		unitList.Clear();
		towerDic.Clear();
		initPathFinder();

		currentGold = startedGold;
	}
	void addUnit()
	{
		UnitBase unit = Instantiate(unit_marine) as UnitBase;
		unitList.Add(unit);
		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);

	}
	void addTower(Point p)
	{
		
		TowerBase tw = Instantiate(tower) as TowerBase;
		tw.transform.parent = unit_field;
		tw.transform.localPosition = new Vector3(p.x * cellSize + cellSize/2.0f, -p.y * cellSize - cellSize);
		towerDic.Add(p, tw);
		
		useGold(tw.buildPrice);
	}
	public void addGold(int g)
	{
		currentGold += g;
	}
	public void useGold(int g)
	{
		currentGold -= g;
	}
	public bool checkGold(int g)
	{
		return (currentGold>=g);
	}
	public void removeUnit(UnitBase ub)
	{
		unitList.Remove(ub);
	}
	void researchPathUnits()
	{
		foreach(UnitBase ub in unitList)
		{
			if(ub!=null)
			{
				ub.getPath();
			}
		}
	}
	Point getStartPoint()
	{
		List<Point> startPointList = getStartPointList();
		if(startPointList.Count == 0)
		{
			Debug.LogError("Not Found Start Position");
			return null;
		}
		int ranIdx = Random.Range(0,startPointList.Count);
		return startPointList[ranIdx];
	}
	List<Point> getStartPointList()
	{
		//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));
				}
			}
		}
		return startPointList;
	}
	public void initPathFinder()
	{
		PathFinder.instance.setMapData(wallMap);
	}
	void OnGUI()
	{
		if(GUI.Button( new Rect( 10, 10, 100, 40), "Add Unit"))
		{
			addUnit();
		}
		GUI.Label(new Rect( (Screen.width - 200)/2.0f, 10, 200, 50), "GOLD : "+currentGold);
	}
	void Update()
	{
		if(Input.GetMouseButtonDown(0) && !towerMenu.isShow) 
		{
			Vector2 pos = Input.mousePosition;
			Vector3 mouseP = mainCam.ScreenToWorldPoint(pos) - unit_field.TransformPoint(Vector3.zero);
			Point myPos = new Point((int)(mouseP.x/cellSize), -(int)(mouseP.y/cellSize));
			if(towerDic.ContainsKey(myPos))
			{
				showMenu(towerDic[myPos]);
			}
			else 
			{
				buildTower(myPos);
			}
		}
	}
	bool checkReachAble()
	{
		PathFinder.instance.setCheckMode(true);
		foreach(Point sp in getStartPointList())
		{
			if(PathFinder.instance.getPath(sp,100+wallMap[sp.x,sp.y]) == null)
			{
				Debug.Log("StartPoint Path NULL");
				return false;
			}
		}
		foreach(UnitBase unit in unitList)
		{
			if(!unit.getPath())
			{
				Debug.Log("Unit Path NULL");
				return false;
			}
		}
		PathFinder.instance.setPath();
		return true;
	}
	void buildTower(Point p)
	{
		if(p.x<0||p.y<0 || p.x >= wallMap.GetLength(0) || p.y >= wallMap.GetLength(1))return;
		int prevIndex = wallMap[p.x, p.y];
		
		if(wallMap[p.x, p.y] == 0)wallMap[p.x, p.y] = 2;
		else return;
			
		if(checkGold(tower.buildPrice) && checkReachAble())
		{
			bgGrid.refreshDisplay();
			researchPathUnits();
			addTower(p);
		}
		else 
		{
			wallMap[p.x, p.y] = prevIndex;
		}
		PathFinder.instance.setCheckMode(false);
	}
	public void sellTower(TowerBase tw)
	{
		if(!towerDic.ContainsValue(tw))return;
		addGold(tw.totalPrice/2);
		removeTower(tw);
	}
	public void removeTower(TowerBase tw)
	{
		if(!towerDic.ContainsValue(tw))return;
		foreach(Point keyP in towerDic.Keys)
		{
			if(towerDic[keyP] == tw)
			{
				if(keyP.x<0||keyP.y<0 || keyP.x >= wallMap.GetLength(0) || keyP.y >= wallMap.GetLength(1))return;
				wallMap[keyP.x, keyP.y] = 0;
				towerDic.Remove(keyP);
				break;
			}
		}
		Destroy(tw.gameObject);
		bgGrid.refreshDisplay();
		checkReachAble();
		PathFinder.instance.setPath();
		PathFinder.instance.setCheckMode(false);
	}
	void showMenu(TowerBase tw)
	{
		towerMenu.showMenu(tw);
	}
}


TowerUIMenu.cs


using UnityEngine;
using System.Collections;

public class TowerUIMenu : MonoBehaviour {
	
	public TowerMenuBtn sellBtn; 
	public TowerMenuBtn atkUpgradeBtn; 
	public TowerMenuBtn speedUpgradeBtn; 
	public TowerMenuBtn rangeUpgradeBtn;
	
	public tk2dUIItem blinkArea;
	
	public tk2dSprite rangeSpr;
	
	private TowerBase targetTower;
	public bool isShow = false;
	// Use this for initialization
	void Start () {
		addBtnEvents();
		hideMenu();
	}
	void addBtnEvents()
	{
		sellBtn.uiItem.OnClick -= towerSell;
		sellBtn.uiItem.OnClick += towerSell;
		atkUpgradeBtn.uiItem.OnClick -= upgradeAtk;
		atkUpgradeBtn.uiItem.OnClick += upgradeAtk;
		speedUpgradeBtn.uiItem.OnClick -= upgradeSpeed;
		speedUpgradeBtn.uiItem.OnClick += upgradeSpeed;
		rangeUpgradeBtn.uiItem.OnClick -= upgradeRange;
		rangeUpgradeBtn.uiItem.OnClick += upgradeRange;
		
		blinkArea.OnClick -= hideMenu;
		blinkArea.OnClick += hideMenu;
	}
	public void showMenu(TowerBase target)
	{
		targetTower = target;
		//set position;
		this.transform.localPosition = targetTower.transform.localPosition;
		updateInfo();
		
		isShow = true;
	}
	public void hideMenu()
	{
		this.transform.localPosition = new Vector3(-90000,0,0);
		targetTower = null;
		isShow = false;
	}
	private void updateInfo()
	{
		setRange(targetTower.range);
		//sellBtnInfo
		sellBtn.enable = true;
		sellBtn.setPrice(targetTower.totalPrice/2);
		//atkBtnInfo
		atkUpgradeBtn.enable = targetTower.getLvupAtkPrice()!=-1&&GameManager.instance.checkGold(targetTower.getLvupAtkPrice());
		if(targetTower.getLvupAtkPrice()<0)atkUpgradeBtn.setLabel("MAX");
		else atkUpgradeBtn.setPrice(targetTower.getLvupAtkPrice());
		//speedBtnInfo
		speedUpgradeBtn.enable = targetTower.getLvupSpdPrice()!=-1&&GameManager.instance.checkGold(targetTower.getLvupSpdPrice());
		if(targetTower.getLvupSpdPrice()<0)speedUpgradeBtn.setLabel("MAX");
		else speedUpgradeBtn.setPrice(targetTower.getLvupSpdPrice());
		//rangeBtnInfo
		rangeUpgradeBtn.enable = targetTower.getLvupRngPrice()!=-1&&GameManager.instance.checkGold(targetTower.getLvupRngPrice());
		if(targetTower.getLvupRngPrice()<0)rangeUpgradeBtn.setLabel("MAX");
		else rangeUpgradeBtn.setPrice(targetTower.getLvupRngPrice());
	}
	private void towerSell()
	{
		Debug.Log("TowerSell");
		GameManager.instance.sellTower(targetTower);
		hideMenu();
	}
	private void upgradeAtk()
	{
		Debug.Log("upgradeAtk");
		targetTower.upgradeAtk();
		updateInfo();
	}
	private void upgradeSpeed()
	{
		Debug.Log("upgradeSpeed");
		targetTower.upgradeSpd();
		updateInfo();
	}
	private void upgradeRange()
	{
		Debug.Log("upgradeRange");
		targetTower.upgradeRng();
		updateInfo();
	}
	private void setRange(float r)
	{
		rangeSpr.scale = Vector3.one * (r/40.0f);
	}
}


TowerMenuBtn.cs


using UnityEngine;
using System.Collections;

public class TowerMenuBtn : MonoBehaviour {
	public tk2dUIItem uiItem;
	public tk2dSprite btnSpr;
	public tk2dSprite disableSpr;
	public tk2dTextMesh label;
	
	private bool _enable = true;
	public bool enable
	{
		get{return _enable;}
		set{
			_enable = value;
			uiItem.enabled = _enable;
			setSprite();
		}
	}
	public void setPrice(int p)
	{
		setLabel(string.Format("$ {0:N0}", p));
	}
	public void setLabel(string msg)
	{
		label.text = msg;
	}
	void Start()
	{
		setSprite();
	}
	private void setSprite()
	{
		btnSpr.gameObject.SetActive(enable);
		disableSpr.gameObject.SetActive(!enable);
	}
}


TowerBase.cs


using UnityEngine;
using System.Collections;

public class TowerBase : MonoBehaviour {
	public UnitBase target;
	public float attack = 10.0f;
	private string spriteAtkNameFormat = "tower_a_{0:d2}";
	private string spriteNormalNameFormat = "tower_n_{0:d2}";
	public float range = 100;
	public float shotDelay = 3.0f;
	private float reloadTime = 0;
	private tk2dSprite spr;

	public int totalPrice = 0;
	public int buildPrice = 50;
	private int atkLv = 1;
	private int spdLv = 1;
	private int rngLv = 1;

	public UpgradeInfo[] attackUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 20), new UpgradeInfo(2, 10, 25), new UpgradeInfo(3, 20, 30), new UpgradeInfo(4, 30, 35), new UpgradeInfo(5, 40, 40)
	};
	public UpgradeInfo[] speedUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 2.5f), new UpgradeInfo(2, 10, 2.1f), new UpgradeInfo(3, 20, 1.7f), new UpgradeInfo(4, 30, 1.3f), new UpgradeInfo(5, 40, .9f)
	};
	public UpgradeInfo[] rangeUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 100), new UpgradeInfo(2, 10, 130), new UpgradeInfo(3, 20, 160), new UpgradeInfo(4, 30, 190), new UpgradeInfo(5, 40, 220)
	};
	private UpgradeInfo getInfo(int lv, UpgradeInfo[] infoArr)
	{
		foreach(UpgradeInfo ui in infoArr)
		{
			if(ui.level == lv)return ui;
		}
		return null;
	}
	public int getLvupAtkPrice() { return getLvupAtkPrice(atkLv + 1); }
	public int getLvupAtkPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, attackUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
	public int getLvupSpdPrice() { return getLvupSpdPrice(spdLv + 1); }
	public int getLvupSpdPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, speedUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
	public int getLvupRngPrice() { return getLvupRngPrice(rngLv + 1); }
	public int getLvupRngPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, rangeUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
	public void upgradeAtk(){upgradeAtk(atkLv+1);}
	public void upgradeAtk(int lv)
	{
		UpgradeInfo info = getInfo(lv, attackUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		atkLv = info.level;
		attack = info.value;
	}
	public void upgradeSpd(){upgradeSpd(spdLv+1);}
	public void upgradeSpd(int lv)
	{
		UpgradeInfo info = getInfo(lv, speedUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		spdLv = info.level;
		shotDelay = info.value;
	}
	public void upgradeRng(){upgradeRng(rngLv+1);}
	public void upgradeRng(int lv)
	{
		UpgradeInfo info = getInfo(lv, rangeUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		rngLv = info.level;
		range = info.value;
	}
	// Use this for initialization
	void Start () {
		spr = this.GetComponentInChildren<tk2dSprite>();
		init();
	}

	void init()
	{
		totalPrice = buildPrice;
		reloadTime = shotDelay;
		upgradeAtk(1);
		upgradeSpd(1);
		upgradeRng(1);
	}

	// Update is called once per frame
	void Update () {
		spr.SortingOrder = -(int)this.transform.localPosition.y;
		checkRangeTarget();
		lookAtTarget();
		autoShot();
		attackSprAnim();
		updateSprite();
	}

	void checkRangeTarget()
	{
		if(target != null)
		{
			if(range < Vector2.Distance((Vector2)target.transform.localPosition, (Vector2)this.transform.localPosition))target = null;
		}
		if(target == null)
		{
			foreach(UnitBase ub in GameManager.instance.unitList)
			{
				if(range > Vector2.Distance((Vector2)ub.transform.localPosition, (Vector2)this.transform.localPosition))
				{
					target = ub;
					return;
				}
			}
		}
		
	}

	void lookAtTarget()
	{
		if(target == null)return;
		float anglePI = Mathf.Atan2(this.transform.localPosition.y - target.transform.localPosition.y, target.transform.localPosition.x - this.transform.localPosition.x) + Mathf.PI/2.0f;
		int angle = (int)((anglePI/Mathf.PI * 18.0f) + 36)%36;
		spriteN = 18 -Mathf.Abs(angle - 18);
		int spriteDir = angle<18?1:-1;
		spr.scale = new Vector3(spriteDir, 1,1);
		
	}

	void autoShot()
	{
		reloadTime -= Time.deltaTime;
		if(target == null || reloadTime>0)return;
		attackTarget();
		
	}
	bool isAtkSpr = false;
	bool isAttacking = false;
	float attackTimeCount = 0f;
	float attackDuration = .1f;
	float attackDelay = .13f;
	int attackRepeat = 3;
	int attackedCount = 0;

	void attackTarget()
	{
		if(target == null || isAttacking)return;
		attackTimeCount = 0;
		attackedCount = 0;
		isAtkSpr = true;
		isAttacking = true;
		reloadTime = shotDelay;

		target.attackMe(attack);
	}

	void attackSprAnim()
	{
		if(!isAttacking)return;
		attackTimeCount += Time.deltaTime;
		if(isAtkSpr)
		{
			if(attackDuration<attackTimeCount)isAtkSpr = false;
		}
		else if(attackDelay<attackTimeCount)
		{
			attackedCount++;
			if(attackedCount<attackRepeat)
			{
				isAtkSpr = true;
				attackTimeCount -= attackDelay;
			}
			else isAttacking = false;
		}
	}

	private int spriteN = 0;
	void updateSprite()
	{
		string spriteName = string.Format(isAtkSpr?spriteAtkNameFormat:spriteNormalNameFormat, spriteN);
		spr.spriteId = spr.GetSpriteIdByName(spriteName);
	}

}
[System.Serializable]
public class UpgradeInfo
{
	public int level = 0;
	public int price = 0;
	public float value = 0;
	
	public UpgradeInfo(int _level, int _price, float _value)
	{
		level = _level;
		price = _price;
		value = _value;
	}
}


UnitBase.cs

using UnityEngine;
using System.Collections;
using common;

public class UnitBase : MonoBehaviour {
	public float maxHp = 100;
	public float curHp = 100;
	public int gainGold = 5;

	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 () {
		curHp = maxHp;
	}
	private bool isMoveAble = false;
	// Update is called once per frame
	void Update () {
		if(!isMoveAble)return;
		float cellSize = GameManager.instance.cellSize;
		float _speed = cellSize * Time.deltaTime * moveSpeed;
		float rx = (nextPoint.x * cellSize + cellSize/2.0f) - this.transform.localPosition.x;
		float ry = (-nextPoint.y * cellSize - cellSize/2.0f) - this.transform.localPosition.y;
		float dx = _speed * makeNomal(rx);
		float dy = _speed * makeNomal(ry);
		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);
		spr.Sprite.SortingOrder = -(int)this.transform.localPosition.y;
		if(isCloseX && isCloseY)
		{
			if(pathArr.Length <= pathIndex + 1)
			{
				isMoveAble = false;
				GameManager.instance.removeUnit(this);
				Destroy(this.gameObject);
				return;
			}
			setNextPoint();
		}
	}
	int makeNomal(float f)
	{
		float k = 0.1f;
		if(f>k)return 1;
		else if(f<-k)return -1;
		else return 0;
	}
	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 bool getPath()
	{
		float cellSize = GameManager.instance.cellSize;
		startPoint = new Point((int)(this.transform.localPosition.x/cellSize),-(int)(this.transform.localPosition.y/cellSize) );
		int wallMapIndex = GameManager.instance.wallMap[startPoint.x,startPoint.y];
		if(wallMapIndex > 0 && wallMapIndex < 10)return true;

		Point[] pArr = PathFinder.instance.getPath(startPoint, 111);
		if(pArr == null){Debug.Log("NULL path");return false;}
		pathArr = pArr;

		if(nextPoint != null && pathArr.Length > 1 && nextPoint.isEqual(pathArr[1]))pathIndex = 1;
		else pathIndex = 0;
		nextPoint = pathArr[pathIndex];
		showCharDir();
		return true;
	}
	public void attackMe(float dmg)
	{
		curHp -= dmg;
		if(curHp < 0)
		{
			isMoveAble = false;
			spr.Play ("destroy");
			GameManager.instance.addGold(gainGold);
			GameManager.instance.removeUnit(this);
			spr.AnimationCompleted = unitDestoryAniComplete;
		}
	}
	private void unitDestoryAniComplete(tk2dSpriteAnimator sprite, tk2dSpriteAnimationClip clip)
	{
		Destroy(this.gameObject);
	}
	private void setNextPoint()
	{
		startPoint = nextPoint;
		pathIndex++;
		nextPoint = pathArr[pathIndex];
		showCharDir();
	}
	private void showCharDir()
	{
		
		float cellSize = GameManager.instance.cellSize;
		float nx = (nextPoint.x * cellSize + cellSize/2.0f);
		float ny = (-nextPoint.y * cellSize - cellSize/2.0f);
		if(this.transform.localPosition.x<nx)
			spr.Play (charAniStr[(int)CHAR_ANI.RIGHT]);
		else if(this.transform.localPosition.x>nx)
			spr.Play (charAniStr[(int)CHAR_ANI.LEFT]);
		else if(this.transform.localPosition.y<ny)
			spr.Play (charAniStr[(int)CHAR_ANI.UP]);
		else if(this.transform.localPosition.y>ny)
			spr.Play (charAniStr[(int)CHAR_ANI.DOWN]);
		spr.ClipFps *= moveSpeed;
	}
}



신고
Posted by andwhy

실수로 TowerBase.cs파일이 마지막버전이 아닌채로 올라가있었고,
GameManager.cs 파일이 누락되어 있었습니다.
다시 적용했으니 한번 더 봐주세요. 

죄송합니다.


아...원래 타워만 짓고, 유닛 웨이브 데이터 만들고 마무리 할려고 했는데...

만들다 보니 재밌어보여서 기능을 좀 더 추가 하려고 합니다.

앞으로 할 내용들을 잠깐 소개하면...

- 타워 업그레이드, 타워 팔기

- 소유 금액 구현(타워 짓기, 업그레이드시 필요, 유닛 죽일때 획득.)

- 유닛 웨이브데이터.(한...50웨이브정도면 될까요??)

- UI표시.

정도 생각하고 있습니다.

유닛 종류나, 다른종류의 타워들도 더 넣을까 싶었는데, 너무 길어질것 같아서, 우선은 여기까지 목표로 하고, 다음에 추가로 만들던지 하겠습니다.


이번 5장 에서 다룰 내용은 "타워 업그레이드&판매"와 "소지금액"을 만들겁니다.


1. TowerBase.cs 파일 코드 추가.

먼저 타워의 업그레이드를 만들건데,

보통은 json이나 기타 DB형식으로 가지고 있을텐데, 벨런싱하기 쉽도록 유니티의 인스팩터를 최대한 활용하겠습니다.

우선 타워의 업그레이드 요소는, "공격력", "공격속도", "공격범위" 를 업그레이드 할수 있도록 하겠습니다.

먼저 TowerBase.cs 에서 타워의 업그레이드 정보와 업그레이드 관련 함수들을 만들껀데, 공격력, 속도, 범위의 함수들은 거의 비슷한 동작을 함으로 반복적인 코드가 많이 보이게 될껍니다.

각각 요소를 Upgrade 할때 업그레이드 "레벨", "필요 금액", "업그레이드 후 값"등의 정보를 가지고 있는 "UpgradeInfo"라는 클래스를 TowerBase.cs 파일의 밑에 추가해줍니다.


클레스 바로 위에 보면 [System.Serializable] 이라는 메타테그가 있는데, 이렇게 메타테그를 지정해주면 개발자가 만든 클레스를 인스팩터상에 보여주게 됩니다.

테스트로 TowerBase클레스 안에 다음과 같이 Public 으로 UpgradeInfo배열을 만들어주면 인스팩터에 다음과 같이 표시되고, 배열길이를 정해주면, 각각의 UpgradeInfo를 인스팩터상에서 기입할수 있게 됩니다.


이부분은 테스트용이니 다시 지우시기바랍니다.

다시 TestBase 클레스 안에 각각 요소의 레벨(atkLv, spdLv, rngLv) 들을 만들어주고(궂이 외부에서 접근할 필요는 없어보여서 private로 선언합니다.)
, 나중에 구현할꺼지만 public 으로 처음 타워의 빌드가격도 필드를 만들어줍니다.

그리고 방금 테스트용으로 했던것 처럼 UpgradeInfo 배열을 공격력, 공격속도, 공격범위 별로 3가지를 만들어주는데, 다음과 같이 만들어서 초기값을 넣어주도록 하겠습니다.(어차피 인스팩터에서 변경되면 이값들은 무시됩니다.)



여기까지 잘 따라오셨다면, tower 프리팹을 선택했을때 나오는 인스팩터는 그림과 비슷한모양이 되어있을껍니다.


이제 업그레이드 정보에대한 준비는 모두 끝났고, 차후에 GameManager 에서 업그레이드에 필요한 금액을 가져올수 있고, 실제로 업그레이드를 할수 있는 
함수들을 만들겠습니다.
(공격력, 공속, 범위 에대한 함수들은 거의 동일하고, 업그레이드시 적용해주는 변수만 다르게 적용됨으로 한가지만 공격력에 대한함수만 적겠습니다.)



공격력 레벨업에 필요한 금액을 가져오는 함수인데, 표시된 부분을 보시면, 아랫쪽 함수와 동일한 이름에, 하나는 파라미터가 없고, 하나는 int 형의 파라미터 하나를 가지고 있습니다.
이 함수를 호출할때 아무 파라미터가 없이 getLvupAtkPrice();라고 호출하면 위쪽 함수가 호출되고, getLvupAtkPrice(1); 이렇게 int 형값이 들어가면 아랫쪽 함수가 호출됩니다.
지금의 함수의 경우 특정레벨을 정해주지 않는다고, 파라미터없이 호출한다면, 현재atk레벨보다 1레벨 높은 레벨의 값을 가져오도록 되어있습니다.


동일한 형식으로 upgradeAtk() 함수도 레벨을 정해주지 않으면 자동으로 1레벨 올리도록 되어있습니다.
공격력 업그레이드 함수는 다음 레벨의 UpgradeInfo 를 가져와서 그value값을 attack 변수에 셋팅해줍니다.

마찬가지로, 공속은 shotDelay, 범위는 range 를 셋팅해주면 됩니다.

마직막으로 타워가 처음 지어지면 모두 1레벨이 되도록 init()함수를 다음처럼 만들어주시고 Start 함수에서 호출해주시면 됩니다.



여기까지 TowerBase.cs 파일의 전체소스입니다.


TowerBase.cs


using UnityEngine;
using System.Collections;

public class TowerBase : MonoBehaviour {
	public UnitBase target;
	public float attack = 10.0f;
	private string spriteAtkNameFormat = "tower_a_{0:d2}";
	private string spriteNormalNameFormat = "tower_n_{0:d2}";
	public float range = 100;
	public float shotDelay = 3.0f;
	private float reloadTime = 0;
	private tk2dSprite spr;

	public int buildPrice = 50;
	private int atkLv = 1;
	private int spdLv = 1;
	private int rngLv = 1;
	public UpgradeInfo[] attackUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 20), new UpgradeInfo(2, 10, 25), new UpgradeInfo(3, 20, 30), new UpgradeInfo(4, 30, 35), new UpgradeInfo(5, 40, 40)
	};
	public UpgradeInfo[] speedUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 2.5f), new UpgradeInfo(2, 10, 2.1f), new UpgradeInfo(3, 20, 1.7f), new UpgradeInfo(4, 30, 1.3f), new UpgradeInfo(5, 40, .9f)
	};
	public UpgradeInfo[] rangeUpgradeInfoArr = new UpgradeInfo[]
	{
		new UpgradeInfo(1, 0, 100), new UpgradeInfo(2, 10, 130), new UpgradeInfo(3, 20, 160), new UpgradeInfo(4, 30, 190), new UpgradeInfo(5, 40, 220)
	};
	private UpgradeInfo getInfo(int lv, UpgradeInfo[] infoArr)
	{
		foreach(UpgradeInfo ui in infoArr)
		{
			if(ui.level == lv)return ui;
		}
		return null;
	}
    public int getLvupAtkPrice() { return getLvupAtkPrice(atkLv + 1); }
	public int getLvupAtkPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, attackUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
    public int getLvupSpdPrice() { return getLvupSpdPrice(spdLv + 1); }
	public int getLvupSpdPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, speedUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
    public int getLvupRngPrice() { return getLvupRngPrice(rngLv + 1); }
	public int getLvupRngPrice(int lv)
	{
		UpgradeInfo info = getInfo(lv, rangeUpgradeInfoArr);
		if(info== null)return -1;
		return info.price;
	}
	public void upgradeAtk(){upgradeAtk(atkLv+1);}
	public void upgradeAtk(int lv)
	{
		UpgradeInfo info = getInfo(lv, attackUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		atkLv = info.level;
		attack = info.value;
	}
	public void upgradeSpd(){upgradeSpd(spdLv+1);}
	public void upgradeSpd(int lv)
	{
		UpgradeInfo info = getInfo(lv, speedUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		spdLv = info.level;
		shotDelay = info.value;
	}
	public void upgradeRng(){upgradeRng(rngLv+1);}
	public void upgradeRng(int lv)
	{
		UpgradeInfo info = getInfo(lv, rangeUpgradeInfoArr);
		if(info== null)return;
		if(!GameManager.instance.checkGold(info.price))return;
		GameManager.instance.useGold(info.price);
		totalPrice += info.price;
		rngLv = info.level;
		range = info.value;
	}
	// Use this for initialization
	void Start () {
		spr = this.GetComponentInChildren<tk2dSprite>();
		reloadTime = shotDelay;
	}

	// Update is called once per frame
	void Update () {
		spr.SortingOrder = -(int)this.transform.localPosition.y;
		checkRangeTarget();
		lookAtTarget();
		autoShot();
		attackSprAnim();
		updateSprite();
	}
	private int spritN = 0;
	void updateSprite()
	{
		string spriteName = string.Format(isAtkSpr?spriteAtkNameFormat:spriteNormalNameFormat, spritN);
		spr.spriteId = spr.GetSpriteIdByName(spriteName);
	}
	void lookAtTarget()
	{
		if(target == null)return;
		float anglePI = Mathf.Atan2(this.transform.localPosition.y - target.transform.localPosition.y, this.transform.localPosition.x - target.transform.localPosition.x) + Mathf.PI/2.0f;
		int angle = 36 -(int)((anglePI/Mathf.PI * 18.0f) + 36)%36;
		spritN = 18 -Mathf.Abs(angle - 18);
		int spriteDir = angle<18?1:-1;
		spr.scale = new Vector3(spriteDir, 1,1);

	}
	void checkRangeTarget()
	{
		if(target != null)
		{
			if(range < Vector2.Distance((Vector2)target.transform.localPosition, (Vector2)this.transform.localPosition))target = null;
		}
		if(target == null)
		{
			foreach(UnitBase ub in GameManager.instance.unitList)
			{
				if(range > Vector2.Distance((Vector2)ub.transform.localPosition, (Vector2)this.transform.localPosition))
				{
					target = ub;
					return;
				}
			}
		}

	}

	void autoShot()
	{
		reloadTime -= Time.deltaTime;
		if(target == null || reloadTime>0)return;
		attackTarget();

	}
    bool isAtkSpr = false;
	bool isAttacking = false;
	float attackTimeCount = 0f;
	float attackDuration = .1f;
	float attackDelay = .13f;
	int attackRepeat = 3;
	int attackedCount = 0;
	void attackTarget()
	{
		if(target == null || isAttacking)return;
		attackTimeCount = 0;
		attackedCount = 0;
		isAtkSpr = true;
		isAttacking = true;
		reloadTime = shotDelay;

		target.attackMe(attack);
	}
	void attackSprAnim()
	{
		if(!isAttacking)return;
		attackTimeCount += Time.deltaTime;
		if(isAtkSpr)
		{
			if(attackDuration<attackTimeCount)isAtkSpr = false;
		}
		else if(attackDelay<attackTimeCount)
		{
			attackedCount++;
			if(attackedCount<attackRepeat)
			{
				isAtkSpr = true;
				attackTimeCount -= attackDelay;
			}
			else isAttacking = false;
		}
	}
}
[System.Serializable]
public class UpgradeInfo
{
	public int level = 0;
	public int price = 0;
	public float value = 0;
	
	public UpgradeInfo(int _level, int _price, float _value)
	{
		level = _level;
		price = _price;
		value = _value;
	}
}

2.GameManager.cs 수정

GameManager 에서 타워 오브젝트를 삭제 하는함수를 추가하겠습니다.

그전에 우선 어떤타워를 삭제 할지 알아야하기때문에 타워 좌표key값으로 하는 Dictionary를 만들고, build시에 Dictionary에 타워를 추가하도록 합니다.



하는김에 init() 함수에서 towerDic.Clear()를 호출해서 클리어도 해줍니다.



이번엔 실제로 선택된 타워를 화면에서 삭제하고, 삭제된 좌표를 다시 길로 바꿔주는 removeTower함수를 만들도록 하겠습니다.


선택된 TowerBase 객체를 towerDic에서 찾아서, key로 가지고 있는 좌표를 "0"(지나갈수 있는 길) 로 변경해주고, dictionary 에서 제거 합니다.
그리고 배경을 다시그리고, 패스파인더 경로도 재검색시킵니다.
(checkReachAble() 함수를 실행시키면 내부에서 자동으로 경로를 검사합니다. 이미 길이 하나 이상있는상태에서 벽이 사라지는것이기 떄문에 checkReachAble은 항상 true가 반환될것입니다.)


GameManager.cs 의 전체 소스는 잠시뒤에 TowerMenu 작업이 끝나면 추가해줄것들이 좀더 있어서 마지막에 공유 하겠습니다.


3. TowerMenu 리소스 만들기.

이번에는 이미 지어진 타워를 선택했을때 타워 업그레이드나 타워를 되파는 메뉴화면을 구성해보겠습니다.


towerMenuResource.zip

(이 리소스 파일은 다른 게임화면에서 임의로 편집해온것이니, 이 강좌의 따라하기 용도 이외에는 절대로 사용하지 마십시오.)

타워 메뉴의 리소스를 다운받으시고, 리소스 파일들을 유니티 프로젝트로 옮겨 놓습니다.


강의 초반에 만들었던 BGUI라는 스프라이트 컬랙션은 Project텝에서 선택한후 인스펙터의 "OpenEditor" 버튼을 누릅니다.


에디터 창에 방금 추가했던 towerMenu 리소스들을 들록 시키고 "commit" 버튼을 누릅니다.


리소스 등록은 끝났고, 이제부터 리소스들로, 메뉴UI작업을 할껀데, 좀 복잡할것 같아서.. 챕터를 나누겠습니다.

4.UI만들기.

좀 복잡할꺼 같긴하지만.. 차근차근 하나씩 해보겠습니다.

우선 하이라키의 tk2dCamera/stage/gameField 아래에 "towerMenu"라는 이름의 비어있는 게임오브젝트를 만들고. 그 밑에 Btn이라는 게임오브젝트를 만들고, 그 밑에 tk2dSrpite 를 만들어줍니다.

이상태로 인스팩터의 Tk2dSprite 항목에서 CollectionBGUI, spritesell_enable 로 변경해줍니다.


다시 하이라키탭에서 sprite를 선택한 상태에서 "Control + D" 키를 누르면 Sprite가 복제가 됩니다.

복제된 Sprite 들의 이름을 enableSpr, disableSpr 로 변경해주고, disableSpr의 이미지를 sell_disable로 바꿔줍니다.
업그레이드 버튼일경우 돈이 부족하면 disable상태로 표시해주려고 2개의 스프라이트를 추가해두었습니다.
(코딩으로 스프라이트 아이디를 변경해버리는 방법도 있지만, 코딩이 더 복잡해질것같아서 오브젝트를 껏다 켰다 하도록 하겠습니다.)

다시 하이라키에서 한단계 위인 btn 오브젝트를 선택하고, 인스팩터의 AddComponent 버튼을 클릭합니다.
검색에서 을 검색해서 추가시켜줍니다.
그리고 boxCollider도 검색해서 추가시켜주면, tk2dUIItem 컴포넌트에 Collider 항목이 보이게 됩니다.
마지막으로 tk2dUITweenItem 항목까지 추가시켜서 버튼을 클릭했을때 버튼이 살짝 작아지는 컴포넌트까지 추가해줍니다.


"fit" 버튼을 누르면 박스 컬라이더가 버튼 크기에 딱 맞게 사이즈가 조정됩니다.

하이라키에서 create->tk2d->UIManager 항목을 클릭해서 tk2dUIManager를 생성합니다.

tk2dUIManager 오브젝트를 클릭해보면 인스팩터에 UiCamera를 입력할수 있는 필드가 보이는데, 당연히 tk2dCamera를 드래그 해놓습니다.

플레이버튼을 눌러서 게임화면에 방금 만든 Btn을 클릭해보면 버튼이 살짝 작아지면서 클릭이 잘되고 있는것을 볼수 있습니다.

다시 에디트 화면으로 넘어와서, 업그레이드나 판매 금액을 표시해줄 라벨을 추가하도록 하겠습니다.

Btn을 클릭한상태로 create->tk2d->textMesh 를 선택하고 생성된 TextMesh의 이름을 "Label"로 바꿔줍니다.


라벨을 선택한 상태에서 인스팩터 화면에서, font는 "UIDemoOldSansBlack"을 선택하고, 인스팩터 하단의 1:1 버튼을 클릭합니다.

그러면 게임화면에 다음과 비슷한 화면이 보이게 되는데, 현재 label이 버튼 이미지 뒤에 가려서 안보이고 있습니다.

그리고 버튼 텍스트라고 하기엔 너무 큰상태 이기도 합니다.

인스펙터에서, order In Layer라는 항목을 1 로 수정해주고, text항목을 "$1,000" 정도로 바꿔서 scale값과 위치값을 적당하게 바꿔줍니다.
Anchor도 Middle Center로 변경해줍니다.


5.TowerMenuBtn Script작업 및 연결

방금 위에서 만든 버튼을 제어하기 위한 TowerMenuBtn.cs파일을 작성합니다.

이 클레스에선 버튼의 enable/disable 상태를 표시해주며, 라벨에 글자를 표시해주는 역할을 합니다.

TowerMenuBtn.cs


using UnityEngine;
using System.Collections;

public class TowerMenuBtn : MonoBehaviour {
	public tk2dUIItem uiItem;
	public tk2dSprite btnSpr;
	public tk2dSprite disableSpr;
	public tk2dTextMesh label;
	
	private bool _enable = true;
	public bool enable
	{
		get{return _enable;}
		set{
			_enable = value;
			setSprite();
		}
	}
	public void setLabel(string msg)
	{
		label.text = msg;
	}
	void Start()
	{
		setSprite();
	}
	private void setSprite()
	{
		btnSpr.gameObject.SetActive(enable);
		disableSpr.gameObject.SetActive(!enable);
	}
}

새로 작성한 TowerMenuBtn을 하이라키의 Btn에 드래그해서 컴포넌트를 추가해주시고, 인스팩터의 각각의 필드에 알맞는 오브젝트들을 배치시켜주시면 됩니다.



이제 기본버튼은 만들어 졌습니다.

이 버튼들을 가지고, 이미지만변경해서 업그레이드 버튼 3종과, 판매 버튼을 만들껀데,
버튼을 복사하기 전에, Z-order 를 좀 정리하겠습니다.

지난시간에 Tower와 유닛들의 Z-order를 정해서, 그리는순서를 정했습니다. 이 게임메뉴들은 항상 타워나 다른유닛보다 최상단에 보여져야 하기때문에
 이 Z-Order를 약간 크게 바꿔주도록 하겠습니다.

버튼안의 enableSpr과, disableSprOrder In Layer 항목은 1000 으로 셋팅하고, Label은 버튼위쪽에 보여지게 하고싶기때문에 1001로 바꿔줍니다.


이제 버튼을 복제해서, 업그레이드 버튼 3종과, 판매버튼을 만들겠습니다.

Btn오브젝트를 선택후 "Cntrol + D" 를 3회 연타 하면 Btn이 복제되어서 총 4개가 되어있는데, 이를 각각 "sell_btn", "atk_upgrade_btn","speed_upgrade_btn","range_upgrade_btn" 으로 이름을 변경해줍니다.


각각 버튼 안의 enableSprdisableSpr의 이미지들을 각각 버튼 이름에 맞는 이미지로 교체해줍니다.
그리고 화면상에 다음과 같이 배치해놓습니다.

12시 방향부터 시계방향으로 sell, range, speed, atk 버튼들이 각가 배치 되어있는데, 이 버튼들의 localPosition은 twoerMenu 오브젝트를 기준으로 삼아서,
(0, 60), (-60, 0), (0, -60), (60, 0) 이렇게 4군데로 배치해둡니다.
(뒤에 메뉴를 타워 위쪽에 띄울때 towerMenu오브젝트의 좌표를 타워와 동일하게 맞춰서 메뉴를 보여줄것입니다.)

타워의 공격범위를 표시해줄 range 스프라이트도 이 towerMenu오브젝트에 넣어주겠습니다.

towerMenu 오브젝트를 선택한 상태에서 tk2dSprite를 추가해주고, collection은 BGUI, sprite는 range로 설정하고, order in Layer는 999로 맞춰줍니다.

6. TowerUIMenu Script작업 및 연결

TowerUIMenu.cs 를 새로 작성합니다.

TowerUIMenu.cs는 지금 만들었던 버튼들이 눌렸을때 각자역할을 연결해주고, 게임화면에서 타워를 선택했을때 타워의 위치위에 메뉴를 띄우고 사라지게 하는역할을 합니다.

먼저 전체소스를 공유 하고 설명하도록 하겠습니다.

TowerUIMenu.cs


using UnityEngine;
using System.Collections;

public class TowerUIMenu : MonoBehaviour {
	
	public TowerMenuBtn sellBtn; 
	public TowerMenuBtn atkUpgradeBtn; 
	public TowerMenuBtn speedUpgradeBtn; 
	public TowerMenuBtn rangeUpgradeBtn;
	
	public tk2dUIItem blinkArea;
	
	public tk2dSprite rangeSpr;
	
	private TowerBase targetTower;
	public bool isShow = false;
	// Use this for initialization
	void Start () {
		addBtnEvents();
		hideMenu();
	}
	void addBtnEvents()
	{
		sellBtn.uiItem.OnClick -= towerSell;
		sellBtn.uiItem.OnClick += towerSell;
		atkUpgradeBtn.uiItem.OnClick -= upgradeAtk;
		atkUpgradeBtn.uiItem.OnClick += upgradeAtk;
		speedUpgradeBtn.uiItem.OnClick -= upgradeSpeed;
		speedUpgradeBtn.uiItem.OnClick += upgradeSpeed;
		rangeUpgradeBtn.uiItem.OnClick -= upgradeRange;
		rangeUpgradeBtn.uiItem.OnClick += upgradeRange;
		
		blinkArea.OnClick -= hideMenu;
		blinkArea.OnClick += hideMenu;
	}
	public void showMenu(TowerBase target)
	{
		targetTower = target;
		//set position;
		this.transform.localPosition = targetTower.transform.localPosition;
		setRange(targetTower.range);
		
		isShow = true;
	}
	public void hideMenu()
	{
		this.transform.localPosition = new Vector3(-90000,0,0);
		targetTower = null;
		isShow = false;
	}
	
	private void towerSell()
	{
		Debug.Log("TowerSell");
		GameManager.instance.removeTower(targetTower);
		hideMenu();
	}
	private void upgradeAtk()
	{
		Debug.Log("upgradeAtk");
		targetTower.upgradeAtk();
	}
	private void upgradeSpeed()
	{
		Debug.Log("upgradeSpeed");
		targetTower.upgradeSpd();
	}
	private void upgradeRange()
	{
		Debug.Log("upgradeRange");
		targetTower.upgradeRng();
		setRange(targetTower.range);
	}
	private void setRange(float r)
	{
		rangeSpr.scale = Vector3.one * (r/40.0f);
	}
}

변수영역 부터 살펴보면,


위쪽에서 만든 4개의 버튼들이 먼저 보이고, blinkArea 라는 UIItem(버튼입니다.)이 있고, 공격범위를 나타내줄 rangeSpr 이 있습니다.

아래의 targetTower는 선택된 타워를 저장할것이고, isShow 는 현재 메뉴가 보이는상태인지 숨겨져있는 상태인지 나태내주는 플래그 값입니다.

여기서 blinkArea라는 변수는 자기 자신의 버튼영역을 넣어줄것인데, 위쪽에 생성된 4개의 버튼 보다 아래쪽 레이어에 버튼영역을 만들고, 위의 4개버튼이 눌리지 않고 화면의 빈공간이 눌리게 되면 메뉴를 숨겨주기 위해 만들어놓은 버튼 영역 입니다.


함수중에, addBtnEvents()를 보면,


각각 버튼들을 클릭했을때, 각각 취해야할 행동들을 delegate로 연결해주었습니다.

주의해서 보셔야 할것들은, TowerMenuBtn 객체들은 바로 OnClick 델리게이트를 등록하는게 아니라, uiItem 이란 맴버 변수를 통해서 등록하게 되어있는것과,
각각 델리게이터를 "-=" 로 한번씩 제거해준뒤 "+=" 로 추가 해주었는데, addBtnEvents가 여러번 호출되서 델리게이터가 중복으로 등록되지 안도록 하기위한 조치입니다.(addBtnEvents 함수도 여러번 호출이 될일은 없습니다.)



showMenuhideMenu는 간단한데, showMenu는 파라미터로 온 타워를 targetTower에 저장하고 메뉴의 위치를 타겟타워와 같은위치로 옮겨서 보여줍니다.
hideMenu는 타겟타워를 null로 바꿔지고, 메뉴를 화면에서 안보이는 곳으로 옮겨버립니다.


버튼과 연결된 towerSell(), upgradeAtk(), upgardeSpeed(), upgradeRange() 함수들은 각각의 기능에 맞는 함수들을 연결해주는 역할임으로 설명은 생략합니다.

마지막으로 볼함수가 setRange입니다.


이 함수는 rangeSpr의 크기를 변경시켜서 화면에 공격범위를 표시해주는 역할입니다.
눈치 채신분도 있겠지만 range 스프라이트는  지름이 80 px인 원 이미지 입니다. 이걸 우리가 원하는 대로마추기 위해선 위의 공식대로 반지름으로 나눈만큼 크기를 곱해주면 됨니다.


이제 작성된 TowerUIMenu.cs towerMenu 오브젝트에 추가하고, towerMenu 오브젝트에 UIItem과 BoxCollider도 추가해줍니다.
BoxCollider의 크기는 충분히 크게, x:3000, y:3000 정도로 잡아주시고, 중심위치를 약간 뒤쪽으로 바꿔줍니다.(z값만 1로 변경)

TowerUIMenu 컴포넌트의 비어있는 필드중 blinkArea는 위의 그림처럼 같은 인스펙터상의 UIItem객체를 드래그 해놓습니다.

나머지 비어있는 필드는 직접한번 넣어보시기 바랍니다.

도저히 모르시겠는 분들은..

이거보고 참고하시면 됩니다.


7. GameManager 수정및 연결

 이제 GameManager에 이미 세워져 있는 타워를 선택하면 TowerUIMenu가 나올수 있도록 약간의 추가 작업만 해주면 됩니다.

GameManager 의 추가된 부분을 보면,

public 변수로 위에서 만든 TowerUIMenu 가 변수로 선언되어 있습니다. 잠시뒤에 towerMenu를 연결해주면 됩니다.


Update함수를 보면

크게 2부분이 변경되었는데,

1. totwerMenu의 isShow가 true면(메뉴가 보여지는 상태라면..) 아무것도 하지 않습니다.
2. 메뉴가 화면에 없고, 화면을 마우스로 클릭했을때, 클릭한 좌표에 이미 타워가 존재하면 메뉴를 보여주고, 존재하지 않는다면 타워를 짓습니다.

아주 간단하죠...

네모칸안에 쓰인 showMenu도 너무나도 간단하네요..



이젠 마지막단계로

GameManager 객체를 선택하고  towerMenu 항목에 towerMenu오브젝트를 드래그해서 연결해줍니다.


이제 플레이버튼을 눌러서 화면상에 타워를 하나 짓고, 그 타워를 클릭해보면 방금 작업한 MenuUI 가 나오는걸 볼수 있습니다.

버튼을 눌렀을때, sell버튼과, range버튼은 화면이 변하는걸 확인할수 있지만, 공격력이나 공속업그레이드는 화면에 바로 표시가 안되서, 해당 타워의 인스펙터값을 확인하는 방법밖엔 없겠네요.


오늘 강의는 여기까지입니다.

리소스준비와, 스크립트를 왔다갔다 하면서 진행하느라 이전보다 복잡하고, 정신없었네요.

오늘 정신없는 강의 따라오시느라 수고 많으셨습니다.

감사합니다.

실수로 누락되었던 GameManager.cs 전체 소스 올립니다.
(새벽에 쓰느라 정신이 없었네요..죄송합니다.ㅠㅠ)

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;
		init();
	}
	//=========================================
	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>();
	[HideInInspector]
	public Dictionary<Point, TowerBase> towerDic = new Dictionary<Point, TowerBase>();
	public BgCellDisplayer bgGrid;
	public Transform unit_field;
	public UnitBase unit_marine;
	public TowerBase tower;
	public TowerUIMenu towerMenu;

	public Camera mainCam;
	private void init()
	{
		unitList.Clear();
		towerDic.Clear();
		initPathFinder();
	}
	void addUnit()
	{
		UnitBase unit = Instantiate(unit_marine) as UnitBase;
		//GameObject go = Instantiate(unit_marine) as GameObject;
		//UnitBase unit = go.GetComponentInChildren<UnitBase>();
		unitList.Add(unit);
		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);

	}
	void addTower(Point p)
	{

		TowerBase tw = Instantiate(tower) as TowerBase;
		tw.transform.parent = unit_field;
		tw.transform.localPosition = new Vector3(p.x * cellSize + cellSize/2.0f, -p.y * cellSize - cellSize);
		towerDic.Add(p, tw);
		
	}
	public void removeUnit(UnitBase ub)
	{
		unitList.Remove(ub);
	}
	void researchPathUnits()
	{
		foreach(UnitBase ub in unitList)
		{
			if(ub!=null)
			{
				ub.getPath();
			}
		}
	}

	Point getStartPoint()
	{
		//check startPoints
		List<Point> startPointList = getStartPointList();
		if(startPointList.Count == 0)
		{
			Debug.LogError("Not Found Start Position");
			return null;
		}
		int ranIdx = Random.Range(0,startPointList.Count);
		return startPointList[ranIdx];
	}
	List<Point> getStartPointList()
	{
		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));
				}
			}
		}
		return startPointList;
	}
	public void initPathFinder()
	{
		PathFinder.instance.setMapData(wallMap);
	}

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

	}
	void Update()
	{
		if (Input.GetMouseButtonDown (0) && !towerMenu.isShow) 
		{
			Vector2 pos = Input.mousePosition;
			Vector3 mouseP = mainCam.ScreenToWorldPoint(pos) - unit_field.TransformPoint(Vector3.zero);
			Point myPos = new Point((int)(mouseP.x/cellSize), -(int)(mouseP.y/cellSize));
			if(towerDic.ContainsKey(myPos))
			{
				showMenu(towerDic[myPos]);
			}
			else 
			{
				buildTower(myPos);
			}
			//Debug.Log(myPos.ToString());
		}
		if (Input.GetKey(KeyCode.Space)) addUnit();
	}
	bool checkReachAble()
	{
		PathFinder.instance.setCheckMode(true);
		foreach(Point sp in getStartPointList())
		{
			if(PathFinder.instance.getPath(sp,100+wallMap[sp.x,sp.y]) == null)
			{
				Debug.Log("StartPoint Path NULL");
				return false;
			}
		}
		foreach(UnitBase unit in unitList)
		{
			if(!unit.getPath())
			{
				Debug.Log("Unit Path NULL");
				return false;
			}
		}

		PathFinder.instance.setPath();
		return true;
	}
	void buildTower(Point p)
	{
		if(p.x<0||p.y<0 || p.x >= wallMap.GetLength(0) || p.y >= wallMap.GetLength(1))return;
		int prevIndex = wallMap[p.x, p.y];

		if(wallMap[p.x, p.y] == 0)wallMap[p.x, p.y] = 2;
		else return;
		//else if(wallMap[p.x, p.y] == 2)wallMap[p.x, p.y] = 0;


		if(checkReachAble())
		{
			bgGrid.refreshDisplay();
			researchPathUnits();
			addTower(p);
		}
		else 
		{
			wallMap[p.x, p.y] = prevIndex;
		}
		PathFinder.instance.setCheckMode(false);
	}
	public void removeTower(TowerBase tw)
	{
		if(!towerDic.ContainsValue(tw))return;
		foreach(Point keyP in towerDic.Keys)
		{
			if(towerDic[keyP] == tw)
			{
				if(keyP.x<0||keyP.y<0 || keyP.x >= wallMap.GetLength(0) || keyP.y >= wallMap.GetLength(1))return;
				wallMap[keyP.x, keyP.y] = 0;
				towerDic.Remove(keyP);
				break;
			}
		}
		Destroy(tw.gameObject);
		bgGrid.refreshDisplay();
		PathFinder.instance.setPath();
		PathFinder.instance.setCheckMode(false);
	}
	void showMenu(TowerBase tw)
	{
		towerMenu.showMenu(tw);
	}
}




신고
Posted by andwhy

음....이번껀 강좌라고 하기엔 너무 짧고, 그냥 중간에 팁??정도로 생각하시면 됩니다.


저번시간까지 만든결과물을 보면,

그림처럼 유닛이 타워에 박혀서 보일수도 있는데..(제대로 보일수도 있습니다.)

이런경우 이외에 유닛이 y축으로 일열로 내려올때 뎁스가 엉켜서 보일수도 있습니다.
(타워디펜스류가 아니더라도 2디게임을 만들면 상당히 자주 보는일입니다.)

뭐...원인은 뎁스 문제인데 해결방법도 간단합니다.

spritezorder를 y 좌표에 맞게 고쳐주면 됩니다.

'tk2dSprite' 에는 SortingOrder라는 값을 설정해주면 플러그인에서 zorder를 설정해줍니다.

UnitBase.cs TowerBase.cs 파일의 update 함수에 각각 다음처럼 한줄씩 추가해주면 됩니다.

UnitBase.cs


TowerBase.cs




잘보시면 UnitBase는 spr.Sprite.SortingOrder 를 변경하고, TowerBase는 spr.SortingOrder를 변경하는데,
UnitBase는 spr이 'tk2dSpriteAnimator' 이고, TowerBase는 'tk2dSprite' 이기 때문입니다.



신고
Posted by andwhy

이번시간엔 맵을 클릭할때 실제로 타워를 배치하고, 유닛들이 타워를 벽으로 인식하고 돌아가도록 만들어 보겠습니다.


GameManager.cs 파일을 수정하여, 벽을 만들던 함수 대신 타워를 배치하는 함수로 변형하겠습니다.

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;
		init();
	}
	//=========================================
	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 BgCellDisplayer bgGrid;
	public Transform unit_field;
	public UnitBase unit_marine;
	public TowerBase tower;
	
	public Camera mainCam;

	private void init()
	{
		unitList.Clear();

		initPathFinder();
	}
	void addUnit()
	{
		UnitBase unit = Instantiate(unit_marine) as UnitBase;
		unitList.Add(unit);
		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);

	}
	void addTower(Point p)
	{
		TowerBase tw = Instantiate(tower) as TowerBase;
		tw.transform.parent = unit_field;
		tw.transform.localPosition = new Vector3(p.x * cellSize + cellSize/2.0f, -p.y * cellSize - cellSize);
		
	}
	public void removeUnit(UnitBase ub)
	{
		unitList.Remove(ub);
	}
	void researchPathUnits()
	{
		foreach(UnitBase ub in unitList)
		{
			if(ub!=null)
			{
				ub.getPath();
			}
		}
	}
	Point getStartPoint()
	{
		List<Point> startPointList = getStartPointList();
		if(startPointList.Count == 0)
		{
			Debug.LogError("Not Found Start Position");
			return null;
		}
		int ranIdx = Random.Range(0,startPointList.Count);
		return startPointList[ranIdx];
	}
	List<Point> getStartPointList()
	{
		//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));
				}
			}
		}
		return startPointList;
	}
	public void initPathFinder()
	{
		PathFinder.instance.setMapData(wallMap);
	}
	void OnGUI()
	{
		if(GUI.Button( new Rect( 10, 10, 100, 40), "Add Unit"))
		{
			addUnit();
		}
	}
	void Update()
	{
		if(Input.GetMouseButtonDown(0))
		{
			Vector2 pos = Input.mousePosition;
			Vector3 mouseP = mainCam.ScreenToWorldPoint(pos) - unit_field.TransformPoint(Vector3.zero);
			Point myPos = new Point((int)(mouseP.x/cellSize), -(int)(mouseP.y/cellSize));
			buildTower(myPos);
		}
	}
	bool checkReachAble()
	{
		PathFinder.instance.setCheckMode(true);
		foreach(Point sp in getStartPointList())
		{
			if(PathFinder.instance.getPath(sp,100+wallMap[sp.x,sp.y]) == null)
			{
				Debug.Log("StartPoint Path NULL");
				return false;
			}
		}
		foreach(UnitBase unit in unitList)
		{
			if(!unit.getPath())
			{
				Debug.Log("Unit Path NULL");
				return false;
			}
		}
		PathFinder.instance.setPath();
		return true;
	}
	void buildTower(Point p)
	{
		if(p.x<0||p.y<0 || p.x >= wallMap.GetLength(0) || p.y >= wallMap.GetLength(1))return;
		int prevIndex = wallMap[p.x, p.y];
		
		if(wallMap[p.x, p.y] == 0)wallMap[p.x, p.y] = 2;
		else return;

		if(checkReachAble())
		{
			bgGrid.refreshDisplay();
			researchPathUnits();
			addTower(p);
		}
		else 
		{
			wallMap[p.x, p.y] = prevIndex;
		}
		PathFinder.instance.setCheckMode(false);
	}
}


변경된부분만 간단히 보겠습니다.

 변수 


맵에 추가될 Tower prefab입니다.
바로 위의 UnitBase와 다를게 없네요.


 함수 

addTower() 는 원하는 좌표에 타워를 배치해주는 역할입니다.
addUnit() 과 크게 다르지 않습니다.


buildTower()는 switchWall() 를 변형하였습니다.
다시 땅으로 바꿔주는 코드를 제거하고, 타워를 추가하는 코드가 들어갔습니다.



switchWall() 이 변경되면서 마우스로 클릭했을때 호출하는 함수도 switchWall() 에서 buildTower로 변경해줍니다.


이젠 에디터 화면으로 돌아와서, 지난시간에 만들었던 tower 오브젝트를 prefab으로 만들어줍니다.
만드는방법은 유닛을 만들었을때처럼 프로젝트의 적당한경로로 드래그 하시면 됩니다.


하이라키 의 tower오브젝트는 삭제해주세요..


하이라키의 GameManager 의 인스팩터를 보면 스크립트에서 추가한 TowerBase 타입의 Tower라는 변수에 방금 만든 "tower" 프리팹을 드래그해서 연결해줍니다.



이제 플레이버튼을 누르고 맵을 마우스로 클릭해보면, 클릭한 위치에 타워가 배치됩니다.
유닛들을 추가해보면 타워를 벽으로 인식하고 길을 찾아서 빙빙 돌아가는것을 볼수 있습니다.



이렇게 보니, 이젠 타워디팬스 게임같이 보이네요.

근데 문제가 있는데, 아무리 공격을 해도 유닛들이 죽지않네요.

이제부턴 유닛들을 죽을수 있도록 해주겠습니다.


UnitBase.csTowerBase.cs를 수정해서 유닛의 체력을 만들어주고, 타워의 공격력을 만들어줄겁니다.
타워가 유닛을 공격할때마다 유닛은 타워의 공격력만큰 체력을 깍아주고, 체력이 0이하가 되면, 유닛을 사라집니다.


UnitBase.cs



using UnityEngine;
using System.Collections;
using common;

public class UnitBase : MonoBehaviour {
	public float maxHp = 100;
	public float curHp = 100;
	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 () {
		curHp = maxHp;
	}
	private bool isMoveAble = false;
	// Update is called once per frame
	void Update () {
		if(!isMoveAble)return;
		float cellSize = GameManager.instance.cellSize;
		float _speed = cellSize * Time.deltaTime * moveSpeed;
		float rx = (nextPoint.x * cellSize + cellSize/2.0f) - this.transform.localPosition.x;
		float ry = (-nextPoint.y * cellSize - cellSize/2.0f) - this.transform.localPosition.y;
		float dx = _speed * makeNomal(rx);
		float dy = _speed * makeNomal(ry);
		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;
				GameManager.instance.removeUnit(this);
				Destroy(this.gameObject);
				return;
			}
			setNextPoint();
		}
	}
	int makeNomal(float f)
	{
		float k = 0.1f;
		if(f>k)return 1;
		else if(f<-k)return -1;
		else return 0;
	}
	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 bool getPath()
	{
		float cellSize = GameManager.instance.cellSize;
		startPoint = new Point((int)(this.transform.localPosition.x/cellSize),-(int)(this.transform.localPosition.y/cellSize) );
		int wallMapIndex = GameManager.instance.wallMap[startPoint.x,startPoint.y];
		if(wallMapIndex > 0 && wallMapIndex < 10)return true;

		Point[] pArr = PathFinder.instance.getPath(startPoint, 111);
		if(pArr == null){Debug.Log("NULL path");return false;}
		pathArr = pArr;

		if(nextPoint != null && pathArr.Length > 1 && nextPoint.isEqual(pathArr[1]))pathIndex = 1;
		else pathIndex = 0;
		nextPoint = pathArr[pathIndex];
		showCharDir();
		return true;
	}
	public void attackMe(float dmg)
	{
		curHp -= dmg;
		if(curHp < 0)
		{
			isMoveAble = false;
			spr.Play ("destroy");
			GameManager.instance.removeUnit(this);
			spr.AnimationCompleted = unitDestoryAniComplete;
		}
	}
	private void unitDestoryAniComplete(tk2dSpriteAnimator sprite, tk2dSpriteAnimationClip clip)
	{
		Destroy(this.gameObject);
	}
	private void setNextPoint()
	{
		startPoint = nextPoint;
		pathIndex++;
		nextPoint = pathArr[pathIndex];
		showCharDir();
	}
	private void showCharDir()
	{
		
		float cellSize = GameManager.instance.cellSize;
		float nx = (nextPoint.x * cellSize + cellSize/2.0f);
		float ny = (-nextPoint.y * cellSize - cellSize/2.0f);
		if(this.transform.localPosition.x<nx)
			spr.Play (charAniStr[(int)CHAR_ANI.RIGHT]);
		else if(this.transform.localPosition.x>nx)
			spr.Play (charAniStr[(int)CHAR_ANI.LEFT]);
		else if(this.transform.localPosition.y<ny)
			spr.Play (charAniStr[(int)CHAR_ANI.UP]);
		else if(this.transform.localPosition.y>ny)
			spr.Play (charAniStr[(int)CHAR_ANI.DOWN]);
		spr.ClipFps *= moveSpeed;
	}
}


우선 유닛의 최대체력(maxHp)과 현재체력(curHp) 를 만들어줍니다.

int형으로 만들어도 괜찮지만, 나중에 혹시라도 데미지가 보정된다던지해서 소수값이 될수도 있으니 float으로 만들었습니다.

Start() 함수에선 현재체력을 최대체력으로 초기화 해줍니다.

attackMe() 함수가 호출되면 현재체력에서 데미지만큼 체력을 깍고, 체력이 0이하가 되면, 더이상 움직이지 못하게 isMoveAble을 false로 바꿔주고,
죽는 애니메이션을 플래이 해줍니다.
그리고 더이상 다른 타워들의 타겟이 되지 않도록 하기위해 GameManagerUnitList에서 제외시켜줍니다.

spr.AnimationCompleted 는 스프라이트 애니메이션이 끝나면 unitDestoryAniComplete 함수를 호출하도록 델리게이트를 연결하였습니다.
(델리게이터로 연결할때는 표시한것처럼 함수이지만 "()" 을 붙이지 않습니다.)
유닛이 파괴되는 애니메이션이 끝나면 실제로 유닛은 스테이지에서 사라지게 됩니다.


TowerBase.cs


using UnityEngine;
using System.Collections;

public class TowerBase : MonoBehaviour {
	public UnitBase target;
	public float attack = 10.0f;
	private string spriteAtkNameFormat = "tower_a_{0:d2}";
	private string spriteNormalNameFormat = "tower_n_{0:d2}";
	public float range = 100;
	public float shotDelay = 3.0f;
	private float reloadTime = 0;
	private tk2dSprite spr;
	// Use this for initialization
	void Start () {
		spr = this.GetComponentInChildren<tk2dSprite>();
		reloadTime = shotDelay;
	}
	
	// Update is called once per frame
	void Update () {
		checkRangeTarget();
		lookAtTarget();
		autoShot();
		attackSprAnim();
		updateSprite();
	}

	void checkRangeTarget()
	{
		if(target != null)
		{
			if(range < Vector2.Distance((Vector2)target.transform.localPosition, (Vector2)this.transform.localPosition))target = null;
		}
		if(target == null)
		{
			foreach(UnitBase ub in GameManager.instance.unitList)
			{
				if(range > Vector2.Distance((Vector2)ub.transform.localPosition, (Vector2)this.transform.localPosition))
				{
					target = ub;
					return;
				}
			}
		}
		
	}

	void lookAtTarget()
	{
		if(target == null)return;
		float anglePI = Mathf.Atan2(this.transform.localPosition.y - target.transform.localPosition.y, target.transform.localPosition.x - this.transform.localPosition.x) + Mathf.PI/2.0f;
		int angle = (int)((anglePI/Mathf.PI * 18.0f) + 36)%36;
		spriteN = 18 -Mathf.Abs(angle - 18);
		int spriteDir = angle<18?1:-1;
		spr.scale = new Vector3(spriteDir, 1,1);
		
	}

	void autoShot()
	{
		reloadTime -= Time.deltaTime;
		if(target == null || reloadTime>0)return;
		attackTarget();
		
	}
	bool isAtkSpr = false;
	bool isAttacking = false;
	float attackTimeCount = 0f;
	float attackDuration = .1f;
	float attackDelay = .13f;
	int attackRepeat = 3;
	int attackedCount = 0;

	void attackTarget()
	{
		if(target == null || isAttacking)return;
		attackTimeCount = 0;
		attackedCount = 0;
		isAtkSpr = true;
		isAttacking = true;
		reloadTime = shotDelay;

		target.attackMe(attack);
	}

	void attackSprAnim()
	{
		if(!isAttacking)return;
		attackTimeCount += Time.deltaTime;
		if(isAtkSpr)
		{
			if(attackDuration<attackTimeCount)isAtkSpr = false;
		}
		else if(attackDelay<attackTimeCount)
		{
			attackedCount++;
			if(attackedCount<attackRepeat)
			{
				isAtkSpr = true;
				attackTimeCount -= attackDelay;
			}
			else isAttacking = false;
		}
	}

	private int spriteN = 0;
	void updateSprite()
	{
		string spriteName = string.Format(isAtkSpr?spriteAtkNameFormat:spriteNormalNameFormat, spriteN);
		spr.spriteId = spr.GetSpriteIdByName(spriteName);
	}

}

Tower쪽은 훨씬 간단합니다.

변수 

추가된 변수는 attack 하나입니다. 

설명안해도 아시겠지만, 공격력입니다. public으로 되어있으니 인스팩터에서 손쉽게 변경 가능합니다.

함수 


새로운 함수가 추가되거나 하진않고, 기존의 attackTarget() 함수에 타겟의 'attackMe()' 함수를 호출하도록만 추가되었습니다.


이제 플래이버튼을 눌러보면 타워가 유닛들을 공격하고 체력이 없는 유닛들을 죽어서 없어지는걸 볼수 있습니다.

그런데, 생각보다 유닛들이 빨리빨리 죽지를 안네요.

간단히 프리팹에서 체력과, 공격력을 수정해서 난이도(?)를 조정해보도록 하겠습니다.

현재는 유닛의 체력이 100, 타워의 공격력이 10 이니, 10번 공격을 당해야 체력이 없어지겠네요.
(실제로는 11번 공격당해야 죽습니다. 이유는 코드에 체력이 0미만이 되었을때 죽었다고 처리하게 되어있습니다.)

한 3대정도 맞으면 죽을수 있게 체력은 80, 공격력은 30 으로 변경시킵니다.



이제 다시 플레이버튼을 눌러서 테스트 해보면 적당한 난이도가 된것 같네요.

사실...너무 쉬워졌네요;;
벨런스는 직접 맞춰보세요...


신고
Posted by andwhy

이번엔 지난시간에 이야기했던대로,

타워의 범위 안에 유닛이 있는지 검색하고, 자동으로 공격하는 스크립트를 만들도록 하겠습니다.


오늘 할일은 거의 스크립트 작업뿐이라 바로 완성된 스크립트부터 알려드리고 설명하도록 하겠습니다.


TowerBase.cs


using UnityEngine;
using System.Collections;

public class TowerBase : MonoBehaviour {
	public UnitBase target;
	private string spriteAtkNameFormat = "tower_a_{0:d2}";
	private string spriteNormalNameFormat = "tower_n_{0:d2}";
	public float range = 100;
	public float shotDelay = 3.0f;
	private float reloadTime = 0;
	private tk2dSprite spr;
	// Use this for initialization
	void Start () {
		spr = this.GetComponentInChildren<tk2dSprite>();
		reloadTime = shotDelay;
	}
	
	// Update is called once per frame
	void Update () {
		checkRangeTarget();
		lookAtTarget();
		autoShot();
		attackSprAnim();
		updateSprite();
	}

	void checkRangeTarget()
	{
		if(target != null)
		{
			if(range < Vector2.Distance((Vector2)target.transform.localPosition, (Vector2)this.transform.localPosition))target = null;
		}
		if(target == null)
		{
			foreach(UnitBase ub in GameManager.instance.unitList)
			{
				if(range > Vector2.Distance((Vector2)ub.transform.localPosition, (Vector2)this.transform.localPosition))
				{
					target = ub;
					return;
				}
			}
		}
		
	}

	void lookAtTarget()
	{
		if(target == null)return;
		float anglePI = Mathf.Atan2(this.transform.localPosition.y - target.transform.localPosition.y, target.transform.localPosition.x - this.transform.localPosition.x) + Mathf.PI/2.0f;
		int angle = (int)((anglePI/Mathf.PI * 18.0f) + 36)%36;
		spriteN = 18 -Mathf.Abs(angle - 18);
		int spriteDir = angle<18?1:-1;
		spr.scale = new Vector3(spriteDir, 1,1);
		
	}

	void autoShot()
	{
		reloadTime -= Time.deltaTime;
		if(target == null || reloadTime>0)return;
		attackTarget();
		
	}
	bool isAtkSpr = false;
	bool isAttacking = false;
	float attackTimeCount = 0f;
	float attackDuration = .1f;
	float attackDelay = .13f;
	int attackRepeat = 3;
	int attackedCount = 0;

	void attackTarget()
	{
		if(target == null || isAttacking)return;
		attackTimeCount = 0;
		attackedCount = 0;
		isAtkSpr = true;
		isAttacking = true;
		reloadTime = shotDelay;
	}

	void attackSprAnim()
	{
		if(!isAttacking)return;
		attackTimeCount += Time.deltaTime;
		if(isAtkSpr)
		{
			if(attackDuration<attackTimeCount)isAtkSpr = false;
		}
		else if(attackDelay<attackTimeCount)
		{
			attackedCount++;
			if(attackedCount<attackRepeat)
			{
				isAtkSpr = true;
				attackTimeCount -= attackDelay;
			}
			else isAttacking = false;
		}
	}

	private int spriteN = 0;
	void updateSprite()
	{
		string spriteName = string.Format(isAtkSpr?spriteAtkNameFormat:spriteNormalNameFormat, spriteN);
		spr.spriteId = spr.GetSpriteIdByName(spriteName);
	}

}


변수들 

위쪽에 추가된 변수들은 먼저 보면,


private string spriteAtkNameFormat = "tower_a_{0:d2}";
공격중인 타워 이미지입니다. 기본이미지와 동일한 각도에 불빛만 추가된 이미지입니다. 아래에서 노멀이미지와 섞어서 공격애니메이션을 만들겠습니다.

public float range = 100;
타워의 공격범위 입니다. public 으로 선언되어있으니 나중에 인스팩터에서 원하는 수치로 수정가능합니다.

public float shotDelay = 3.0f;
한번공격후 다음공격까지의 딜레이 시간입니다. 나중에 터렛 업그레이드에 공격속도증가 같은게 들어갈때 수정하면 좋겠네요.

private float reloadTime = 0;
재장전 시간입니다. 위의 shotDelay로 셋팅되고, 점점 줄어들다가 0 이되면 재 발사 가능상태가 됩니다.


함수 

우선 기본적인 Start와, Update에 추가되어있는것만 보겠습니다.

Start();
 재장전시간만 shotDelay로 초기화 시켜줍니다. 맵에 추가된뒤 바로공격하지 않고, shotDelay만큼 재장전시간이 지난후에 공격을 시작하게 됩니다.

Update();
기존보다 좀더 많은 함수들을 호축해줍니다.
순서대로 설명하면,
 범위안에 타겟이 있는지 검사 -> 타겟을 조준 -> 자동공격 -> 공격애니메이션 -> 스프라이트 갱신
이런 순서입니다.

추가된 함수들을 아래에서 하나씩 보도록 하겠습니다.



현재 설정된 타겟과, 타워사이의 거리가 공격범위 보다 많으면 타겟을 해제합니다.
그리고 GameManager의 unitList에서 범위내에 있는유닛이 있으면 그 유닛을 타겟으로 지정합니다.

transform.localPosition은 Vector3 형이지만, Vector3로 Distance를 구할경우 z 값에도 영향을 받기때문에 강제로 Vector2 로 캐스팅해서 계산합니다.
그리고, 두번째 if 에서 else를 쓰지 않은 이유는 첫번째검사에서 타겟이 해제될경우 바로 다른 타겟을 찾을수 있도록 하기위해서 else를 사용하지 않습니다.


autoShot은 reloadTime을 계속 줄여서 0 이하의 값이 되면 attackTarget()을 실행해줍니다.


타워가 한번 공격에 딱 한번씩 총알을 쏘고 끝낼수도 있지만, 좀더 퀄리티를 높이기 위해 3연발 기관총을 쏘는 타워로 만들어보겠습니다.

(공격딜레이나, 리핏 값을 수정해서 다양하게 사용가능합니다.)



변수들을 먼저 살펴보면,

isAtkSpr 가 '참' 일경우 공격하는 sprite를 표시하고 '거짓'일경우 일반스프라이트를 표시해줄기위한 bool값입니다.

isAttacking 은 현재 공격애니메이션인지를 판단할수있는 bool값입니다.

attackTimeCount는 공격애니메이션을 위해 공격시간을 카운트 하는 변수입니다.

attackDuration은 공격하는 스프라이트 를 표시해주는 시간으로, 0.1초간 공격하는 스프라이트를 보여주도록 설정했습니다.

attackDelay는 연발로 총을쏠때 다음발이 발사 될때까지의 딜레이입니다. 여기선 0.13 초로 되어있어서 공격후 0.03초뒤에 다음공격을 하게됩니다.

attackRepeat 은 연발 공격 횟수로 현재 3회로 되어있습니다.

attackedCount 는 한공격에 이미 몇발 공격했는지 count하는 변수입니다. 위의 attackRepeat횟수보다 많으면 공격을 중지합니다.


attackTarget() 함수는

위의 autoShot()이 재장전시간이 끝나면 호출하는 함수로 공격애니메이션에 필요한 값들을 초기화 하고 재장전시간을 셋팅합니다.


attackSprAnim() 함수를 보면
현재 공격화면인경우(isAtkSpr 가 'true') 공격애니메이션 시간이 공격화면 표시시간보다 길어지면 보통화면으로 바꾸고, 
보통화면일 경우,
연발 발사 딜레이시간보다 커지게 되면 연발 누적회수를 증가시키고,
연발 공격회수보다 작으면 다시 공격화면으로 스프라이트를 변경해줍니다.

공격횟수만큼 도달한다면 공격 애니메이션을 중단시킵니다.




마지막으로
updateSprite()는, 

지난시관과 크게 바뀐점은 없지만 위에 표시한대로 스프라이트 이름을 만들때,
isAtkSpr가 '참' 일경우 공격스프라이트를,
'거짓' 일경우 일반 스프라이트를 만들어주게 됩니다.


이제 하이라키 텝 안에, gameField에서 tower를 적당히 옮겨서 화면 중앙에 배치시키고,(임시로 붙였던 "unit_marine" 오브젝트는 삭제합니다.)
플레이 버튼을 누르고 "AddUnit"버튼으로 유닛을 추가해보면, 



화면처럼 마린을 겨냥해서 총을 쏴대는 타워를 볼수 있습니다.

다음 시간에는 맵상에 타워를 짓고, 마린들을 정말로 죽여보겠습니다.




신고
Posted by andwhy

드디어 타워짓기 입니다.

사실..길찾기 쪽이 너무 분량이 커서..좀 설명을 대충한거 같은데,

이제 타워쪽은 쉽고 재밌는부분이라 좀 설명을 자세히 적어볼까합니다.


1. 리소스 만들기

이전에 유닛을 만들었을때처럼 타워도 리소스를 만들어야 합니다.

tower.zip

첨부한 리소스를 열어보시면, 필드러너에서 뽑아온 타워 스프라이트가 있습니다.

원래는 타워종류 * 업그레이드 단계 * 공격모션 * 방향 별로 리소스가 있지만...
기본 타워의 노멀, 공경 스프라이트와 방향만가지고 만들도록 하겠습니다.

노멀 상태의 타워 스프라이트를 보면, 총 19 개의 이미지가 있습니다.
보시면 아시겠지만 오른쪽 회전 반향의 이미지들(17장)과 상, 하 이미지(2장)만 있습니다.

전체 이미지가 전부 있다면36장장이 되겠죠.

이 이미지들을 배치해보면 그림과 같습니다.


밝은쪽은 이미 있는 19장의 이미지이고, 왼쪽의 어두운 부분은 없는 이미지이죠.
왼쪽 어두운 이미지들은 사실 오른쪽이미지를 좌우 반전만 시킨 이미지들입니다.좌우대칭이되는상황이면 굳이 필요없는 이미지를 추가해서 리소스를 늘일 필요가 없기때문에 한쪽이미지로만 사용하는것입니다.
(사실 유닛을 만들때도, 좌우 대칭이기에 한쪽만 있으면 충분했지만, 아틀라스 공간도 남고 코딩으로 처리하기도 귀찮아서 반대쪽이미지도 만들어버렸습니다.)


이제, 유닛만들었을때와 마찬가지로 타워의 스프라이트 컬렉션은 만들겠습니다.

Create->tk2d->SpriteCollection을 누르고, 이름을 tower_sc 로 변경합니다.


만들어진 "tower_sc"를 선택하고, openEditor를 눌러서 뜬 에디터 창에 tower 이미지 리소스들을 드래그 해서 등록합니다.
타워의 원본 이미지가 너무 커서 그냥 쓰기엔 유닛이랑 차이가 많이 납니다.
창의 왼쪽에서 스프라이트들을 모두 선택하고,(shift를 누르고 선택하시면 편합니다.) 오른쪽의 scale 항목을 ".3"으로 변경하시고 꼭 apply버튼을 눌러주세요.

이후 commit버튼을 눌러 완료합니다.
(가장 좋은방법은 원본이미지를 1/3 로 스케일을 줄여서 작업하는방법입니다.)

이제 하이라키에 "gameField" 를 선택하고 tk2dSprite를 만들어줍니다.


만일,tower스프라이트가 아닌 엉뚱한 스프라이트가 추가 되었다면 인스팩터 창의 "Tk2dSprite"항목에서 Collection 과, Sprite를 다음과 같이 맞춰주면 됩니다.

이제 한번 플레이버튼을 눌러서 마린들과 크기를 한번 비교해보겠습니다.


마린과의 크기도 적당하고 맵상에 배치해도 딱 좋을것 같네요.

2. 타워 타겟팅

리소스는 준비되었으니, 유닛의 위치에 따라 타워의 총구를 돌려서 유닛을 타겟팅하겠습니다.

우선 방금 추가한 타워Sprite를 "tower"라는 이름으로 변경하고, TowerBase.cs 파일을 만들어서 그림과 같이 tower 오브젝트에 추가해줍니다.

TowerBase.cs



using UnityEngine;
using System.Collections;

public class TowerBase : MonoBehaviour {
	public UnitBase target;
	private string spriteNormalNameFormat = "tower_n_{0:d2}";
	private tk2dSprite spr;
	// Use this for initialization
	void Start () {
		spr = this.GetComponentInChildren<tk2dSprite>();
	}
	
	// Update is called once per frame
	void Update () {
		lookAtTarget();
		updateSprite();
	}
	private int spritN = 0;
	void updateSprite()
	{
		string spriteName = string.Format(spriteNormalNameFormat, spritN);
		spr.spriteId = spr.GetSpriteIdByName(spriteName);
	}
	void lookAtTarget()
	{
		if(target == null)return;
		float anglePI = Mathf.Atan2(this.transform.localPosition.y - target.transform.localPosition.y, target.transform.localPosition.x - this.transform.localPosition.x) + Mathf.PI/2.0f;
		int angle = (int)((anglePI/Mathf.PI * 18.0f) + 36)%36;
		spritN = 18 -Mathf.Abs(angle - 18);
		int spriteDir = angle<18?1:-1;
		spr.scale = new Vector3(spriteDir, 1,1);
		
	}
}


이렇게 코딩을 해주면, tower 인스펙터에 "target"이라는 항목이 생성됩니다.

"target"은 UnitBase 데이터형을 변수로 받고 있는데, 우선 테스트를 위해서 프로젝트 폴더의 "unit_marine" prefab을 하이라키의 gameField 아래로 드래그 해서 추가합니다.
(추가경로를 꼭 확인하세요. 방향을 체크할때 상대좌표를 사용하기때문에 경로가 다르면 엉뚱한곳을 보고 있을수 있습니다.)

추가된 unit_marine 을 tower의 target 에 드래그 해줍니다.


이제 플레이를 버튼을 누른뒤 scene 화면에서 tower나 추가된 marine의 좌표를 이동해보면, 항상 marine을 향해 총구를 겨냥하고 있게 됩니다.
(현재는 테스트 코드임으로 다른 유닛들이 추가되더라도 인스팩터에서 셋팅된 유닛만을 바라보게 됩니다.)




스크립트 코드를 보면서 하나씩 설명을 하도록 하겠습니다.

변수들

public UnitBase target;
타워가 바라보게될 타겟입니다.현재는 강제로 하나의 유닛을 잡고 있지만 다음엔 범위에서 벗어나면 새로운 타겟을 잡도록 변경할겁니다.

private string spriteNormalNameFormat = "tower_n_{0:d2}";
스프라이트 이름형식입니다. String.Format 함수를 통해서 스프라이트 이름을 만들어줍니다.
string.Format 함수를 처음보시는분들을 위해 설명드리면, {0} 이란곳에 2번째 파라미터를 집어 넣게 되고, 옵션으로 d2(2자리 정수값) 으로 들어갑니다.
예를들어서 2번째 파라미터로 1 이 들어갈경우 
string.Format("tower_n_{0:d2}", 1) 은  "tower_n_01" 이출력되고, string.Format("tower_n_{0:d2}", 19) 는 "tower_n_19" 가 출력됩니다.


함수들

void lookAtTarget()
자신과 타겟의위치를 비교하여, 알맞은 각도를 구하고, 알맞은 sprite의 번호를 구합니다.


만일 타겟이 없는경우 아무것도 하지 않습니다.
타겟이 있을경우 자신의 위치와, 타겟의 위치를 가지고 각도를 구합니다.
Mathf.Atan2(y,x)는 y변화량과, x 변화량으로 각도를 구해줍니다. 이때 나오는각도는 -Mathf.PI에서 Mathf.PI까지 (-3.141592....부터 3.141592..)까지입니다. 
라디안 값으로 구해지게 되는데, 아마 고등학교 수학시간에 나오는 범위인것같습니다.
간단히 이야기하면.. Mathf.PI(원주율) == 180도 라고 생각하시면 됩니다.
라디안 값을 평소에 자주쓰는 도단위 로 치환하기 위해선 180/Mathf.PI 를 곱해주면 됩니다.
이렇게 Atan2로 두 위치의 각도를 구하게 되면 타워기준으로 9시 방향이 0도가 되고 시계방향으로 값이 증가하여 180도, 시계반대방향으로 증가 하며 -180도가 됩니다.
그런데 우리가 가지고 있는 타워의 총구는 12시 방향이 0도로 되어있습니다. 그래서 시계방향으로 90도를 더 더해줍니다.( + Mathf.PI/2.0f )

이제 구해진 라디안 값을 10도 단위로 잘라줘야 합니다.
위에서 잠깐 이야기한대로 라디안 값에 180/Mathf.PI를 곱해주면 되는데, 우리가 필요한건 10도 로 나눈값이기에 18/Mathf.PI만 곱하겠습니다.
우리가 원하는 값은 0~35까지의 각도를 알고 싶은데, 지금은 음수 값이 섞여있어서, 원하는데로 0~35까지의 값만 나오도록 간단한 연산을 추가해줍니다.
(어떤 값에 36을 더해주고, % 36으로 36으로 나눈 나머지를 구하면 0~35까지의 값이 나오게 됩니다. 단, 어떤 값이 -36보단 커야합니다.)

여기까지 해주면 angle값은 0~35의 정수로 나오게 됩니다.
타워의 Sprite가 각도별고 0~35개 있다면 바로 계산이 끝나게 되지만, 리소스를 주리기 위해서 반대쪽방향은 Sprite가 없는상황입니다.
그래서 대칭되는 값을 찾아줘야 하는데, 0,1,2,3,4....17,18,17,16,15.... 이런식으로 18을 기준으로 다시 숫자가 줄어들면 될것 같습니다.

중학교 수학시간에 많이 했던 그래프로 그려보면...

이런식이 되면 딱 좋을것 같습니다.

공식으로 써보면...

y = 18 -|(x - 18)| 

이면 될것같네요.

이걸 유니티 코드로 바꾼게,
spriteN = 18 -Mathf.Abs(angle - 18);
입니다.

이제 spriteN은 0부터 증가하다가 18이상이되면 하나씩 감소하게 될껍니다.

마지막으로 angle이 18 이상일경우 xScale을 -1 로 바꿔서 좌우 대칭으로만 바꿔주면 됩니다.


void updateSprite()
각도에 따라서 sprite를 교체시켜줍니다.


string.Format과 위에서 구한 spriteN값으로 원하는 각도의 sprite이름을 만들어줍니다.
tk2d의 경우 스프라이트 네임으로 바로 스프라이트 교체가 안되서, GetSpriteIdBuName으로 아이디값을 알아낸뒤, spriteId를 셋팅해서 스프라이트를 교체시킵니다.


여기까지 타워가 유닛을 바라보게 만들어주었습니다.

그런데, 조금 마음에 안드는 부분이..
타워 주위를 원을그리며 유닛을 움직여보면 타워가 덜컹거리면서 움직여보입니다.
원인은 타워스프라이트의 중심점이 맞지 않아서 입니다.
오늘은 중심점만 맞추고, 끝내도록 하겠습니다.


3. 중심점 맞추기.

프로젝트 텝에 tower_sc를 선택하고, openEditor를 클릭해서 편집창을 엽니다.

에디터 창에서 오른쪽에 Sprites를 모두 선택후, Anchor 를 Custom으로 변경후 Apply버튼을 누릅니다.(공격 모션도 어차피 맞춰야하기때문에 이번에 한번에 다 하겠습니다.)

apply버튼을 누르면 모든 sprite들이 anchor가 Custom으로 변경되어있습니다.

이제부터 약간의 노가다 작업이 필요합니다.


화면 아랫쪽에 "Anchor"버튼을 누르면 다음과 같이 중심점을 변경할수 있는 화면이 나옵니다.

중심점을 마우스로 이동시켜 동그라미로 표시된 부분(이미지에 동그란 기준점이 보입니다.)으로 모든 스프라이트의 위치를 하나씩 이동시켜주고, commit 버튼을 눌러줍니다.


다시 플레이버튼을 누르고 회전하는 모습을 보면 이전처럼 덜컹거리지 않고 아주 자연스럽게 잘 돌아가보입니다.



이번 강좌는 여기서 마치도록 하겠습니다.

다음번엔 범위안에 유닛이 있는지 체크하고, 유닛을 자동으로 공격하는 방법까지 만들어보도록 하겠습니다.




신고
Posted by andwhy

오늘은 길찾기 마지막 시간으로 최적화 를 하도록 하겠습니다.

유니티가 느려지는데는 여러가지 원인이 있습니다. 

복잡한 계산을 하거나, 많은양의 데이터를 처리하거나, 많은수의 개체를 화면에 그리거나(DrawCall)
Debug.Log로 많은양의 로그를 남기거나, 많은양의 계산을 하는일 모두 포퍼먼스를 저하시키게 됩니다.

길찾기로직에서 유닛이 많아지면 많아질수록 길찾기 연산에 소요되는 시간이 늘어나게 됩니다.

이 계산시간을 줄이는 방법을 생각해보면,
한번 했던 계산을 다시 하지 않는다면.. 필요없는 계산을 줄일수 있습니다.

우리가 만든 길찾기 로직은 시작위치목표지점 인덱스 만을 가지고 길을 찾고 있습니다.

다시말해서, 같은 목표지점과, 같은 시작위치라면 몇번을 계산해도 똑같은 경로를 가지게 됩니다.

그래서, 한번 검색한 경로의 시작위치와, 목표지점인덱스을 저장해두고, 계산을 하기전에 이미 계산한 경로가 있으면 그경로를 가져다 쓰도록 만드는게 목표입니다.

여기에 덧붙여서 실시간으로 벽을 만들때, 벽을 만들수 있는지 없는지도 지난시간에 체크해서 판단하도록 하였습니다.

그래서 도달할수 있는지를 체크하는 체크모드를 만들어서, 체크모드를 통과할때만 새로운 경로데이터를 갱신하는 모듈까지 함께 만들도록하겠습니다.


PathFinder.cs 파일에 코드를 추가합니다.



우선 조금 복잡한 Dicrionary 형의 checkDic 과 pathDic을 넣어줍니다.

checkDic은 유효한 맵인지 검사하기 위한 임시 Dictionary 이고, 유효한 맵일경우 pathDic으로 복사, 유효하지 않을경우 패기 시켜버릴겁니다.

checkDic 과, pathDic을 데이터 형을 좀 살펴보면..

우선 목표지점 인덱스로 구분할수 있게, int 형 "key" 와 또다시 Dictionary 형의 "value" 가 있습니다.

value에 들어 있는 Dictionary 를 다시 뜯어보면, 시작 포인트좌표로 구분할수 있게, Point 형 "key"와 경로 포인트들을 가지고 있는 List<Point> 형이 "value"로 들어있습니다.

풀어서 반대로 이야기해보면,

탐색한 경로를 시작 포인트를 key로 가지는 그룹을 만들고, 이 그룹들을 도착점을 key로 가지는 그룹으로 만들어 놓는겁니다.

이러면 key값 이 있는지 2번만 검사하면 해당되는 경로가 있는지 없는지 알수 있습니다.



윗쪽 사각형은 체크모드인지 아닌지를 설정합니다. 위에서 말했듯이 checkDic은 임시로 저장해놓은 Dictionary 이기에 Clear시켜줍니다.

아랫쪽의 setPath()는 유효한 맵일경우 기존의 pathDic을 초기화 시키고 checkDic의 값 (유효화 검사를 하면서 검색한 결과값)을 pathDic으로 Copy합니다.


마지막으로 경로를 탐색할때, 기존처럼 검색을 하기전에 이미 검색한 Dictionary 중에 해당되는 조건이 있는지 검색해보고, 해당되는 경로가 있으면 미리 저장된 경로를 리턴시켜줍니다.

만일 해당조건과 맞는 경로가 없다면 기존 방법대로 검색하고, 검색결과를 Dictionary에 저장해줍니다.
이러면, 다음번에 동일한 조건으로 검색을 시도하면 방금 저장한 경로를 리턴시켜 주게 됩니다.


전체 소스를 다시 적어보면.. 아래와 같습니다.


PathFinder.cs


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

using Debug = UnityEngine.Debug;

public class PathFinder{
	private static PathFinder _instance = null;
	public static PathFinder instance
	{
		get{
			if(_instance == null)_instance = new PathFinder();
			return _instance;
		}
	}
	public PathFinder()
	{
		init();
	}

	private Dictionary<int,Dictionary<Point, List<Point>>> checkDic = new Dictionary<int, Dictionary<Point, List<Point>>>();
	private Dictionary<int,Dictionary<Point, List<Point>>> pathDic = new Dictionary<int, Dictionary<Point, List<Point>>>();

	private MapData mapData;
	private int[,] orgMap;
	private bool isSearching = false;
	private int searchCount = 0;
	private Point goalPoint;
	
	public void init()
	{
		mapData = new MapData();
		isSearching = false;
		searchCount =0;
	}
	
	public void setMapData(int[,] _map)
	{
		orgMap = _map;
		mapData.setMapData(orgMap);
	}
	private bool isCheckMode = false;
	public void setCheckMode(bool tf)
	{
		checkDic.Clear();
		isCheckMode = tf;
	}
	public void setPath()
	{
		pathDic.Clear();
		pathDic = new Dictionary<int, Dictionary<Point, List<Point>>>(checkDic);
		
	}
	public Point[] getPath(Point current, int targetIdx)
	{
		Dictionary<int, Dictionary<Point, List<Point>>> myDic;
		if(isCheckMode)myDic = checkDic;
		else myDic = pathDic;
		if(myDic.ContainsKey(targetIdx))
		{
			Dictionary<Point,List<Point>> dic = myDic[targetIdx];
			if(dic.ContainsKey(current))
			{
				return dic[current].ToArray();
			}
		}
		if(!checkInMapPoint(current)||!checkInMapIndex(targetIdx))return null;
		mapData.setMapData(orgMap);
		//Debug.Log("START SEARCHING.. ");
		isSearching = true;
		searchCount = 1;
		setPathCount(new List<Point>(){current}, targetIdx, 100);
		if(goalPoint == null) return null;
		List<Point> pList = findPath();
		if(pList == null) return null;
	
		Dictionary<Point, List<Point>> pDic;

		if(myDic.ContainsKey(targetIdx))
		{
			pDic = myDic[targetIdx];
		}
		else
		{
			pDic = new Dictionary<Point, List<Point>>();
			myDic.Add(targetIdx, pDic);
		}
		pDic.Add(current, pList);
		return pList.ToArray();
	}
	private void setPathCount(List<Point> pList, int targetIdx)
	{
		if(orgMap==null)return;
		setPathCount(pList, targetIdx, orgMap.GetLength(0) * orgMap.GetLength(1));
	}
	private void setPathCount(List<Point> pList, int targetIdx, int max)
	{
		if(!isSearching)return;
		List<Point> _List = new List<Point>();
		foreach(Point p in pList)
		{
			List<Point> retList = recordPath(p, targetIdx);
			if(retList!=null&&retList.Count>0){_List.AddRange(retList);}
		}
		if(_List.Count==0){return;}
		searchCount++;
		if(searchCount>=max){isSearching = false; return;}
		
		setPathCount(_List, targetIdx, max);
	}
	private List<Point> recordPath(Point p, int targetIdx)
	{
		if(!isSearching)return null;
		if(orgMap[p.x, p.y] == targetIdx)
		{
			isSearching = false;
			mapData.map[p.x,p.y] = searchCount;
			goalPoint = p;
			//Debug.Log("SEACHING COMPLETE!!!!   : "+searchCount);
			return null;
		}
		List<Point> rList = null;
		if(mapData.map[p.x,p.y] == 0)
		{
			mapData.map[p.x,p.y] = searchCount;
			rList = new List<Point>();
			getNeighbours(p, ref rList);
		}
		return rList;
	}
	private void getNeighbours(Point p, ref List<Point> rList)
	{
		if(p.x>0 && mapData.map[p.x - 1,p.y] == 0)rList.Add(new Point(p.x-1,p.y));
		if(p.y>0 && mapData.map[p.x,p.y - 1] == 0)rList.Add(new Point(p.x,p.y-1));
		if(p.x<orgMap.GetLength(0)-1 && mapData.map[p.x + 1, p.y] == 0)rList.Add(new Point(p.x+1,p.y));
		if(p.y<orgMap.GetLength(1)-1 && mapData.map[p.x, p.y + 1] == 0)rList.Add(new Point(p.x,p.y+1));
	}
	private List<Point> findPath()
	{
		List<Point> pathList = new List<Point>();
		Point temPoint = goalPoint;
		int _count = searchCount - 1;
		while(_count>0)
		{
			pathList.Insert(0,temPoint.clone());
			getPathPoint(ref temPoint, _count);
			_count --;
		}
		pathList.Insert(0,temPoint);
		if(isSearching)
		{
			isSearching = false;
			return null;
		}
		return pathList;
	}
	private void getPathPoint(ref Point p, int count)
	{
		if(p.y<mapData.map.GetLength(1)-1 && mapData.map[p.x, p.y+1] == count)
		{
			p.y += 1;
			return;
		}
		if(p.x<mapData.map.GetLength(0)-1 && mapData.map[p.x+1, p.y] == count)
		{
			p.x += 1;
			return;
		}
		if(p.y>0 && mapData.map[p.x, p.y-1] == count)
		{
			p.y -= 1;
			return;
		}
		if(p.x>0 && mapData.map[p.x-1, p.y] == count)
		{
			p.x -= 1;
			return;
		}
	}
	private bool checkInMapPoint(Point p)
	{
		if(orgMap==null)return false;
		if(p.x<0||p.y<0)return false;
		if(p.x >= orgMap.GetLength(0) ||p.y >= orgMap.GetLength(1))return false;
		return true;
	}
	private bool checkInMapIndex(int idx)
	{
		if(orgMap==null)return false;
		foreach(int _idx in orgMap)
		{
			if(idx == _idx)return true;
		}
		return false;
	}
}


public class MapData
{
	public int[,] map;
	public MapData()
	{
	}
	public void setMapData(int[,] _map)
	{
		int w = _map.GetLength(0);
		int h = _map.GetLength(1);
		map = new int[w, h];
		int x,y;
		
		for(x = 0; x < w; x++)
		{
			for(y = 0; y < h; y++)
			{
				if(_map[x,y] > 0 && _map[x,y] < 10)
					map[x,y] = -1;
				else
					map[x,y] = 0;
			}
		}
	}
}



마지막으로 GameManager.cs의 checkReachAble() 함수에 다음과 같이 체크모드를 활성, 비활성 화해주는 코드를 추가합니다.



이제 다시 플레이해보면 이전보다 훨씬 부드러워진 유닛들을 볼수 있습니다.




신고
Posted by andwhy

지난시간에 이어서 

오늘은 유닛들이 런타임에서 실시간으로 길을 찾아갈수 있도록 하겠습니다.


1. 실시간으로 벽만들기.

가장 처음으로 게임실행시 맵클릭으로, 길을 막았다 다시 만드는 스크립트를 작성하겠습니다.


GameManager.cs 에


public BgCellDisplayer bgGrid;
public Camera mainCam;

를 추가하고 인스팩터에 각각 BG와 camera를 드래그 합니다.




아래와 같이 Update(), 와 switchWall(Point p)을 추가 해줍니다.


이번에는 

BgCellDisplayer.cs파일에 refreshDisplay()함수를 추가해줍니다.

기존 코드와 겹치는 부분이 있어서,showBgCell()부분도 수정을 하였습니다.


using UnityEngine;
using System.Collections;

public class BgCellDisplayer : MonoBehaviour {
	public BgCell bgcell;

	private BgCell[,] bgCellArr;
	private GameManager gm;

	// Use this for initialization
	void Start () {
		gm = GameManager.instance;
		showBgCells();
	}
	private void showBgCells()
	{
		int _w = gm.wallMap.GetLength(0);
		int _h = gm.wallMap.GetLength(1);
		bgCellArr = new BgCell[_w,_h];
		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);
				bgCellArr[x,y] = bc;
			}
		}
		refreshDisplay();
	}
	public void refreshDisplay()
	{
		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 = bgCellArr[x,y];
				if(gm.wallMap[x,y] < 10 && gm.wallMap[x,y] != 0)
				{
					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);
				}
			}
		}
	}
}

이상태로 플레이를 해보면 화면상의 map을 클릭했을때, 길이 없어졌다 생겼다 하는것을 볼수 있습니다.

이상태에서 유닛을 추가하면, 새로 추가된 유닛들은 제대로 길을찾아 가지만, 기존에 이미 추가된 유닛들은 벽을 무시하고 원래 검색된 경로로 움직이는것을 볼수 있습니다.

또한, 시작점에서 도착지까지 갈수 있는길을 없애버리고, 유닛을 추가하면 에러가 발생합니다.

이번에는 GameManager.cs, UnitBase.cs파일을 수정해서, 시작점에서 도착점까지 갈수 없는 일이 생기지 않고, 화면상의 모든유닛들에게 맵이변경될경우 현재 위치부터 맵을 재 검색하도록 수정하겠습니다. 


UnitBase.cs 파일에서 간단하게 getPath() 함수만 void형에서 bool 형을 리턴하도록 변경하여, 
갈수 있는 길이 있으면 true, 갈수 있는길이 없을땐 false를 리턴하게 할겁니다.



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;
		init();
	}
	//=========================================
	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;
	public BgCellDisplayer bgGrid;
	public Camera mainCam;

	private void init()
	{
		unitList.Clear();

		initPathFinder();
	}
	void addUnit()
	{
		UnitBase unit = Instantiate(unit_marine) as UnitBase;
		unitList.Add(unit);
		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);

	}
	public void removeUnit(UnitBase ub)
	{
		unitList.Remove(ub);
	}
	void researchPathUnits()
	{
		foreach(UnitBase ub in unitList)
		{
			if(ub!=null)
			{
				ub.getPath();
			}
		}
	}
	Point getStartPoint()
	{
		List<Point> startPointList = getStartPointList();
		if(startPointList.Count == 0)
		{
			Debug.LogError("Not Found Start Position");
			return null;
		}
		int ranIdx = Random.Range(0,startPointList.Count);
		return startPointList[ranIdx];
	}
	List<Point> getStartPointList()
	{
		//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));
				}
			}
		}
		return startPointList;
	}
	public void initPathFinder()
	{
		PathFinder.instance.setMapData(wallMap);
	}
	void OnGUI()
	{
		if(GUI.Button( new Rect( 10, 10, 100, 40), "Add Unit"))
		{
			addUnit();
		}
	}
	void Update()
	{
		if(Input.GetMouseButtonDown(0))
		{
			Vector2 pos = Input.mousePosition;
			Vector3 mouseP = mainCam.ScreenToWorldPoint(pos) - unit_field.TransformPoint(Vector3.zero);
			Point myPos = new Point((int)(mouseP.x/cellSize), -(int)(mouseP.y/cellSize));
			switchWall(myPos);
		}
	}
	bool checkReachAble(Point p)
	{
		foreach(Point sp in getStartPointList())
		{
			if(PathFinder.instance.getPath(sp,100+wallMap[sp.x,sp.y]) == null)
			{
				Debug.Log("StartPoint Path NULL");
				return false;
			}
		}
		foreach(UnitBase unit in unitList)
		{
			if(!unit.getPath())
			{
				Debug.Log("Unit Path NULL");
				return false;
			}
		}
		
		return true;
	}
	void switchWall(Point p)
	{
		if(p.x<0||p.y<0 || p.x >= wallMap.GetLength(0) || p.y >= wallMap.GetLength(1))return;
		int prevIndex = wallMap[p.x, p.y];
		
		if(wallMap[p.x, p.y] == 0)wallMap[p.x, p.y] = 2;
		else if(wallMap[p.x, p.y] == 2)wallMap[p.x, p.y] = 0;
		
		
		if(checkReachAble(p))
		{
			bgGrid.refreshDisplay();
			researchPathUnits();
		}
		else 
		{
			wallMap[p.x, p.y] = prevIndex;
		}
	}
}


함수들

void addUnit()
유닛이 추가될때마다 
unitList에 추가 시킵니다.

public void removeUnit(UnitBase ub)
끝까지 도착한 유닛을 
unitList에서 제외시킵니다.

void researchPathUnits()
unitList에 등록된 Unit들에게 현재 위치에서 목표지점까지의 경로검색을 다시 명령합니다.

Point getStartPoint(),List<PointgetStartPointList()
기존엔 
getStartPoint()하나였지만, StartPoint들의 위치가 필요해서 나눴습니다.

bool checkReachAble()
현재 맵상태에서 모든시작점에서 목적지까지 갈수 있는 경로가 있는지 검사하고, 모든 유닛들이 목적지까지 갈수 있는 경로가 있는지 검사합니다.

void switchWall(Point p)
p좌표의 맵을 벽으로 바꿔도 되는지 검사하고, 땅은 벽으로, 벽은 땅으로 스위치 시킵니다.
(시작전부터 벽이였던 공간과 구분하기 위해 "2"로 셋팅합니다.)

이상태로 플레이를 해보면, 길을 없게 땅을 바꿀수도 없고, 이미 추가된 유닛들도 맵이 변형될때마다 경로를 다시 검색하는것을 볼수 있습니다.


하지만, 유닛들이 움직임이 어딘가 이상해서 수정하도록하겠습니다.


UnitBase.cs 의 update부분을 

에서 



처럼 수정합니다.

코드를 정리했을뿐 크게 수정된 부분은 없습니다.

추가된 부분은 표시된 부분처럼 더이상 갈 경로가 없을때( 목적지에 도착했을때..) GameManager의 UnitList 에서 자기 자신을 제외시킵니다.


가장 중요하게 바뀐부분은 getPath() 부분인데,


표시된 두 부분이 추가되었습니다.

윗쪽부분의 내용은 현재 유닛이 서있는땅이 벽일경우(유닛위에 벽이 만들어질경우) 경로를 재탐색하지 않고, 기존경로를 유지하도록 해주었고(탐색해봤자 갈곳이 없어서 에러가 발생합니다.)

아랫쪽부분의 경우 현재 유닛이 재탐색한 경로의 첫부분과, 기존경로의 첫부분이 같다면 가던길을 가게 하고, 다르다면 유닛을 되돌아갈수있도록 경로설정을 해주었습니다.


만일 아래 그림처럼 유닛의 기존경로가 (1, 1), (2, 1), (3, 1)..... 의 경로로 움직이는상황에서,
단기적으로 봤을때 유닛의 현재 시작점과 목적지는 (1, 1) 과 (1, 2) 가 됩니다.


새로운 경로를 탐색했을때 (1,1), (1, 0)... 의 순으로 가야한다면 유닛은 아래그림처럼 시작점과 도착점을 재설정하고, 먼저 시작점으로 돌아가야합니다.



하지만, 재탐색 했을떄 기존경로와 동일하게, (1, 1), (2, 1).... 순으로 경로가 지정된다면 시작점으로 돌아가는것이 오히려 어색하게 됩니다.

이런경우를 막기 위해 재탐색 전의 목적지와, 재탐색후의 목적지가 동일하면 목적지로 지정하고, 
목적지가 다르다면 시작점으로 돌아가서 새로운 목적지를 찾아가도록 변경하였습니다.


전체소스는 다음과 같습니다.



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 () {
	}
	private bool isMoveAble = false;
	// Update is called once per frame
	void Update () {
		if(!isMoveAble)return;
		float cellSize = GameManager.instance.cellSize;
		float _speed = cellSize * Time.deltaTime * moveSpeed;
		float rx = (nextPoint.x * cellSize + cellSize/2.0f) - this.transform.localPosition.x;
		float ry = (-nextPoint.y * cellSize - cellSize/2.0f) - this.transform.localPosition.y;
		float dx = _speed * makeNomal(rx);
		float dy = _speed * makeNomal(ry);
		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;
				GameManager.instance.removeUnit(this);
				Destroy(this.gameObject);
				return;
			}
			setNextPoint();
		}
	}
	int makeNomal(float f)
	{
		float k = 0.1f;
		if(f>k)return 1;
		else if(f<-k)return -1;
		else return 0;
	}
	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 bool getPath()
	{
		float cellSize = GameManager.instance.cellSize;
		startPoint = new Point((int)(this.transform.localPosition.x/cellSize),-(int)(this.transform.localPosition.y/cellSize) );
		int wallMapIndex = GameManager.instance.wallMap[startPoint.x,startPoint.y];
		if(wallMapIndex > 0 && wallMapIndex < 10)return true;

		Point[] pArr = PathFinder.instance.getPath(startPoint, 111);
		if(pArr == null){Debug.Log("NULL path");return false;}
		pathArr = pArr;

		if(nextPoint != null && pathArr.Length > 1 && nextPoint.isEqual(pathArr[1]))pathIndex = 1;
		else pathIndex = 0;
		nextPoint = pathArr[pathIndex];
		showCharDir();
		return true;
	}
	private void setNextPoint()
	{
		startPoint = nextPoint;
		pathIndex++;
		nextPoint = pathArr[pathIndex];
		showCharDir();
	}
	private void showCharDir()
	{
		
		float cellSize = GameManager.instance.cellSize;
		float nx = (nextPoint.x * cellSize + cellSize/2.0f);
		float ny = (-nextPoint.y * cellSize - cellSize/2.0f);
		if(this.transform.localPosition.x<nx)
			spr.Play (charAniStr[(int)CHAR_ANI.RIGHT]);
		else if(this.transform.localPosition.x>nx)
			spr.Play (charAniStr[(int)CHAR_ANI.LEFT]);
		else if(this.transform.localPosition.y<ny)
			spr.Play (charAniStr[(int)CHAR_ANI.UP]);
		else if(this.transform.localPosition.y>ny)
			spr.Play (charAniStr[(int)CHAR_ANI.DOWN]);
		spr.ClipFps *= moveSpeed;
	}
}


유닛의 방향을 정해주는 함수도, 약간 수정되었는데, 기존은 포인트 정보만으로 방향을 정했지만, 이번엔 실제 유닛의 x,y 좌표를 가지고 방향을 잡아주도록 변경하였습니다.


여기까지 하고 플레이를 해보면,특별한 문제 없이 원하는대로 모든유닛이 길을 잘찾아가는걸 볼수 있습니다.



헌데, 유닛이 많아지면 많아질수록 계산이 느려지는걸 알수 있습니다.

다음시간엔 같은계산을 최소화 시켜서 길찾기를 최적화 해보도록 하겠습니다.



신고
Posted by andwhy


오늘은 드디어 길찾기를 만들겠습니다.

타워디펜스 게임을 만들면서 가장 중요하다고 생각한게 길찾기였습니다.
뭐...미리 길이 정해져있는 게임이라면 굳이 길찾기를 안하고, 웨이포인트를 몇몇개 찍어두면 훨씬 쉽게 만들수 있지만,
우리가 만들것은 지형이 계속 변하게 되니까, 길찾기 가 가장중요한 부분이 됩니다.
(아...그냥 웨이포인트방식으로 만들고 다음에 다시만들껄...ㅜ.-)

우선 길찾기 알고리즘중 가장 잘알려져있고 최적화된것은 A*(에이스타) 인데, 뭐...좀 복잡해서 설명하고, 이해하기 쉬운방식으로 넣겠습니다.
차후에 A* 를 적용하셔도 상관은 없습니다.
(우리가 하려는 방식도 비슷하게 계산하는 방법이 있긴한데, 잘 살펴보진 않았네요..A스타랑 비교해보면 A스타가 약 2배정도 계산이 적어서 더 빠르긴 한것 같네요..)

우리가 구현할 pathFinder에 대한 설명은 http://andwhy.tistory.com/76 에서 확인하시고,

바로 스크립트로 넘어가겠습니다.

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;
		}
		public string ToString()
		{
			return "Point[x: "+x +", y: "+y+"]";
		}
		public Point clone()
		{
			return new common.Point(x, y);
		}
		public bool isEqual(Point p)
		{
			return (p.x == x) && (p.y == y);
		}
		public static bool operator ==(Point p1,Point p2){
			if (object.ReferenceEquals(null, p1))return object.ReferenceEquals(null, p2);
			if (object.ReferenceEquals(null, p2))return object.ReferenceEquals(null, p1);
			return p1.isEqual(p2); 
		}
		public static bool operator !=(Point p1,Point p2){
			if (object.ReferenceEquals(null, p1))return !object.ReferenceEquals(null, p2);
			if (object.ReferenceEquals(null, p2))return !object.ReferenceEquals(null, p1);
			return !p1.isEqual(p2); 
		}
		public override bool Equals(object p1)
		{
			return this.isEqual((Point) p1);
		}
		public override int GetHashCode()
		{
			return ToString().GetHashCode();
		}
	}
	
}

이전 Point.cs파일에 몇가지 함수를 추가합니다.

함수들

public string ToString()

디버깅시 좌표를 좀더 편하게 보기위해 만든 함수.

public Point clone()

자신과 동일한 Point객체를 하나 더 만듭니다.

public bool isEqual(Point p)
public static bool operator ==(Point p1,Point p2)
public static bool operator !=(Point p1,Point p2)

if문 등에서 Point끼리 비교하기 위한 함수들

public override bool Equals(object p1)
public override int GetHashCode()

Dictionary 에서 키값으로 Point를 쓰기 위해서 구현 해줍니다.


PathFinder.cs

using System.Collections;
using System.Collections.Generic;
using common;
using Debug = UnityEngine.Debug;
public class PathFinder{
	private static PathFinder _instance = null;
	public static PathFinder instance
	{
		get{
			if(_instance == null)_instance = new PathFinder();
			return _instance;
		}
	}
	public PathFinder()
	{
		init();
	}

	private MapData mapData;
	private int[,] orgMap;
	private bool isSearching = false;
	private int searchCount = 0;
	private Point goalPoint;

	public void init()
	{
		mapData = new MapData();
		isSearching = false;
		searchCount =0;
	}

	public void setMapData(int[,] _map)
	{
		orgMap = _map;
		mapData.setMapData(orgMap);
	}
	public Point[] getPath(Point current, int targetIdx)
	{
		if(!checkInMapPoint(current)||!checkInMapIndex(targetIdx))return null;
		mapData.setMapData(orgMap);
		Debug.Log("START SEARCHING.. ");
		isSearching = true;
		searchCount = 1;
		setPathCount(new List<Point>(){current}, targetIdx, 100);
		if(goalPoint == null) return null;
		List<Point> pList = findPath();
		if(pList == null) return null;
		return pList.ToArray();
	}
	private void setPathCount(List<Point> pList, int targetIdx)
	{
		if(orgMap==null)return;
		setPathCount(pList, targetIdx, orgMap.GetLength(0) * orgMap.GetLength(1));
	}
	private void setPathCount(List<Point> pList, int targetIdx, int max)
	{
		if(!isSearching)return;
		List<Point> _List = new List<Point>();
		foreach(Point p in pList)
		{
			List<Point> retList = recordPath(p, targetIdx);
			if(retList!=null&&retList.Count>0){_List.AddRange(retList);}
		}
		if(_List.Count==0){Debug.Log ("SEACHING FAIL!!!!");return;}
		searchCount++;
		if(searchCount>=max){isSearching = false; return;}
		
		setPathCount(_List, targetIdx, max);
	}
	private List<Point> recordPath(Point p, int targetIdx)
	{
		if(!isSearching)return null;
		if(orgMap[p.x, p.y] == targetIdx)
		{
			isSearching = false;
			mapData.map[p.x,p.y] = searchCount;
			goalPoint = p;
			Debug.Log("SEACHING COMPLETE!!!!   : "+searchCount);
			return null;
		}
		List<Point> rList = null;
		if(mapData.map[p.x,p.y] == 0)
		{
			mapData.map[p.x,p.y] = searchCount;
			rList = new List<Point>();
			getNeighbours(p, ref rList);
		}
		return rList;
	}
	private void getNeighbours(Point p, ref List<Point> rList)
	{
		if(p.x>0 && mapData.map[p.x - 1,p.y] == 0)rList.Add(new Point(p.x-1,p.y));
		if(p.y>0 && mapData.map[p.x,p.y - 1] == 0)rList.Add(new Point(p.x,p.y-1));
		if(p.x<orgMap.GetLength(0)-1 && mapData.map[p.x + 1, p.y] == 0)rList.Add(new Point(p.x+1,p.y));
		if(p.y<orgMap.GetLength(1)-1 && mapData.map[p.x, p.y + 1] == 0)rList.Add(new Point(p.x,p.y+1));
	}
	private List<Point> findPath()
	{
		List<Point> pathList = new List<Point>();
		Point temPoint = goalPoint;
		int _count = searchCount - 1;
		while(_count>0)
		{
			pathList.Insert(0,temPoint.clone());
			getPathPoint(ref temPoint, _count);
			_count --;
		}
		pathList.Insert(0,temPoint);
		if(isSearching)
		{
			isSearching = false;
			return null;
		}
		return pathList;
	}
	private void getPathPoint(ref Point p, int count)
	{
		if(p.y<mapData.map.GetLength(1)-1 && mapData.map[p.x, p.y+1] == count)
		{
			p.y += 1;
			return;
		}
		if(p.x<mapData.map.GetLength(0)-1 && mapData.map[p.x+1, p.y] == count)
		{
			p.x += 1;
			return;
		}
		if(p.y>0 && mapData.map[p.x, p.y-1] == count)
		{
			p.y -= 1;
			return;
		}
		if(p.x>0 && mapData.map[p.x-1, p.y] == count)
		{
			p.x -= 1;
			return;
		}
	}
	private bool checkInMapPoint(Point p)
	{
		if(orgMap==null)return false;
		if(p.x<0||p.y<0)return false;
		if(p.x >= orgMap.GetLength(0) ||p.y >= orgMap.GetLength(1))return false;
		return true;
	}
	private bool checkInMapIndex(int idx)
	{
		if(orgMap==null)return false;
		foreach(int _idx in orgMap)
		{
			if(idx == _idx)return true;
		}
		return false;
	}
}


public class MapData
{
	public int[,] map;
	public MapData()
	{
	}
	public void setMapData(int[,] _map)
	{
		int w = _map.GetLength(0);
		int h = _map.GetLength(1);
		map = new int[w, h];
		int x,y;

		for(x = 0; x < w; x++)
		{
			for(y = 0; y < h; y++)
			{
				if(_map[x,y] > 0 && _map[x,y] < 10)
					map[x,y] = -1;
				else
					map[x,y] = 0;
			}
		}
	}
}



라이브러리..

using Debug = UnityEngine.Debug; 유니티 엔진을 임포트 하셔도 상관없습니다.


싱글톤

    private static PathFinder _instance = null;
    public static PathFinder instance
    {
        get{
            if(_instance == null)_instance = new PathFinder();
            return _instance;
        }
    }
    public PathFinder()
    {
        init();
    }

이전에 봤던 싱글톤과 비슷하지만, 이쪽이 정석입니다. 객체가 없으면 새로운 객체를 만들어서 리턴해줍니다.

변수들

private MapData mapData;     패스파인더에 사용할 맵노드 클레스입니다.     PathFinder.cs파일 아래쪽에 보시면 MapData라는 클래스를 하나 더 선언했는데, C#에선 한파일에 여러 클래스파일을 선언해    서 사용할수 있습니다.     MapData가 하는 역할은 갈수 없는 셀은 -1로 표시하고, 갈수 있는길은 0으로 표시해둡니다.     길을 찾을때 값이 0인 셀들만 검색해서 찾고 찾은셀은 seachCount 값을 넣어줄겁니다.
    private int[,] orgMap;
원본 맵입니다. 보통 길찾기라면 굳이 필요없지만, 이번엔 목표지점이 정해져있지않고, 특정인덱스를 찾아가기 때문에 필요합니다.
    
private Point goalPoint; 목표지점 포인트 좌표 입니다. 특정인덱스를 맵에서 검색해서 목표지점을 정해줍니다.


함수들

public void setMapData(int[,] _map)

gameManager  에 있는 wallMap을 MapData 로 변형해줍니다.


public Point[] getPath(Point current, int targetIdx)

외부에서 호출하는 길찾기 매쏘드 입니다. 현재 위치와 목표 인덱스를 가지고 길을 찾습니다. 현재위치가 맵상에 없는 위치이거나, 목표인덱스가 맵상에서 찾을수 없을경우 굳이 검색을 하지 않아도 되기때문에 첫번째 줄에서 검사하여, return 시켜버립니다. 길찾기를 한번할때마다 mapData를 초기화 시켜주고, 시작점부터 카운터를 올려가며 탐색을 시킵니다.("setPathCount()" 재귀함수 사용). 탐색이 끝나고, 목표지점까지 갈수있는길이 있는지 없는지 확인후에, 목표지점에서 시작점까지 오는 최단거리를 검색합니다.("findPath()" 재귀함수 사용).

private void setPathCount(List<Point> pList, int targetIdx, int max)

시작점부터 목표점까지 갈수있는 길을 표시합니다. pList는 현재 탐색중인 셀들의 리스트 입니다.처음엔 1개로 시작하지만, 상하좌우 의 인접한 셀들이 계속 추가 됩니다. pList의 Point좌표들을 돌면서 각각 셀에 seachCount를 표시하고(시작점의 seachCount는 1입니다.), 표시된 셀의 인접한 셀들을 다음검색목록(_List)에 추가합니다. 만일 pList의 Point들을 다 돌고도 인접한 셀이 더이상없다면, 더이상은 갈길이 없으므로 막혀있는길입니다. 최대치를 넣어서, 최대치 이상으로 검색될경우 길이 없다고 처리해버립니다.(없어도 되지만 재귀함수가 무한으로 도는걸방지합니다.) 다음검색목록이 비어있지않고, 목적지까지 길을 찾지도 못했다면 다음검색목록을 가지고 다시 setPathCount함수를 실행합니다. (길을 찾거나, 길이 없을때까지 이함수를 계속 실행하게 됩니다.만일 조건인 잘못되어서 무한으로 함수가 돌게 된다면 유니티를 강제 종료하는것이외엔 멈출수 없습니다.

private List<Point> recordPath(Point p, int targetIdx)

위의 setPathCount 에서 pList의 Point 좌표에 seachCount를 실제로 넣어주고, 주변 셀들을 리턴해주는 함수입니다. 만일 현재 Point p가, targetIdx와 일치 한다면 goalPoint에 현재 p를 넣고, 더이상 탐색을 하지 않도록 다음 검색할 셀이 없다고 리턴해줍니다. isSearching 값도 false로 변경해서 더이상은 검색하지 않도록 합니다. 만일 p가 targetIdx가 아니라면 seachCount를 현재 포인트좌표에 넣어주고, 상,하,좌,우 셀들중 값이 '0'인 셀(한번도 검색하지 않은 셀)들만 찾아서 리턴해줍니다.

private List<Point> findPath()     도착점부터 시작점까지 가는길을 구하는 함수입니다.     도착점에 표시된 searchCount 값을 시작으로, point의 상,하,좌,우 의 셀을 검색해서 현재 count값보다 하나 작은 카운트    값을 찾아서("getPathPoint()") 이동합니다.     이동후엔 카운트값이 1 이될때까지 계속 검색("getPathPoint()" 재귀함수 사용)합니다.     카운트가 1인지점은 시작지점이기때문에 1까지 검색이 완료되었으면 검색한 Point값을 차례대로 리턴해줍니다.


마지막 findPath()에서 리턴된 List<Point> 값이 최종으로 찾아진 길입니다. 만일 null이 나오게 된다면, 갈수 없는길이란 이야기죠.


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;
		init();
	}
	//=========================================
	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();

		initPathFinder();
	}
	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);

	}
	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];
	}
	public void initPathFinder()
	{
		PathFinder.instance.setMapData(wallMap);
	}
	void OnGUI()
	{
		if(GUI.Button( new Rect( 10, 10, 100, 40), "Add Unit"))
		{
			addUnit();
		}
	}
}

약간의 스크립트가 추가되었습니다.

-Awake 시에 init()함수 호출.
-PathFinder초기화해주는 initPathFinder 함수 추가.
-init함수에 initPathFinder()함수 호출.

public void initPathFinder() 에대해선 위에서 만든 PathFinder초기화가 전부라서 따로 설명은 하지 않습니다.



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 () {
	}
	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()
	{
		Point[] pArr = PathFinder.instance.getPath(startPoint, 111);
		if(pArr == null){Debug.Log("NULL path");return;}
		pathArr = pArr;
		//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;
	}
}


UnitBase 클래스에선 getPath()함수 부분만 임시로 넣었던 Path에서 PathFinder를 통해 실제 검색한 길을 찾아오도록만 수정되었습니다.

public void getPath()
{
Point[] pArr = PathFinder.instance.getPath(startPoint, 111);
if(pArr == null){Debug.Log("NULL path");return;}
pathArr = pArr;
pathIndex = 0;
}

이대로 Play버튼을 눌러서 확인해보면 유닛들이 녹색 시작점에서 파란색 시작점까지 잘 걸어가는걸 볼수 있습니다.
벽이있을때 어떻게 돌아가는지 확인하고 싶다면, GameManager.cs의 wallMap에 중앙쯤을 적당히 1로 변경해서 벽을 만들어서 플래이 해보시면 됩니다.(길이 없다면 에러가 날껍니다.)

이상으로 길찾기 1편을 마치고, 다음번엔 런타임에서 맵을 변경하고, 바로 새로운 경로를 찾아갈수 있도록 구현해보겠습니다.



신고
Posted by andwhy

원래 그냥 바로 pathFinder스크립트 만들고 바로 진행하려고 했는데,

생각보다 pathFinder부분이 커서 좀 나누어서 설명하도록하겠습니다.

그래서 이론수업부분이 하나 더 생겼네요.

부록 정도로 생각해주세요.


1. 캐릭터의 움직임.

우리는 w*h 만큼의 4방향 타일맵방식으로 맵을 만들고 상, 하, 좌, 우 로만 움직일수 있도록 할껍니다.

대각선이나 벽을 뛰어넘거나, 통과하는일은 없습니다.

그러니, 길찾기를 할때도 4방향만 신경쓰면 됩니다.


2. 시작점과 목적지


임의로 8*8 짜리 타일맵에 그림과 같이 시작점과 목적지를 표시합니다.

위치는 어디여도 상관없지만, 설명을 위해 맘대로 표시했습니다.


2. 갈수 없는타일 표시


검정색 부분은 벽입니다. 지나갈수 없는 땅이죠..


3. 시작점부터 탐색

제가 길을 찾는방법은 시작점부터 도착점까지 갈수 있는 모든 방향을 하나씩 표시할껍니다.

SearchCount라는 값을 하나 가지고, 한칸씩 움직일때마다 표시를 하는거죠.

우선 S 에서 시작하면 움직일수 있는 방향은 4방향 상,하, 좌,우 모두 움직일수 있습니다.

그럼 아래 그림처럼 상,하,좌,우 칸에 "1" 이라고 표시합니다.


4. 계속 탐색(재귀함수 사용)

이제 "1" 이라고 표시된부분을 기점으로, 3번에서 했던일을 똑같이 합니다.

단 이번엔 "1" 이 아닌 "2" 로 표시하는거죠.

그리고 이미 숫자가 표시된곳은 다시 표지 하지 않습니다.

모두 표시하면 다시 숫자만 하나 올려서 4번의 행동을 또 똑같이 반복합니다.

더이상 표시할수 있는곳이 없거나, "G"에 도착할때까지....계~~속

이번맵에서 탐색을 계속 하면 아래처럼 8번째 탐색에서 목적지까지 검색하게 됩니다.


이 과정으로 S에서 G까지 상하좌우 로만 움직일경우 가장 빨리 도착할수있는 턴은 8 번인걸 알수 있겠네요.


5. 경로 찾기

이제 저 많이 표시된 숫자중에 S->G까지 돌아가지 않고 한번에 가는경로만 찾으면 됩니다.

1,2,3,4,5,6,7,8 순으로 S부터 G까지 따라가면 되겠지만, 중간에 끊길수도 있고, 돌아갈수도 있습니다.

저는 반대로 G부터 S까지 8에서 1씩줄여서 가는방법으로 찾아가도록 하겠습니다.

(이러면 최소한 끊길일은 없습니다.)

지금 표시한 경로 이외에,  위,위,위,위,우,우,우,우 로 가는 루트도 있지만, 둘다 잘못된 길이거나, 최소경로보다 돌아가는건 아니니, 크게 상관 없겠네요.

저 경로는 마지막에 count를 줄여가며 경로를 찾을때 주위타일 검색 순서에따라 틀려지게 됩니다.


이론수업은 이상입니다.

상당히 복잡할꺼같았는데, 그림이랑 같이 설명하니까 금방 끝나네요.




저작자 표시
신고
Posted by andwhy


강의글을 다시 검토하는중 몇가지 빠진 부분이 있어서 추가했습니다.
혹시 이전에 글을 보시고 따라하시는중에 에러가 난다면,
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


첫시간으로, 타워를 배치할수 있는 cell을 표시하는 grid를 만들고, 적 유닛들이 지나갈수 있는 길인지 

아닌지를 판단하는 wallMap 데이터를 만들겠습니다.


bgUI.zip




1.유니티 화면 셋팅

먼저 유니티 화면을 셋팅하도록 합니다.

저는 NGUI,와 TK2d를 사용할예정으로 두가지 플러그인은 설치합니다.(NGUI는 없어도 됩니다.)



하이라키에서 기본 camera를 지우고,  create -> tk2d -> camera 로 새로운 tk2d카메라를 올려놓습니다.'



카메라 인스팩터에서 pexel per meter값은 1로 변경하겠습니다.



Game 화면의 해상도도 IPhone5 해상도인 1136 * 640 으로 셋팅해주세요.




2. 리소스 준비

배경이미지는 아직 없어서, 빈공간에 체크무늬의 그리드를 만들고 배경을대신하겠습니다.

체크무늬에 사용할 스프라이트를 준비하겠습니다.

프로젝트 텝에, resource 폴더를 만들고 리소스들을 임포트 합니다.

SpriteCollections라는 폴더도 만들고, Create메뉴 -> tk2d -> spriteCollection을 선택해서

 새로운 스프라이트 컬랙션을 만듭니다.

만들어진 프리팹을 선택하면 인스팩터 창에 Open Editor버튼이 보이는데, 눌러줍니다.

새로 뜬 에디터 창에 resource폴더의 whiteDot을 드래그해서 올려놓으면 추가 됩니다.

setting 에서 pixels per meter값도 1로 변경해줍니다.



설정이 끝났으면 commit버튼을 꼭 눌러주세요.


처음에 만들었던 tk2d카메라의 밑에 새로운 GameObject를 만들고, 이름을 stage로 변경합니다.

stage의 위치를 0,0,100 정도로 마추세요.

(2D게임을 주로 만들때 카메라의 기준되는 위치를 맞춰주기 위해서 주로 빈 게임오브젝트를 하나 만들어서 기준을 잡습니다.)


이제 stage아래에 create -> tk2d -> Sliced Sprite를 선택해서 새 sprite를 생성하면 화면 중앙에 하얀색 네모가 보입니다.

저는 셀하나의 크기를 40*40 으로 하겠습니다.

( 세로로 최대 15개정도 타일을 배치할수 있게 하면 40 픽셀정도가 적당하겠네요.)


새로 생성된 sprite의 디멘션값을 40,40 으로 변경해주고 아래 anchor 도 좌측상단으로 정하겠습니다.

이름도 "BgCell"로변경합니다.

stage의 좌표도, 지금 생성한 sprite가 게임 화면의 좌측상단에 표시될수 있도록 

x:-578, y: 320, z: 100 으로 설정합니다. 아래와 같은 화면처럼 보이면 됩니다.




프로젝트 텝에서 BgCell.cs 이란 C#스크립트를 하나생성하고 위에서 만든 BgCell 게임오브젝트에 드래그 해둡니다.


하이라키에 있는 BgCell을 선택해서 프로젝트쪽으로 드래그 해놓으면 자동으로 prefab이 만들어지면서 하이라키의 BgCell도 파란색 글씨로 변경됩니다.



하이라키의 BgCell은 지우고, BG라는 이름의 새 게임오브젝트를 만듭니다.(좌표는 0,0,0)

이번엔 BgCell 들을 화면에 뿌려줄 BgCellDisplayer,cs라는 스크립트를 만들고,

방금 만든 BG 게임오브젝트에 드래그해 놓습니다.


3. 스크립트

이번엔 스크립트를 한번 보도록 하겠습니다.

BgCell.cs

using UnityEngine;
using System.Collections;

public class BgCell : MonoBehaviour {

	public Color white = Color.white;
	public Color black = Color.black;
	
	public Color startCol = Color.green;
	public Color goalCol = Color.blue;

	private tk2dSlicedSprite _spr;
	private tk2dSlicedSprite spr
	{
		get
		{
			if(_spr == null)_spr = this.GetComponentInChildren<tk2dSlicedSprite>();
			return _spr;
		}
	}
	public bool isVisible
	{
		get
		{
			return this.gameObject.activeSelf;
		}
		set
		{
			this.gameObject.SetActive(value);
		}
	}
	public void setBlack(bool isBlack)
	{
		if(isBlack)spr.color = black;
		else spr.color = white;
	}
	public void setStart()
	{
		spr.color = startCol;
	}
	public void setGoal()
	{
		spr.color = goalCol;
	}
}

BgCell.cs는 특별한 내용없이, 화면에 오브젝트를 보여줄지말지, 색상을 black으로 할지 white로 할지 정도만 정해주는 역할입니다.

추가로 유닛의 시작 블럭과, 도착지 블럭도 표시하겠습니다.

시작위치는 10~110 사이의 값으로 하고, 110이상의 값은 도착지로 정하겠습니다.

BgCellDisplayer.cs

using UnityEngine;
using System.Collections;

public class BgCellDisplayer : MonoBehaviour {
	public BgCell bgcell;
	private 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}};

	// Use this for initialization
	void Start () {
		showBgCells();
	}
	public void showBgCells()
	{
		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++)
			{
				BgCell bc = Instantiate (bgcell) as BgCell;
				bc.transform.parent = this.transform;
				bc.transform.localPosition = new Vector3( 40 * x, -40 * y, 0);
				if(wallMap[x,y] == 1)
				{
					bc.isVisible = false;
				}
				else if(wallMap[x,y] >= 10 && wallMap[x,y] <= 100)
				{
					bc.isVisible = true;
					bc.setStart();
				}
				else if(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 의 역할은 화면에 BgCell 오브젝트를 복제해서 적절한 위치에 배치시키고, wallMap데이터와 비교해서, 화면에 보여줄지 말지, 색상은 white,black중 뭘로할지 정해주고 시작점은 녹색, 도착점은 파란색으로 칠해주는게 끝입니다.

한줄씩 살펴보면,

public BgCell bgcell;

화면에 표시할 체크무늬 cell의 원본프리팹을 정해줍니다. public 으로 되어있으니 인스팩터에 표시되어 바로 연결해줄수 있습니다.

private int[,] wallMap

맵의 초기 지형을 2차 배열로 만들어놨습니다. 지형지물에 따라 터렛을 설치할수 없거나 벽으로 셋팅되게 됩니다.

이번맵에선 양쪽 중간부분의 3칸만 시작점, 목적지로 지정하기때문에 양쪽의 나머지를 벽으로 지정해놨습니다.

BgCell bc = Instantiate (bgcell) as BgCell; bc.transform.parent = this.transform; bc.transform.localPosition = new Vector3( 40 * x, -40 * y, 0);

bgcell의 원본 프리팹을 화면에 추가하고, 위치를 맞춰줍니다.

BgCellDisplayer.cs는 위에서 만든 BG 오브젝트에 넣기때문에 새로 생성된 BgCell오브젝트는 BG 밑의 자식 오브젝트로 옮겨집니다.




4. 컴포넌트 연결하기 및 세부조정.

이제 만든 컴포넌트(스크립트)를 연결하고, 조정을 하겠습니다.


하이라키의 BG오브젝트를 클릭하고, 인스펙터 창에서 Bgcell에 프로젝트 텝의 BgCell 프리팹을 드래그해서 연결합니다.



UnityEditor의 Play버튼을 누르면 화면상에 아래와 같이 표시됩니다.

BG오브젝트의 trasform좌표를 정당히 조정하여 화면 가운데에 오도록 배치하세요.

Play버튼을 끄게 되면 셋팅값이 다시 돌아오기 때문에, 좌표를 적어두었다가 stop상태에서 다시한번 셋팅하세요.


체크무늬의 색상이나 투명도를 변경하고 싶다면 프로젝트의 BgCell 프리팹을 선택하여 인스팩터에서 생상값을 조정하고 Play하면 바로 적용됩니다.




최종적으로 아래와 같은 화면이 나오면 오늘 챕터는 완성입니다.







ps. 설명을 하면서 개발하려니까 상당히 오래걸리고 힘드네요..

잘이해 안되거나 오류나는부분있으면 바로바로 알려주세요.






신고
Posted by andwhy

안녕하세요.

andwhy입니다.


이번엔 전부터 만들어보고 싶었던 타워디펜스 게임을 만들어보려고 합니다.

타워디펜스도 상당히 많은 종류가 있는데, 그중에서 "fieldRunners" 라는 게임과 유사하게 만들어볼려고 합니다.


http://www.youtube.com/watch?v=fz_ckX1Ecoc


이번 게임을 만들어보면, 

기본적인 스프라이트 애니메이션과, UI는 물론이고,

오브젝트간의 거리계산, 방향계산,

타일맵 게임과, 재규 함수를 통한 간단한 패스파인더도 공부 할수 있겠네요.


제작툴은 unity3d 4.3.4 pro버전(4.0 이상이면 free여도 큰문제 없을겁니다.)과,

tk2d(ver 2.4.0), NGUI(ver 3.6.6) 플러그인을 사용합니다. (UI파트가 없어져서 NGUI사용안합니다.)


플러그인을 사용하지 않고, 비슷한 기능를 만들수 있으면 큰문제 없습니다.


계획은...

1. 타일맵GRID만들기 및, wallMap만들기.

2. 기본 유닛 만들기및 배치

3. 더미타워 배치 및 패스파인더.

4. 타워 타겟팅

5. 타워 공격및 유닛Destroy

6. 최적화를 위한 pooling.

7. UI 작업

6.타워 업그레이드

7. WaveData

이런순서로 진행될것같습니다.




신고
Posted by andwhy
이전버튼 1 이전버튼

블로그 이미지
andwhy 개인 블로그.
andwhy

공지사항

Yesterday23
Today7
Total108,449

달력

 « |  » 2017.12
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            

글 보관함