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

스타크래프트 봇 강의 튜토리얼

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

지난 번에는 준비 과정을 끝냈습니다.

이제 본격적으로 개발 강의를 시작하겠습니다.

 

먼저 튜토리얼을 살짝 하도록 하겠습니다.

모든 API들이 다 그러하겠지만, Jbwapi 같은 경우에는 더더욱 다루기 어려운 라이브러리 중 하나이기 때문에

어느정도 틀을 잡고 시작하기 위해 튜토리얼을 진행하도록 하겠습니다.

 

 

먼저 크게 3가지를 알고 있으시면 됩니다.

1) Jbwapi는 스타크래프트에서 발생하는 이벤트를 활용하여 전/후 처리가 작동하는 메커니즘을 가지고 있습니다.

한 마디로, 이벤트 없이는 작동하지 않는다라고 요약할 수 있고, 이 말은 즉슨 이벤트를 받아들이는 메소드 내에 코드를 작성해야한다고 말할 수 있습니다.

조금 말인 난해할 수 있기에 다시 조금 쉽게 설명하자면 아래 그림과 같습니다.

스타크래프트에서 이벤트가 발생하면 JBwapi가 이를 잡습니다.

이를 Jbwapi 이벤트로 처리하여 해당 이벤트가 발생했음을 처리합니다.

이때, 이벤트가 처리하고 나서 추후 어떤 처리를 할 것인지 작성합니다.

해당 코드들을 다시 한 번 jbwapi 이벤트로 묶어 jbwapi가 이를 다시 스타크래프트로 보냅니다.

이를 스타크래프트가 이해를 하여 코드를 처리합니다.

 

2) JBwapi는 편한 프레임워크가 아니다.

위 그림만 참고한다면 굉장히 편한 것처럼 느껴지실 수도 있으십니다만

저희는 제가 강의 준비편에서 말했다시피 

"사람이 할 수 있는 플레이는 봇 역시 수행할 수 있어야한다."라고 말했습니다.

 

위 그림과 같이 이벤트들 외에는 크게 알려주는 기능들이 없습니다.

(이벤트 발생과 동시에 많은 것들을 JBwapi가 저장하여 가져와서 나름 편하게 개발할 수 있도록 해주지만, 아직까지도 편하지는 않습니다.)

예를 들어 어디가 벽인지, 또 어디가 길목인지 어디가 멀티 진영인지 알려주지 않습니다.

그렇기에 외부 라이브러리들을 계속 붙여다 쓰면서 봇을 완성시켜가야하기에 편한 프레임워크가 아닌 겁니다.

 

3) 게임 = 수학

게임은 수학의 집합체와도 같습니다. 그렇기에 주로 어떠한 물리작용 혹은 방향등을 잡을 때에는 전부 수학을 사용해야한다는 의미입니다. 이는 누구나 알겠지만, 이를 저희는 게임을 직접 만드는 것도 아닌데 왠만한 게임 개발자들도 잘 사용하지않는 수학들을 사용해야합니다. (물론 사용할 수도 있습니다.)

Unity와 같이 미리 내장 메소드로 방향과 속도를 편하게 조절할 수 없기때문에

저희는 자바 내장 Math 라는 패키지를 사용하여 일일이 계산해가며 개발을 진행해야합니다.

 

 

위 3가지를 어느정도 숙지하시고 봇을 개발하시길 바랍니다.

이제 본격적으로 튜토리얼을 진행해봅시다.

 

먼저 메인 클래스를 만드세요.

그냥 대충

public class Main {

    public static void main(String[] args) {
    }
    
}

이렇게 해주세요.

 

그리고 BWClient, Game 변수를 멤버 변수로 선언해주세요.

public BWClient client;
public static Game game;

그리고 인터페이스 하나를 상속해서 이벤트를 받아들일 클래스라는 것을 정의합니다.

implements BWEventListener

 

그러면 Override(재정의)를 해야하는 메소드들이 굉장히 많기 때문에

만약 본인이 필요한 이벤트만 딱딱 사용하고 싶으시다면

