스타봇/스타봇 강의 및 개발

스타크래프트 봇 예제 프로토스편

오잎 클로버 2021. 12. 23. 21:00
728x90

지난 번에는 튜토리얼을 진행했습니다.

 

이제부터는 각 종족별 예제 봇 하나씩 개발할 것입니다.

 

먼저 어떤 빌드를 어떻게 사용할 것인지 정합니다.

저같은 경우에는 최소한 봇은 이길 수 있도록 하기 위해

2게이트 질럿 러쉬를 하되, 만일 막힐 경우 생존한 질럿들을 다시 귀환한 뒤

드라군 및 질럿 다시 모아 공격할 수 있도록 하였습니다.

 

먼저 Main, ExampleBot, ExampleUtil 이 3개의 클래스로 개발을 하였습니다.

Main은 아래와 같이 ExampleBot에 있는 run 메소드만 사용하도록 하였습니다.

public static void main(String[] args) {
    try {
        new ExampleBot().run();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

printStackTrace은 혹여나 오류가 날 경우 어디서 오류가 발생했는 지 파악하기 위해 하였습니다.

일단 먼저 기존에 개발하였던 Tools를 활용하여 ExampleUtil를 개발하였습니다.

 

제일 가까운 유닛을 반환하는 메소드인 getClosestUnitTo

해당 종류의 Type이 총 몇 기인지 반환하는 countUnitType

현재 총 인구수 계산을 하는 getTotalSupply

그리고 건물을 건설하는 buildBuildings

게임 특정 프레임 마다 작동하기 위해 조건형 메소드인 delay

적군 위치가 어디인지 파악하는 checkEnemyBase

마지막으로 move, attack을 보다 효율적으로 하기 위해 개발한 메소드들이 있습니다.

먼저 buildBuildings, delay, checkEnemyBase 이 3가지 메소드를 다룰 것이고, 그외 메소드들을 더 자세히 확인하고 싶으신 분들은 아래 링크 확인해주세요.

링크

public static void buildBuildings(UnitType type) {
    Unit builder = ExampleBot.workers.get(0);

    if (builder.isConstructing()
            || (ExampleBot.scoutWorker != null && builder.getID() == ExampleBot.scoutWorker.getID())) {
        return;
    }

    TilePosition desiredPos = ExampleBot.BroodWar.self().getStartLocation();
    TilePosition buildPos = ExampleBot.BroodWar.getBuildLocation(type, desiredPos, 64, false);

    if (type.requiresPsi()) {
        for (int i = 64; i < 128; i += 8) {
            if (ExampleBot.BroodWar.hasPowerPrecise(buildPos.toPosition())) {
                break;
            }
            buildPos = ExampleBot.BroodWar.getBuildLocation(type, desiredPos, i, false);
        }
    }

    builder.build(type, buildPos);
}

프로토스는 주변에 전력이 있어야하기 때문에

위와같이 type이 전력이 요구된다면 해당 위치를 계속 찾도록 합니다.

public static boolean delay(int delay) {
    return ExampleBot.BroodWar.getFrameCount() % delay == 0;
}

delay는 말그대로 특정 딜레이마다만 코드를 실행하고자 만든 메소드입니다.

public static boolean checkEnemyBase(TilePosition tilePosition) {
    for (Unit u : ExampleBot.BroodWar.getUnitsInRadius(tilePosition.toPosition(), 256)) {
        if (u.getPlayer() == ExampleBot.BroodWar.enemy()) {
            return true;
        }
    }
    return false;
}

해당 위치 256(시야: 8) 안에 존재하는 모든 유닛들 중 하나라도 적군 것이라면 true를 반환합니다.

 

이를 활용하여 ExampleBot으로 돌아가

최대한 효율적으로 많은 기능을 소화내고자

private BWClient bwClient;
public static Game BroodWar;
public static BWMap map;

public static final ArrayList<Unit> workers = new ArrayList<>();
public static final ArrayList<Unit> soldiers = new ArrayList<>();
public static Unit scoutWorker = null;

public static Unit gasWorker1;
public static Unit gasWorker2;

public static boolean StartAttack = false;
public static boolean ZealotRush = false;
public static int zealotRushEnd = 6;
public static int deadZealots = 0;
public static Position waitingPosition = Position.None;
public static Position enemyBase = Position.Unknown;
public static boolean hasBrokeCenter = false; // 적군 depot 파괴 여부
public static ArrayList<TilePosition> bases = new ArrayList<>();

public static final ArrayList<Unit> leftBuildings = new ArrayList<>();

위와 같이 변수들을 선언하여

onStart 메소드에

BroodWar = bwClient.getGame();
BWEM bwem = new BWEM(BroodWar);
System.out.println("전부 초기화 준비");
bwem.initialize();
map = bwem.getMap();
map.enableAutomaticPathAnalysis();
waitingPosition = map.getArea(BroodWar.self().getStartLocation()).getChokePoints().get(0).getCenter().toPosition();
System.out.println("게임 시작");
bases.addAll(BroodWar.getStartLocations());

위와 같이 변수들에 초기값을 넣고

@Override
public void onUnitComplete(Unit unit) {
    if (unit.getPlayer() != BroodWar.self()) {
        return;
    }

    if (unit.getType().isWorker()) {
        workers.add(unit);
    }

    if (unit.getType() == UnitType.Protoss_Zealot
            || unit.getType() == UnitType.Protoss_Dragoon) {
        soldiers.add(unit);
    }
}

본인의 유닛들 생성을 soldiers에 넣어 보다 편하게 공격 및 후퇴를 하기 위해 추가합니다.

@Override
public void onUnitDestroy(Unit unit) {
    if (unit.getPlayer() != BroodWar.self()) {

        if (unit.getPlayer() == BroodWar.enemy()) {
            if (unit.getType() == BroodWar.enemy().getRace().getResourceDepot()) {
                hasBrokeCenter = true;
                System.out.println("파괴");
            }
            if (unit.getType().isBuilding()) {
                leftBuildings.remove(unit);
            }
        }

        return;
    }

    if (unit.getType().isWorker()) {
        if (scoutWorker != null && scoutWorker.getID() == unit.getID()) {
            scoutWorker = null;
        }
        if (gasWorker1 != null && gasWorker1.getID() == unit.getID()) {
            gasWorker1 = null;
        }
        if (gasWorker2 != null && gasWorker2.getID() == unit.getID()) {
            gasWorker2 = null;
        }

        workers.remove(unit);

    }

    if (unit.getType() == UnitType.Protoss_Zealot
            || unit.getType() == UnitType.Protoss_Dragoon) {
        soldiers.remove(unit);

        if (unit.getType() == UnitType.Protoss_Zealot && ZealotRush) {
            deadZealots++;
        }
    }
}

그리고 정찰병이 죽었는 지, 그리고 죽은 질럿 수, 그리고 상대 본진 건물 파괴여부 등을 저장합니다.

@Override
public void onUnitShow(Unit unit) {
    if (unit.getPlayer() != BroodWar.enemy()) {
        return;
    }

    if (!unit.getType().isBuilding()) {
        return;
    }

    leftBuildings.add(unit);
}

남은 상대 건물들을 저장하여 모든 건물들을 다 파괴할 수 있도록 돕습니다.

 

미네랄 채취는 기존 코드는 튜토리얼 방식을 그대로 인용하였습니다.

가스 채취를 새롭게 추가하였습니다.

public void gatherGas(Unit worker) {

    if (BroodWar.self().completedUnitCount(UnitType.Protoss_Assimilator) > 1) {
        return;
    }

    if (worker != null && worker.exists()) {
        if (!worker.isIdle()) {
            return;
        }

        Unit closestGas = ExampleUtil.getClosestUnitTo(worker, BroodWar.getStaticGeysers());
        if (closestGas != null) {
            worker.gather(closestGas);
        }
    }
}

프로브 생산 역시 기존 방식을 사용하되, 최대 10기의 프로브만 생산하도록 하였습니다.

 

public void scoutUpdate() {
    assignScoutWorker();
    scout();
    updateBaseInfo();
}

정찰병을 지정 및 영역들을 저장하여 최종적으로 적군 위치를 찾아냅니다.

public void assignScoutWorker() {

    if (enemyBase.isValid(BroodWar) && enemyBase != Position.Unknown) {
        return;
    }

    if (ExampleUtil.countUnitType(UnitType.Protoss_Pylon) < 1) {
        return;
    }

    if (scoutWorker == null) {
        for (Unit unit : workers) {
            if (unit == null || !unit.exists()) {
                continue;
            }

            if (unit.isConstructing()) {
                continue;
            }

            scoutWorker = unit;
            System.out.println("정찰 유닛 ID: " + scoutWorker.getID());
            break;
        }
    }

}
public void scout() {

    if (enemyBase.isValid(BroodWar) && enemyBase != Position.Unknown) {
        return;
    }

    if (scoutWorker == null) {
        return;
    }

    List<TilePosition> startLocations = ExampleBot.BroodWar.getStartLocations();
    for (TilePosition tp : startLocations) {
        if (ExampleBot.BroodWar.isExplored(tp)) {
            if (ExampleUtil.checkEnemyBase(tp)) {
                try {
                    enemyBase = map.getArea(tp).getBases().get(0).getLocation().toPosition();
                } catch (NullPointerException e) {
                    enemyBase = BroodWar.getStartLocations().get(BroodWar.getStartLocations().size() - 1).toPosition();
                }
            }
            continue;
        }

        Position pos = tp.toPosition();

        scoutWorker.move(pos);
    }
}
public void updateBaseInfo() {
    if (scoutWorker == null) {
        return;
    }

    if (ExampleBot.enemyBase != Position.Unknown) {
        System.out.println("상대 위치: " + ExampleBot.enemyBase.toTilePosition());
        scoutWorker.move(ExampleBot.BroodWar.self().getStartLocation().toPosition());
        scoutWorker = null;
    }
}

 

// 질럿 생산
public void trainSoldier(UnitType unitType) {

    if (unitType == null) {
        return;
    }

    for (Unit trainableBuilding : BroodWar.self().getUnits()) {
        if (trainableBuilding == null || !trainableBuilding.exists()) {
            continue;
        }

        if (!trainableBuilding.getType().isBuilding()) {
            continue;
        }

        if (!trainableBuilding.canTrain(unitType)) {
            continue;
        }
        
        if (!trainableBuilding.isTraining()) {
            trainableBuilding.train(unitType);
        }
    }

}

일꾼 생산 코드를 활용하여 질럿들을 생산하고

// 공격
public void zealotRush() {

    if (!ExampleUtil.delay(24)) {
        return;
    }

    if (!StartAttack) {
        // 공격 가기 전에는 첫 번째 길목에서 대기
        for (Unit soldier : soldiers) {
            if (soldier == null || !soldier.exists()) {
                continue;
            }

            soldier.move(waitingPosition);
        }
        
        if (!ZealotRush) {
            if (BroodWar.self().completedUnitCount(UnitType.Protoss_Zealot) > 5) {
                StartAttack = true;
                ZealotRush = true;
            }
            return;
        }

        if (BroodWar.enemy().getRace() == Race.Zerg) {
            StartAttack = soldiers.size() > 15;
        }
        else {
            StartAttack = soldiers.size() > 20;
        }

    }
    else {
        attack();
        StartAttack = (soldiers.size() > 4);

        if (ZealotRush) {
            if (deadZealots > zealotRushEnd) {
                zealotRushEnd = 99;
                StartAttack = false;
            }
        }

    }
}

public void attack() {
    for (Unit soldier : soldiers) {
        if (soldier == null || !soldier.exists()) {
            continue;
        }

        if (!hasBrokeCenter) {
            soldier.attack(enemyBase);
        }
        else {
            for (Unit building : leftBuildings) {
                if (building == null) {
                    continue;
                }

                soldier.attack(building.getPosition());
            }
        }
    }
}

만일 질럿 러쉬를 시도하였으나 4기 이상 질럿이 사망하였다면 후퇴를 결정하고

public void buildCore() {
    UnitType core = UnitType.Protoss_Cybernetics_Core;
    int gasCount = ExampleUtil.countUnitType(UnitType.Protoss_Assimilator);
    int coreCount = ExampleUtil.countUnitType(core);

    if (StartAttack || !ZealotRush) {
        return;
    }

    if (coreCount > 1 || gasCount < 1) {
        return;
    }

    ExampleUtil.buildBuildings(core);
}

코어를 건설 및 드라군 생산을 준비합니다.

보다 자세한 코드 링크를 참조해주시면 감사하겠습니다.

 

해당 완성본 봇에 대한 리플레이 파일 및 영상입니다.

Zealot.rep
0.06MB