Skip to content

Tutorial_Step4_JAVA

TeamSAIDA edited this page Nov 13, 2018 · 2 revisions

(초급 단계) 유닛 생산 및 건물 건설하기

TutorialLevel4Bot 프로젝트를 통해, 유닛 생산 및 건물 건설하기를 해보겠습니다.

개발 범위 정의

  1. 유닛 생산하기

유닛을 생산하는 것은 상대적으로 단순한 작업입니다. 유닛을 생산할 수 있는 건물에 대해 유닛 생산을 명령하면 되니까요.

자원이 일정 수치 이상 확보되면 유닛을 생산하는 코드를 작성해보겠습니다.

  1. 건물 건설하기

건물을 건설하기 위해서는 건설할 건물의 종류를 먼저 선정하고, 건물을 건설할 일꾼 유닛을 선정한 후, 건물을 건설할 위치를 결정해야 합니다.

사실 건물 건설은 상당히 복잡한 작업으로서, 건물이 건설될 위치는 다른 유닛이 없이 텅 빈 공간이어야 하고, Zerg 종족의 경우 Creep 이라고 하는 것이 바닥에 깔려있어야 하는등 훨씬 많은 제약조건이 존재합니다. 또한 Terran 종족의 경우 건물을 건설하다가 일꾼 유닛이 죽으면 건물 건설이 중지되어서, 새로운 일꾼 유닛을 보내 속행 할 수 있다는 특징이 있습니다.

지금은 이런 복잡한 요소들은 생각하지않고, 간단하게 자원이 일정 수치 이상 확보되면 Start Location 근처에 빈공간을 찾아 건물 건설을 하는 코드를 작성해보겠습니다.

개발 환경 설정

  1. Eclipse 를 실행시킵니다

  2. Package Explorer 에서 TutorialLevel4Bot 프로젝트를 선택합니다

  3. 메뉴 -> Run -> Run Configurations... -> 왼쪽 트리에서 Java Application 밑에 TutorialLevel4Bot 을 선택합니다

  4. 오른쪽 Arguments 탭 -> Working Directory : Others 에 C:\StarCraft 를 입력합니다

파일 목록

파일명 설명
Main.java 봇 프로그램의 시작 지점입니다. MyBotModule을 실행시킵니다
MyBotModule.java 스타크래프트 게임과 Connection 을 맺고, 스타크래프트 게임에서 발생하는 각 이벤트를 GameCommander 가 처리하도록 전달합니다. 스타크래프트 게임이 종료되면 봇 프로그램을 종료시킵니다
GameCommander.java 각각의 게임 이벤트가 적절하게 처리되도록 해당 객체에게 이벤트를 전달하는 관리자 역할을 합니다
InformationManager.java 게임 상황정보 중 일부를 자체 자료구조 및 변수들에 저장하고 업데이트합니다
WorkerManager.java 일꾼 유닛들의 상태를 관리하고 컨트롤합니다
BuildManager.java 빌드(건물 건설 / 유닛 생산 등) 를 실행합니다

1. 유닛 생산하기

먼저 용어 정의를 하고 시작하겠습니다. 스타크래프트 및 BWAPI 에서 원래 유닛(Unit)은 건물 유닛, 지상 유닛, 공중 유닛을 통칭하는 단어입니다. 그러나 일반적으로 사람들은 유닛이라고 하면 건물 유닛을 제외한, 지상 유닛(Ground Unit) 및 공중 유닛 (Air Unit) 을 의미하는 것으로 생각합니다. 그래서, 앞으로는 '모든 유닛' 과 '유닛' 을 다음과 같이 정의하겠습니다.

  • 유닛 = 지상 유닛 + 공중 유닛

  • 모든 유닛 = 지상 유닛 + 공중 유닛 + 건물 유닛

유닛 생산은 어떤 건물(producerUnit)으로 하여금 어떤 유닛타입(targetUnitType)을 생산하도록 명령하면 됩니다. 유닛 생산 명령은 Protoss 및 Terran 종족의 경우 train 명령을 사용하며, Zerg 종족의 경우 morph 명령을 사용합니다.

	if (MyBotModule.Broodwar.self().getRace() != Races.Zerg) {
		producer.train(targetUnitType);
	}
	else {
		producer.morph(targetUnitType);
	}