인터페이스 상속이 아닌 그냥 상속을 해주시면 됩니다.

extends DefaultBWListener

그리고 메인 메소드에 메인을 인스턴스화 변수를 하나 정의하시고

client 멤버를 생성자를 사용하셔서 정의해주세요.

그리고

client.startGame();

메소드로 이벤트를 받아들이도록 합니다.

public static void main(String[] args) {
    Main main = new Main();
    main.client = new BWClient(main);
    main.client.startGame();
}

사람마다 다르게 작성할 수 있지만 저는 튜토리얼 코드에만 이런방식으로 작성하였습니다.

 

그리고 onStart 이벤트에 client game을 지정합니다.

@Override
public void onStart() {
    game = client.getGame();
}

위처럼 하는 이유는 단순합니다.

여기서 말하는 client는 봇입니다.

즉, 봇이 현재 진행할 게임에 대해서 game에 지정하므로서 

해당 게임에서 발생하여 저장이되는 여러 변수들을 보다 편하게 작업할 수 있도록 도와줍니다.

onStart 이벤트는 게임이 최초 시작되었을 때 작동하는 이벤트입니다.

초기화를 진행할 때 사용될 수 있다는 의미입니다.

 

튜토리얼에서 개발할 기능들은 단순한 기능들 뿐입니다.

* 미네랄 채취

* 일꾼 생산

* 인구수 건물

* 맵 그리기

 

먼저 Tools 라는 클래스를 하나 만듭니다.

Tools 클래스는 스타봇이 작동할 때, 조금 편하게 개발하기 위해 만드는 유틸리티 클래스라고 생각하시면 됩니다.

getClosestUnitTo(Position p, final List<Unit> units);

라는 메소드를 하나 만듭니다.

public static Unit getClosestUnitTo(Position p, final List<Unit> units) {
    Unit closestUnit = null;

    for (Unit unit : units) {
        if (closestUnit == null) {
            closestUnit = unit;
            continue;
        }

        if (unit.getDistance(p) < closestUnit.getDistance(p)) {
            closestUnit = unit;
        }
    }

    return closestUnit;
}

Unit 클래스를 반환하는 메소드로 제일 가까운 Unit을 반환합니다.

Unit클래스는 스타크래프트에 존재하는 모든 유닛들이 전부 상속되어 있습니다.

예) 미네랄, 가스, 간혈천, 마린, 질럿, 건물 등등

Position클래스는 말그대로 위치 클래스 중 하나입니다.

위치 클래스는 크게 3가지가 있습니다.

Position / TilePosition / WalkPosition 

TilePosition이 제일 작은 단위입니다.

쉽게 생각하시면 스타크래프트를 해보시면 다들 아시겠지만

한 타일 (픽셀)이라고 생각하시면 됩니다.

그리고 WalkPosition은 8TilePosition이 1WalkPosition이라고 생각하시면 됩니다.

스타크래프트는 유닛의 크기를 비례하여 8타일의 공간이 있다면 이동 가능한 위치입니다.

그리고 Position이 있는 데 소위 말하는 맵크기가 128 x 128 이라고 말하는 데 말하는 것과 똑같다고 생각하시면 됩니다.

1Position = 8WalkPosition = 32TilePosition 

 

그리고 스타봇을 개발하다보면 NullPointerException이 유독 많이 발생하는 데, 이는 JBwapi가 불친절함이 조금 있기때문에 직접 if (unit == null) return; 를 해줘야하는 번거로움이 조금 있습니다.

unit.getDistance(Position) 이라는 메소드가 있는 데,

이는 unit의 위치에서 Position까지의 거리를 TilePosition으로 반환해주는 기능을 합니다.

 

getClosestUnitTo 메소드를 오버로딩해서 아래와 같은 메소드도 작성합니다.

public static Unit getClosestUnitTo(Unit unit, final List<Unit> units) {
    if (unit == null) {
        return null;
    }
    return getClosestUnitTo(unit.getPosition(), units);
}

 

그리고 자신이 소유한 유닛들 중 특정 유닛 하나를 반환하기 쉽게 하는 메소드를 작성합니다.

