Unity3D 2015.11.23 14:39

android 연동을 하면 간단하긴하지만..

따로 플러그인으로 배포하기 귀찮고, 그냥 유니티에서만 C#코드를 가지고 언어 셋팅값을 가져오게 만들어봤습니다.

AndroidJavaClass Locale = new AndroidJavaClass"java.util.Locale" );
AndroidJavaObject DefaultLang = 
Locale.CallStatic<AndroidJavaObject>("getDefault");
Debug.Log("lang " + 
DefaultLang.Call<string>("toString"));


정말 저거하나만 필요할때는 사용할만한데, 다른거랑 같이 쓸꺼면 플러그인 만들어서 쓰는게 깔끔하겠군요.

신고
posted by andwhy
Unity3D/TowerDefence 2014.10.12 23:13


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

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

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

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

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

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

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

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

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


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

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


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



감사합니다.







신고
posted by andwhy
Unity3D/TowerDefence 2014.10.11 18:36

마지막 시간이네요.

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

점수 보여주기와, 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
Unity3D/TowerDefence 2014.10.05 23:21
앞으로 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
Unity3D/TowerDefence 2014.09.28 21:21

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

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

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

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


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
Unity3D/TowerDefence 2014.09.23 00:59

실수로 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
Unity3D/TowerDefence 2014.09.19 23:51

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


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

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

이런경우 이외에 유닛이 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
Unity3D/TowerDefence 2014.09.17 22:27

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


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
Unity3D/TowerDefence 2014.09.16 02:23

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

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


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


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
Unity3D/TowerDefence 2014.09.13 11:24

드디어 타워짓기 입니다.

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

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


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

티스토리 툴바