참고로, Zerg_Lurker, Protoss_Archon, Zerg_Infested_Terran 등은 기존 유닛으로하여금 Morph 를 하게 하거나, 합체기술을 사용해서 생산하거나, train 명령을 사용하는등 유닛 생산 방법이 다른데 지금은 다루지 않겠습니다.

GameCommander 가 BuildManager 에게 update()를 지시하고, BuildManager 가 자원이 충분히 있으면 일꾼 유닛을 생산하도록 하는 코드는 다음과 같습니다.

아래 코드에서 ResourceDepot 이라는 용어가 나오는데, 이것은 해당 종족의 사령부에 해당하는 건물로서, Protoss 종족인 경우 Nexus, Terran 종족인 경우 Command Center, Zerg 종족인 경우 Hatchery / Lair / Hive 를 의미합니다.

ResourceDepot 건물을 갖고 canMake 함수를 써서 일꾼 유닛을 생산 가능한 상황인지 체크하고, ResourceDepot 건물이 이미 일꾼 유닛을 생산하고 있는것은 아닌지 체크하는 부분을 눈여겨 보시기 바랍니다.

(GameCommander.java)

public void onFrame(){
	...
	// 유닛 훈련 및 건물 건설을 한다
	BuildManager.Instance().update();
}
(BuildManager.java)

public void update() {
	...
	buildWorkerUnits();
}

private void buildWorkerUnits()
{
	// 자원이 50이상 있으면 일꾼 유닛을 훈련한다
	if (MyBotModule.Broodwar.self().minerals() >= 50) {
		buildWorkerUnit();
	}
}

private void buildWorkerUnit()
{
	Unit producer = null;

	UnitType targetUnitType = UnitType.None;

	if (MyBotModule.Broodwar.self().getRace() == Race.Protoss) {
		targetUnitType = UnitType.Protoss_Probe;
	}
	else if (MyBotModule.Broodwar.self().getRace() == Race.Terran) {
		targetUnitType = UnitType.Terran_SCV;
	}
	else if (MyBotModule.Broodwar.self().getRace() == Race.Zerg) {
		targetUnitType = UnitType.Zerg_Drone;
	}
		
	// ResourceDepot 건물이 일꾼 유닛을 생산 가능한 상태이면 생산을 명령한다
	for (Unit unit : MyBotModule.Broodwar.self().getUnits())
	{
		if (unit.getType().isResourceDepot() ){

			if (MyBotModule.Broodwar.canMake(targetUnitType, unit) && unit.isTraining() == false && unit.isMorphing() == false) {

				producer = unit;

				if (MyBotModule.Broodwar.self().getRace() != Race.Zerg) {
					producer.train(targetUnitType);
				}
				else {
					producer.morph(targetUnitType);
				}
				break;
			}
		}
	}
}

여기까지만 해서 빌드 및 게임 실행을 해보세요. 일꾼 유닛이 생산되는 것을 볼 수 있으실 것입니다.

전투 유닛 생산은 일꾼 유닛 생산과 크게 다르지 않습니다. 일꾼은 ResourceDepot 건물에서 생산가능하지만 전투 유닛은 유닛 타입에 따라 각각 생산되는 건물이 다르다는 것만 차이날 뿐이죠.

이번에는 유닛 생산 하는 함수를 조금 더 일반화 시켜서 만들어보겠습니다. whatBuilds 함수를 사용해서 생산하려는 유닛 타입에 대해 생산 건물의 종류를 알아내는 부분을 눈여겨 보시기 바랍니다.

(BuildManager.java)

public void update()
{	
	...

	buildCombatUnits();

	buildWorkerUnits();
}

private void buildCombatUnits()
{
	UnitType targetUnitType = UnitType.None;

	// 자원이 100이상 있으면 먼저 전투 유닛을 훈련한다
	if (MyBotModule.Broodwar.self().minerals() >= 100) {
		if (MyBotModule.Broodwar.self().getRace() == Race.Protoss) {
			targetUnitType = UnitType.Protoss_Zealot;
		}
		else if (MyBotModule.Broodwar.self().getRace() == Race.Terran) {
			targetUnitType = UnitType.Terran_Marine;
		}
		else if (MyBotModule.Broodwar.self().getRace() == Race.Zerg) {
			targetUnitType = UnitType.Zerg_Zergling;
		}

		trainUnit(targetUnitType);
	}
}