public static Unit getUnitOfType(UnitType type) {
    for (Unit unit : Main.game.self().getUnits()) {
        if (unit.getType() == type && unit.isCompleted()) {
            return unit;
        }
    }

    return null;
}

Main.game.self().getUnits() 는

아까 말했던 game에 있는 self, 본인에 있는 getUnits, 모든 유닛들을 리스트 처리하여 반환해준 것을 바탕으로

해당 타입과 동일한가, 그리고 완성이 되었는 지 파악합니다.

 

이를 조금 응용하여 depot(메인 건물)을 가져오는 메소드

public static Unit getDepot() {
    final UnitType depot = Main.game.self().getRace().getResourceDepot();
    return getUnitOfType(depot);
}

그리고 건물을 짓는 메소드를 개발합니다.

링크를 참고해주시길 바랍니다.

public static boolean buildBuilding(UnitType type) {
    UnitType builderType = type.whatBuilds().getFirst();

    Unit builder = Tools.getUnitOfType(builderType);
    if (builder == null) {
        return false;
    }

    TilePosition desiredPos = Main.game.self().getStartLocation();

    int maxBuildingRange = 64;
    boolean buildingOnCreep = type.requiresCreep();
    TilePosition buildPos = Main.game.getBuildLocation(type, desiredPos, maxBuildingRange, buildingOnCreep);
    return builder.build(type, buildPos);
}

 

그리고 MapTools이라는 클래스를 만듭니다.

MapTools는 맵에 관련해서 처리하는 클래스입니다.

그리고 이를 도와주는 클래스인 Grid 내부 클래스를 생성합니다.

Grid 클래스는 제네릭을 사용하여 코드를 조금이라도 더 효율적으로 작성하도록 도와줍니다.

static class Grid<T> {
}

 

그리고 높이, 넓이, 그리고 이를 저장할 2차원 배열를 멤버 변수로 정의 및 생성자를 작성합니다

static class Grid<T> {
    int m_width;
    int m_height;

    T[][] m_grid;

    public Grid(int width, int height, T val) {
        this.m_width = width;
        this.m_height = height;
        this.m_grid = (T[][]) new Object[width][height];
    }

    public T get(int x, int y) {
        return m_grid[x][y];
    }

    public void set(int x, int y, T val) {
        m_grid[x][y] = val;
    }

    public int width() {
        return m_width;
    }

    public int height() {
        return m_height;
    }

}

그리고 이를 활용하여 내용들을 저장하고

맵을 그립니다

 

멤버 변수들로는 저장의 역할을 합니다.

public static int m_width = 0;
public static int m_height = 0;
public static int m_frame = 0;
public static boolean m_drawMap = false;

public static Grid<Boolean> m_walkable;
public static Grid<Boolean> m_buildable;
public static Grid<Boolean> m_depotBuildable;
public static Grid<Integer> m_lastSeen;

그리고 이를 기반으로 그리는 기능 및 메소드를 기반으로 특정 영역을 저장을 돕도록 만듭니다.

public void toggleDraw() {
    m_drawMap = !m_drawMap;
}

public boolean isExplored(TilePosition pos) {
    return isExplored(pos.x, pos.y);
}

public boolean isExplored(Position pos) {
    return isExplored(pos.toTilePosition());
}

public boolean isExplored(int tileX, int tileY) {
    if (!isValidTile(tileX, tileY)) {
        return false;
    }
    return Main.game.isExplored(tileX, tileY);
}

public boolean isVisible(int tileX, int tileY) {
    if (!isValidTile(tileX, tileY)) {
        return false;
    }
    return Main.game.isVisible(tileX, tileY);
}

public boolean isPowered(int tileX, int tileY) {
    return Main.game.hasPower(new TilePosition(tileX, tileY));
}

public boolean isValidTile(int tileX, int tileY) {
    return tileX >= 0 && tileY >= 0 && tileX < m_width && tileY < m_height;
}

public boolean isValidTile(TilePosition tile) {
    return isValidTile(tile.x, tile.y);
}

