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