void BuildManager::trainUnit(BWAPI::UnitType targetUnitType)
{
	BWAPI::Unit producer = nullptr;
	BWAPI::UnitType producerUnitType = targetUnitType.whatBuilds().first;

	// targetUnitType을 생산 가능한 상태가 되면 생산을 명령한다
	for (auto & unit : BWAPI::Broodwar->self()->getUnits())
	{
		if (unit->getType() == producerUnitType) {
			if (BWAPI::Broodwar->canMake(targetUnitType, unit) 
			&& unit->isTraining() == false 
			&& unit->isMorphing() == false) {

				producer = unit;

				if (BWAPI::Broodwar->self()->getRace() != BWAPI::Races::Zerg) {
					producer->train(targetUnitType);
				}
				else {
					producer->morph(targetUnitType);
				}
				break;
			}
		}
	}
}

여기까지만 해서 빌드 및 게임 실행을 해보세요. 봇 프로그램에 건물을 건설하는 코드가 아직 없기 때문에 건물 건설은 직접 해줘야 합니다. 전투 유닛을 생산하는 건물을 만들어놓으면 전투 유닛이 생산되는 것을 볼 수 있으실 것입니다.

2. 건물 건설하기

이제 건물을 건설해보겠습니다. 건물을 건설하기 위해서는 건설할 건물의 종류를 먼저 선정하고, 건물을 건설할 유닛을 선정한 후, 건물을 건설할 위치를 결정해야 합니다.

건물 건설은 일반적으로 일꾼 유닛에게 build 명령을 사용하면 됩니다.

	producer.build(targetUnitType, targetPosition);

참고로, Zerg 종족의 Lair / Hive, Sunken Colony / Spore Colony, Great Spire 건물은 Hatchery, Creep Colony, Spire 건물에 대해 morph 명령을 사용해서 건설하는데, 지금은 다루지 않겠습니다.

다음은 건물을 건설하는 함수를 구현한 코드입니다. Protoss 종족의 경우 Pylon 건물을 지은 후 Gateway 건물을 짓고, Terran 종족의 경우 Supply Depot 건물을 지은 후 Barracks 건물을 짓고, Zerg 종족의 경우 Spawing Pool 건물을 지을 것입니다.

건물이 건설될 위치를 정하는 방법으로는 간단하게 아군의 Start Location 주위 타일에 대해 canBuildHere 함수를 호출해서 건물을 지을수 있는지 체크하는 방식으로 구현하였습니다.

(BuildManager.java)

public void update()
{
	constructBuildings();

	buildCombatUnits();

	buildWorkerUnits();
}

private void constructBuildings()
{
	UnitType targetUnitType = UnitType.None;

	// 자원이 200이상 있으면 전투유닛 생산 건물을 건설 한다
	if (MyBotModule.Broodwar.self().minerals() >= 200) {
		if (MyBotModule.Broodwar.self().getRace() == Race.Protoss) {
			targetUnitType = UnitType.Protoss_Gateway;
		}
		else if (MyBotModule.Broodwar.self().getRace() == Race.Terran) {
			targetUnitType = UnitType.Terran_Barracks;
		}
		else if (MyBotModule.Broodwar.self().getRace() == Race.Zerg) {
			targetUnitType = UnitType.Zerg_Spawning_Pool;
		}
		constructBuilding(targetUnitType);
	}

	// 자원이 100이상 있고, 서플라이가 부족해지면 SupplyProvider 에 해당하는 유닛을 만든다
	// 서플라이 숫자는 스타크래프트 게임에서 표시되는 숫자의 2배로 계산해야한다
	if (MyBotModule.Broodwar.self().minerals() >= 100
		&& MyBotModule.Broodwar.self().supplyUsed() + 6 > MyBotModule.Broodwar.self().supplyTotal()) {
		if (MyBotModule.Broodwar.self().getRace() == Race.Protoss) {
			targetUnitType = UnitType.Protoss_Pylon;
			constructBuilding(targetUnitType);
		}
		else if (MyBotModule.Broodwar.self().getRace() == Race.Terran) {
			targetUnitType = UnitType.Terran_Supply_Depot;
			constructBuilding(targetUnitType);
		}
		else if (MyBotModule.Broodwar.self().getRace() == Race.Zerg) {
			targetUnitType = UnitType.Zerg_Overlord;
			trainUnit(targetUnitType);
		}
	}
}