public boolean isValidPosition(Position pos) {
    return isValidTile(pos.toTilePosition());
}

public boolean isBuildable(int tileX, int tileY) {
    if (!isValidTile(tileX, tileY)) {
        return false;
    }

    return m_buildable.get(tileX, tileY);
}

public boolean isBuildable(TilePosition tile) {
    return isBuildable(tile.x, tile.y);
}

public void printMap() {
    for (int y = 0; y < m_height; ++y) {
        for (int x = 0; x < m_width; ++x) {
            System.out.printf("%b", isWalkable(x, y));
        }
        System.out.println();
    }
}

public boolean isDepotBuildableTile(int tileX, int tileY) {
    if (!isValidTile(tileX, tileY)) {
        return false;
    }

    return m_depotBuildable.get(tileX, tileY);
}

public boolean isWalkable(int tileX, int tileY) {
    if (!isValidTile(tileX, tileY)) {
        return false;
    }

    return m_walkable.get(tileX, tileY);
}

public boolean isWalkable(TilePosition tile) {
    return isWalkable(tile.x, tile.y);
}

public int width() {
    return m_width;
}

public int height() {
    return m_height;
}

public void drawTile(int tileX, int tileY, final Color color) {
    final int padding = 2;
    final int px = tileX * 32 + padding;
    final int py = tileY * 32 + padding;
    final int d = 32 - 2 * padding;

    Main.game.drawLineMap(px, py, px + d, py, color);
    Main.game.drawLineMap(px + d, py, px + d, py + d, color);
    Main.game.drawLineMap(px + d, py + d, px, py + d, color);
    Main.game.drawLineMap(px, py + d, px, py, color);
}

public boolean canWalk(int tileX, int tileY) {
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            if (!Main.game.isWalkable(tileX * 4 + i, tileY * 4 + j)) {
                return false;
            }
        }
    }

    return true;
}

public boolean canBuild(int tileX, int tileY) {
    return Main.game.isBuildable(new TilePosition(tileX, tileY));
}

public void draw() {
    final TilePosition screen = Main.game.getScreenPosition().toTilePosition();
    final int sx = screen.x;
    final int sy = screen.y;
    final int ex = sx + 20;
    final int ey = sy + 15;

    for (int x = sx; x < ex; ++x) {
        for (int y = sy; y < ey; y++) {
            final TilePosition tilePos = new TilePosition(x, y);
            if (!tilePos.isValid(Main.game)) {
                continue;
            }
                
            if (true) {
                Color color = isWalkable(x, y) ? new Color(0, 255, 0) : new Color(255, 0, 0);
                if (isWalkable(x, y) && !isBuildable(x, y)) {
                    color = new Color(255, 255, 0);
                }
                if (isBuildable(x, y) && !isDepotBuildableTile(x, y)) {
                    color = new Color(127, 255, 255);
                }
                drawTile(x, y, color);
            }
        }
    }

    final String red = Integer.toHexString(8);
    final String green = Integer.toHexString(7);
    final String white =  Integer.toHexString(4);
    final String yellow = Integer.toHexString(3);

    Main.game.drawBoxScreen(0, 0, 200, 100, Color.Black, true);
    Main.game.setTextSize(Text.Size.Huge);
    Main.game.drawTextScreen(10, 5, white + "Map Legend");
    Main.game.setTextSize(Text.Size.Default);
    Main.game.drawTextScreen(10, 30, red + "Red");
    Main.game.drawTextScreen(60, 30, white + "Can't walk or build");
    Main.game.drawTextScreen(10, 45, green + "Green");
    Main.game.drawTextScreen(60, 45, white + "Can't walk or build");
    Main.game.drawTextScreen(10, 60, yellow + "YELLOW");
    Main.game.drawTextScreen(60, 60, white + "Resource Tile, Can't Build");
    Main.game.drawTextScreen(10, 75, "Teal:");
    Main.game.drawTextScreen(60, 75, white + "Can't Build Depot");
}

내용이 너무 길어지는 관계로 튜토리얼 2편으로 다시 찾아뵙겠습니다.

 

이상입니다.