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

티스토리 툴바