private void constructBuilding(UnitType targetBuildingType)
{
	// 일꾼 중 미네랄을 운반하고 있지 않은 일꾼 하나를 producer로 선정한다
	Unit producer = null;
	UnitType producerUnitType = targetBuildingType.whatBuilds().first;

	for (Unit unit : MyBotModule.Broodwar.self().getUnits())
	{
		if (unit.getType() == producerUnitType) {
			if (MyBotModule.Broodwar.canMake(targetBuildingType, unit)
				&& unit.isCompleted() 
				&& unit.isCarryingMinerals() == false
				&& unit.isConstructing() == false) {

				producer = unit;
				break;
			}
		}
	}

	if (producer == null) {
		return;
	}

	// 건물을 건설할 위치를 Start Location 근처에서 찾는다
	// 처음에는 Start Location 반경 4타일에 대해 찾아보고, 
	// 다음에는 Start Location 반경 8타일에 대해 찾아보는 식으로 범위를 넓혀나간다
	TilePosition seedPosition = InformationManager.Instance().mainBaseLocations.get(InformationManager.Instance().selfPlayer).getTilePosition();
	TilePosition desiredPosition = TilePosition.None;
	int maxRange = 32;
	boolean constructionPlaceFound = false;

	for (int range = 4; range <= maxRange; range *= 2) {
		for (int i = seedPosition.getX() - range; i < seedPosition.getX() + range; i++) {
			for (int j = seedPosition.getY() - range; j < seedPosition.getY() + range; j++) {
				desiredPosition = new TilePosition(i,j);
				if (MyBotModule.Broodwar.canBuildHere(desiredPosition, targetBuildingType, producer, true))	{
					constructionPlaceFound = true;
					break;
				}
			}
			if (constructionPlaceFound) break;
		}
		if (constructionPlaceFound) break;
	}

	if (constructionPlaceFound == true && desiredPosition != TilePosition.None) {
		producer.build(targetBuildingType, desiredPosition);
	}
}

문제점

빌드 및 게임 실행을 해보면, 그럭저럭 건물 건설 및 유닛 훈련이 이루어지는 것을 볼 수 있습니다. 그러나, 여기에는 많은 문제가 있습니다.

첫째로, 무엇을 언제 건설 할지에 대해 전략적으로 판단해서 명령을 내리는 부분과 건설 명령을 실행하는 부분이 BuildManager 클래스에 함께 있는데, 전략적인 판단 부분은 별도로 분리가 필요해보입니다.

둘째로, 건물 건설 명령을 내린 후 건설 명령이 실행될 때까지의 시간 갭 사이에 다시 건설 명령이 내려져서 여러명의 일꾼 유닛이 건설 명령을 실행하려고 하는 현상이 나타납니다. 건설 명령들의 진행 상황을 관리하는 자료구조가 필요한 것이죠.

셋째로, 건물 건설 위치를 결정하는 로직에 많은 개선이 필요합니다. 건물 건설을 Start Location 주위에 할지, 길목 주위에 할지, 다른 Base Location 에 할지 정하는 것이 매우 중요하기 때문입니다. 또한, 건물이 다닥다닥 붙어서 건설되다보면 건물에서 생산된 유닛이 갇혀서 빠져나오지 못하는 경우도 생길 것입니다.

넷째로, 위에서는 단순히 일꾼 유닛, 기본 전투 유닛, 서플라이 건물, 기본 전투 유닛 생산 건물 등 간단한 유닛들만 만들기 때문에 별 문제가 없지만, 고급 유닛을 만들기 위해서는 다양한 선행 요소들이 갖춰졌는지 체크하는 것이 필요합니다. 만약 선행 요소들이 갖춰져있지 않은 상태에서 고급 유닛 생산 명령을 내리거나, 적군에 의해 일부 건물이 파괴되어 선행 요소들이 갖춰지지 않게 되었다면, Dead Lock (무기한 교착 상태) 에 빠지게 되기 때문에 이에 대해 해결하는 로직이 필요합니다.

사람 플레이어라면 별로 어려운 문제가 아니지만, 봇 프로그램에 대해서는 꼼꼼하게 체크해서 로직을 작성해야 하는 부분입니다.

그럼 이제 개발의 시작점이 되는 BasicBot 을 살펴봅시다.

Home

Clone this wiki locally