UnitGroup
UnitGroup은 epScript에서 여러 유닛을 효율적으로 그룹화하고 관리할 수 있는 자료구조입니다.
유닛의 EPD 를 순회할 때 순서를 고려할 필요가 없고 소·중규모 그룹 관리에 적합합니다.
대규모일 땐?
1000기 이상 많은 유닛을 대상으로 하는 대규모 그룹일 때는 EUDLoopUnit과 같은 전체유닛루프가 더 나을 수 있습니다. 또한 대규모 유닛을 대상으로 하면서 성능을 극한으로 최적화해야 하는 상황이라면, cptrick을 활용한 루프를 직접 제작하는게 가장 좋습니다.
- 1000기 이상 대규모 유닛 관리 시 →
EUDLoopUnit권장 - 성능 극한 최적화가 필요하다면 → cptrick 기반 직접 루프 제작 참고
1. UnitGroup이란 무엇인가?
스타크래프트에서 특정 유닛을 일괄 제어하려면, 각 유닛의 EPD 주소를 직접 관리해야 합니다.
UnitGroup은 이를 대신해주는 EPD 기반 컨테이너이며, cploop와 같은 순회 기능을 제공합니다. 이를 통해 특정 조건을 만족하는 유닛만 그룹에 모아 순회하거나 일괄 처리할 수 있습니다.
2. 주요 특징
- 유닛 추가 / 제거 / 순회 / 초기화
- 그룹 단위 상태 관리
dying블록을 통한 사망 처리- 정해진 최대 용량 내에서만 관리 (생성 시 지정)
- 코드 가독성과 유지보수성이 향상됨
3. 기본 메서드
add(unit_epd)
유닛의 EPD를 그룹에 추가합니다.
// 최대 1000개의 유닛을 저장할 수 있는 그룹 생성
// 전역변수로 선언하는 걸 추천합니다.
const marines = UnitGroup(1000);
function createGroupUnit() {
// SCdata 자료형에서 설명하겠지만, 다음에 생성될 유닛의 포인터를 통해 CUnit 객체를 생성합니다.
const cunit = CUnit().from_next();
CreateUnit(1, "Terran Marine", "Anywhere", P1);
marines.add(cunit); // 개별 유닛 추가
}
cploop
그룹에 포함된 모든 유닛을 순회하면서 처리합니다. CurrentPlayer를 해당 유닛의 EPD로 설정하면서 순회합니다.
foreach(unit : marines.cploop) {
// unit.dying으로 죽은 유닛 감지 가능
foreach(dead : unit.dying) {
// 죽은 유닛 처리
// dying 블록이 끝나면 자동으로 remove() 호출
}
// 살아있는 유닛 처리
}
dying
유닛이 죽었는지 확인하고 처리할 수 있는 블록을 제공합니다. 죽은 유닛은 블록이 끝날 때 자동으로 그룹에서 제거됩니다.
remove()
현재 순회 중인 유닛을 그룹에서 제거합니다. 주의: dying 블록 안에서는 직접 호출하지 마세요. 자동으로 호출됩니다.
length
그룹에 포함된 유닛의 수를 반환합니다. (읽기 전용)
move_cp(offset)와 set_cp(offset)
이 두 메서드는 CurrentPlayer를 조작하여 유닛의 메모리에 접근하는 방법을 제공합니다. 트리거 코드 생성 시점에서의 차이점이 중요합니다.
move_cp | set_cp |
|---|---|
| 이전 위치를 기준으로 계산된 트리거를 생성 | 항상 유닛의 시작 위치를 기준으로 계산 |
| if문이나 반복문에서 현재 위치 추적이 어려울 수 있음 | 조건문과 관계없이 동일한 위치로 이동 |
| 제어문이 복잡할수록 의도하지 않은 위치로 이동할 위험 존재 | 독립적인 메모리 접근에 유용 |
| 연속된 메모리 접근에는 효율적이나, 제어문 내에서는 주의 필요 | 지정된 위치로 이동하기 때문에 직관적이나 매번 계산 필요 |
offset은 무엇인가?
Offset은 각 구조체의 시작점으로부터 떨어진 거리를 의미합니다. 그리고 여기서 이야기하는 Offset은 CUnit 구조체와 관련되어 있고 이에 대한 정보는 EUD Lab - CUnit 탭에서 확인할 수 있습니다.
foreach(unit : marines.cploop) {
if(condition) {
// move_cp: 현재 위치에서 상대적으로 이동
unit.move_cp(0x08/4); // HP 위치로
const hp1 = bread_cp(0, 0);
unit.move_cp(0x4C/4); // 소유자 위치로 (이전 위치 기준)
const owner = bread_cp(0, 0);
} else {
// set_cp: 절대적인 위치로 이동
unit.set_cp(0x08/4); // 직접 HP 위치로
const hp2 = bread_cp(0, 0);
unit.set_cp(0x4C/4); // 직접 소유자 위치로
const owner = bread_cp(0, 0);
}
}
두 메서드는 조건문이나 반복문 안에서 트리거가 생성되는 방식에 차이가 있습니다. move_cp는 현재 위치를 추적하면서 트리거를 생성하지만, 복잡한 제어문에서는 위치 추적이 실패할 수 있습니다. 반면 set_cp는 매번 절대 위치로 이동하는 트리거를 생성하므로 제어문과 관계없이 안전합니다. 효율적인 순환과 cp트릭 활용을 위해 이부분을 주의해야합니다.
// 주의: move_cp 사용 시 제어문에서의 위험 예시
foreach(unit : marines.cploop) {
if(condition1) {
unit.move_cp(0x08/4); // HP 위치로
if(condition2) {
// 여기서의 위치가 예상과 다를 수 있음
unit.move_cp(0x4C/4); // 의도한 위치로 이동하지 않을 수 있음
}
unit.move_cp(0x98/4);
//move_cp 는 컴파일 시간에 CurrentPlayer에 몇을 더해야할지 계산 후 cp 변경하는 트리거를 생성해주기 move_cp 라인이 보일 때마다 현재 currentPlayer 값을 캐싱합니다.
//unit.move_cp(0x98/4); 라인을 실행할 때 현재 위치를 0x4C/4 로 알고 addcurpl 을 해주기 때문에 condition2 가 false 일때는 필요한 것보다 작은 값을 더하게 됩니다.
}
}
// 안전: set_cp 사용: set_cp 는 기억된 현재 위치를 항상 하나로 고정하고 연산하기 때문에 move_cp 가 가진 문제를 피해갈 수 있지만 cp 트릭은 최적화에 초점을 맞추고 있기 때문에 안정성이 걱정되어 set_cp 만 쓰고 싶다면 차라리 const cunit = CUnit(unit.epd); 를 써서 CUnit 사용을 고려해보세요.
foreach(unit : marines.cploop) {
const cunit = CUnit(unit.epd)
if(cunit.hp > 256) {
const orderId= cunit.orderID;
if(condition2) {
}
}
}
4. 사용 예시
체력이 낮은 유닛을 감지하고 치료하는 예시입니다. 처음 입문하시는 분들에겐 cptrick보단 UnitGroup을 활용하면서 내부는 scdata 같은 자료형을 적극적으로 사용하기를 권장합니다.
const s = StringBuffer(1024);
const lowHpUnits = UnitGroup(1000);
function afterTriggerExec() {
// 새로 생성된 유닛들 중에서 체력이 낮은 유닛 찾기
foreach(cunit : EUDLoopNewCUnit()) {
if (cunit.hp <= 50*256) {
lowHpUnits.add(cunit);
}
}
// 그룹 내 유닛들 처리 (cp트릭 활용버전)
foreach(unit : lowHpUnits.cploop) {
foreach(dead : unit.dying) {
// 죽은 유닛 처리
dead.move_cp(0x4C/4);
const owner = maskread_cp(0, 0xF);
setcurpl(owner);
s.printf("유닛이 치료받기 전에 죽었습니다.");
}
// 살아있는 유닛 치료
// 먼저, 해당 유닛타입의 최대체력 가져오기
unit.move_cp(0x64/4);
const unitType = maskread_cp(0, 0xFFFF);
const scUnit = TrgUnit(unitType);
const maxHp = scUnit.maxHp;
// cp트릭 내부에서는 함수 이름이 _cp로 끝나는 함수를 써야 합니다.
// 만약, 위의 scUnit.maxHp처럼 cp트릭이 아닌 경우에는 set_cp를 써서 안전하게 이동합니다.
// 최대체력보다 현재체력이 낮은 경우 회복시켜주기
unit.set_cp(0x08/4);
if (Deaths(CurrentPlayer, AtMost, maxHp-1, 0)) {
SetDeaths(CurrentPlayer, Add, 256, 0); // 체력은 256단위이므로, 256을 써야 1 회복됨.
}
}
}
const s = StringBuffer(1024);
const lowHpUnits = UnitGroup(1000);
function afterTriggerExec() {
// 새로 생성된 유닛들 중에서 체력이 낮은 유닛 찾기
foreach(cunit : EUDLoopNewCUnit()) {
if (cunit.hp <= 50*256) {
lowHpUnits.add(cunit);
}
}
// cp트릭 대신 SCdata 자료형 활용
foreach(unit : lowHpUnits.cploop) {
foreach(dead : unit.dying) {
// 죽은 유닛 처리
const cunit = CUnit(dead.epd);
setcurpl(cunit.owner);
s.printf("유닛이 치료받기 전에 죽었습니다.");
}
// 살아있는 유닛 치료
// 먼저, 해당 유닛타입의 최대체력 가져오기
const cunit = CUnit(unit.epd);
const scUnit = TrgUnit(cunit.unitType);
const maxHp = scUnit.maxHp;
// 최대체력보다 현재체력이 낮은 경우 회복시켜주기
if (cunit.hp < maxHp) {
cunit.hp += 256;
}
}
}
5. 주의할 점
UnitGroup은 생성 시 지정한 최대 용량 이상의 유닛을 저장할 수 없습니다.dying블록 안에서는remove()를 직접 호출하지 마세요.- 모든 유닛이 포함된 그룹은 그룹화 할 필요 없는 그룹입니다.
EUDLoopUnit을 고려하세요. - 그룹 순회 시에는
cploop를 사용하세요.
6. 정리
UnitGroup은 유닛 EPD 관리의 복잡성을 줄이는 컨테이너입니다.- 순회 시
cploop를 통해 CurrentPlayer 조작을 자동화합니다. - 죽은 유닛 처리를 위한
dying블록을 제공합니다. - 메모리 효율을 위해 정해진 크기로 관리됩니다.
- 처음 입문하는 분들에게는 UnitGroup 기본적인 사용법(cploop, dying)과 SCdata 자료형을 섞어서 사용하기를 권장합니다.