문제

저는 Java 플랫폼에서 실시간 전략 게임 클론을 개발 중이며 어디에 배치하고 게임 상태를 관리하는 방법에 대한 몇 가지 개념적인 질문이 있습니다.게임은 렌더링으로 Swing/Java2D를 사용합니다.현재 개발 단계에서는 시뮬레이션이나 AI가 존재하지 않으며 사용자만이 게임 상태를 변경할 수 있습니다(예: 건물 건설/철거, 생산 라인 추가/제거, 차량 및 장비 조립).따라서 게임 상태 조작은 렌더링 조회 없이 이벤트 전달 스레드에서 수행될 수 있습니다.게임 상태는 사용자에게 다양한 집계 정보를 표시하는 데에도 사용됩니다.

그러나 시뮬레이션(예: 건물 진행 상황, 인구 변화, 차량 이동, 제조 프로세스 등)을 도입해야 하므로 타이머 및 EDT에서 게임 상태를 변경하면 렌더링 속도가 확실히 느려집니다.

시뮬레이션/AI 작업이 500ms마다 수행되고 SwingWorker를 사용하여 약 250ms 길이의 계산을 한다고 가정해 보겠습니다.시뮬레이션과 가능한 사용자 상호 작용 사이에 게임 상태 읽기와 관련된 경쟁 조건이 없는지 어떻게 확인할 수 있나요?

저는 시뮬레이션 결과(소량의 데이터)를 SwingUtilities.invokeLater() 호출을 통해 효율적으로 EDT로 다시 이동할 수 있다는 것을 알고 있습니다.

게임 상태 모델은 너무 복잡해서 어디에서나 불변 값 클래스를 사용하는 것이 불가능해 보입니다.

이 읽기 경쟁 조건을 제거하기 위한 비교적 올바른 접근 방식이 있습니까?모든 타이머 틱에서 전체/부분 게임 상태 복제를 수행하거나 게임 상태의 생활 공간을 EDT에서 다른 스레드로 변경하시겠습니까?

업데이트: (내가 준 의견에서) 이 게임은 13명의 AI 제어 플레이어, 1명의 인간 플레이어로 작동하며 약 10000개의 게임 개체(행성, 건물, 장비, 연구 등)가 있습니다.예를 들어 게임 개체에는 다음과 같은 속성이 있습니다.

World (Planets, Players, Fleets, ...)
Planet (location, owner, population, type, 
    map, buildings, taxation, allocation, ...)
Building (location, enabled, energy, worker, health, ...)

시나리오에서 사용자는 이 행성에 새 건물을 짓습니다.지도 및 건물 컬렉션을 변경해야 하므로 이는 EDT에서 수행됩니다.이와 병행하여 모든 게임 행성의 건물에 대한 에너지 할당을 계산하기 위해 500ms마다 시뮬레이션이 실행되며, 이는 통계 수집을 위해 건물 컬렉션을 통과해야 합니다.할당이 계산되면 EDT에 제출되고 각 건물의 에너지 필드가 할당됩니다.

AI 계산 결과는 어쨌든 EDT의 구조에 적용되기 때문에 인간 플레이어 상호 작용에만 이 속성이 있습니다.

일반적으로 객체 속성의 75%는 정적이며 렌더링에만 사용됩니다.나머지 부분은 사용자 상호 작용이나 시뮬레이션/AI 결정을 통해 변경할 수 있습니다.또한 이전 단계에서 모든 변경 사항을 다시 기록할 때까지 새로운 시뮬레이션/AI 단계가 시작되지 않도록 보장됩니다.

내 목표는 다음과 같습니다

  • 사용자 상호작용을 지연시키지 마세요.사용자가 건물을 행성에 배치하고 0.5초 후에만 시각적 피드백을 받습니다.
  • 계산, 잠금 대기 등으로 EDT를 차단하지 마세요.
  • 컬렉션 순회 및 수정, 속성 변경과 관련된 동시성 문제 방지

옵션:

  • 세밀한 개체 잠금
  • 불변 컬렉션
  • 휘발성 장
  • 부분 스냅샷

이 모든 것에는 모델과 게임에 대한 장점, 단점 및 원인이 있습니다.

업데이트 2: 내가 얘기하는거야 이것 게임.내 클론은 여기.스크린샷은 렌더링 및 데이터 모델 상호 작용을 상상하는 데 도움이 될 수 있습니다.

업데이트 3:

내 문제가 잘못 이해된 것처럼 보이는 문제를 명확히 하기 위해 작은 코드 샘플을 제공하려고 합니다.

List<GameObject> largeListOfGameObjects = ...
List<Building> preFilteredListOfBuildings = ...
// In EDT
public void onAddBuildingClicked() {
    Building b = new Building(100 /* kW */);
    largeListOfGameObjects.add(b);
    preFilteredListOfBuildings.add(b);
}
// In EDT
public void paint(Graphics g) {
    int y = 0;
    for (Building b : preFilteredListOfBuildings) {
        g.drawString(Integer.toString(b.powerAssigned), 0, y);
        y += 20;
    }
}
// In EDT
public void assignPowerTo(Building b, int amount) {
    b.powerAssigned = amount;
}
// In simulation thread
public void distributePower() {
    int sum = 0;
    for (Building b : preFilteredListOfBuildings) {
        sum += b.powerRequired;
    }
    final int alloc = sum / (preFilteredListOfBuildings.size() + 1);
    for (final Building b : preFilteredListOfBuildings) {
        SwingUtilities.invokeLater(=> assignPowerTo(b, alloc));            
    }
}

따라서 onAddBuildingClicked()와 distributionPower() 사이에 겹치는 부분이 있습니다.이제 게임 모델의 다양한 부분 사이에 이러한 종류의 중복이 50개 있는 경우를 상상해 보십시오.

도움이 되었습니까?

해결책

이는 클라이언트/서버 접근 방식의 이점을 누릴 수 있는 것처럼 들립니다.

플레이어는 클라이언트입니다. 그 끝에서 상호작용과 렌더링이 발생합니다.따라서 플레이어가 버튼을 누르면 요청이 서버로 이동합니다.서버로부터 응답이 돌아오고 플레이어의 상태가 업데이트됩니다.이러한 일이 발생하는 사이 어느 시점에서든 화면을 다시 칠할 수 있으며 클라이언트가 현재 알고 있는 게임 상태를 반영합니다.

AI도 마찬가지로 클라이언트입니다. 이는 봇과 같습니다.

시뮬레이션은 서버입니다.다양한 시간에 클라이언트로부터 업데이트를 받고 세계 상태를 업데이트한 다음 이러한 업데이트를 적절하게 모든 사람에게 보냅니다.귀하의 상황과 관련된 부분은 다음과 같습니다.시뮬레이션/AI에는 정적인 세계가 필요하며 많은 일이 동시에 일어나고 있습니다.서버는 업데이트를 클라이언트에 다시 보내기 전에 변경 요청을 대기열에 추가하고 적용할 수 있습니다.따라서 서버에 관한 한 게임 세계는 실제로 실시간으로 변경되는 것이 아니라 서버가 결정하는 대로 변경됩니다.

마지막으로, 클라이언트 측에서는 몇 가지 빠른 대략적인 계산을 수행하고 결과를 표시한 다음(즉각적인 요구 사항이 충족됨) 서버가 돌아오면 더 정확한 결과를 표시함으로써 버튼을 누른 후 결과를 확인하는 사이의 지연을 방지할 수 있습니다. 당신과 이야기하는 것입니다.

이는 실제로 인터넷을 통한 TCP/IP 방식으로 구현될 필요는 없으며 단지 이러한 용어로 생각하는 데 도움이 됩니다.

또는 데이터베이스가 이미 잠금 및 일관성을 염두에 두고 구축되었으므로 시뮬레이션 중에 데이터 일관성을 유지하는 책임을 데이터베이스에 둘 수 있습니다.sqlite와 같은 것은 네트워크에 연결되지 않은 솔루션의 일부로 작동할 수 있습니다.

다른 팁

귀하가 찾고 있는 동작을 완전히 이해했는지는 모르겠지만 모든 상태 변경이 단일 스레드에 의해 처리되도록 하려면 상태 변경 스레드/큐와 같은 것이 필요한 것 같습니다.

상태 변경 요청을 처리하기 위해 상태 변경 대기열에 대해 SwingUtilities.invokeLater() 및/또는 SwingUtilities.invokeAndWait()와 같은 API를 만듭니다.

그것이 GUI에 어떻게 반영되는지는 당신이 찾고 있는 행동에 달려 있다고 생각합니다.즉.현재 상태가 $0이므로 출금할 수 없거나 출금 요청이 처리될 때 계좌가 비어 있다는 메시지가 사용자에게 표시됩니다.(아마도 해당 용어는 아닐 것입니다 ;-))

가장 쉬운 접근 방식은 EDT에서 실행할 수 있을 만큼 시뮬레이션을 빠르게 만드는 것입니다.효과가 있는 프로그램을 선호하세요!

2스레드 모델의 경우, 제가 제안하는 것은 도메인 모델을 렌더링 모델과 동기화하는 것입니다.렌더링 모델은 도메인 모델에서 나온 데이터를 유지해야 합니다.

업데이트의 경우:시뮬레이션 스레드에서 렌더 모델을 잠급니다.렌더 모델을 탐색하여 예상되는 것과 다른 부분을 업데이트하면서 렌더 모델을 업데이트합니다.탐색이 끝나면 렌더링 모델의 잠금을 해제하고 다시 그리기를 예약합니다.이 접근 방식에서는 엄청난 수의 청취자가 필요하지 않습니다.

렌더링 모델의 깊이는 다를 수 있습니다.극단적인 경우에는 이미지일 수 있으며 업데이트 작업은 단일 참조를 새 이미지 개체로 바꾸는 것뿐입니다(예를 들어 크기 조정이나 기타 피상적인 상호 작용은 잘 처리되지 않습니다).항목에 변경 사항이 있는지 확인하지 않고 모든 항목을 업데이트할 수도 있습니다.

게임 상태 변경이 빠른 경우(무엇으로 변경해야 하는지 알고 나면) 게임 상태를 다른 Swing 모델처럼 처리하고 EDT에서만 상태를 변경하거나 볼 수 있습니다.게임 상태 변경이 빠르지 않은 경우 상태 변경을 동기화하고 스윙 작업자/타이머(EDT 아님)에서 수행하거나 EDT와 유사하게 처리하는 별도의 스레드에서 수행할 수 있습니다(이 시점에서 사용하는 것을보세요 BlockingQueue 변경 요청을 처리하기 위해).마지막 방법은 UI가 게임 상태에서 정보를 검색할 필요가 없고 대신 리스너나 관찰자를 통해 렌더링 변경 사항이 전송되는 경우 더 유용합니다.

게임 상태를 점진적으로 업데이트하면서도 일관된 모델을 유지하는 것이 가능합니까?예를 들어 렌더링/사용자 업데이트 사이에 행성/플레이어/함대 개체의 하위 집합을 다시 계산합니다.

그렇다면 EDT가 사용자 입력을 처리하고 렌더링하도록 허용하기 전에 상태의 작은 부분만 계산하는 EDT에서 증분 업데이트를 실행할 수 있습니다.

EDT의 각 증분 업데이트 후에는 업데이트해야 할 모델의 양을 기억하고 보류 중인 사용자 입력 및 렌더링이 수행된 후 이 처리를 계속하도록 EDT에 새 SwingWorker를 예약해야 합니다.

이를 통해 사용자 상호 작용의 반응성을 유지하면서 게임 모델을 복사하거나 잠그는 것을 방지할 수 있습니다.

나는 World가 데이터를 저장하거나 객체 자체를 변경하도록 해서는 안 된다고 생각합니다. 이는 객체에 대한 참조를 유지하는 데에만 사용해야 하며 해당 객체를 변경해야 할 경우 변경을 수행하는 플레이어가 직접 변경하도록 해야 합니다.이 이벤트에서 해야 할 유일한 일은 플레이어가 변경을 할 때 다른 플레이어가 변경할 수 없도록 게임 세계의 각 개체를 동기화하는 것입니다.내가 생각하는 것의 예는 다음과 같습니다.

플레이어 A는 행성에 대해 알아야 하므로 월드에 해당 행성을 요청합니다(구현에 따라 어떻게 달라집니다).World는 플레이어 A가 요청한 Planet 개체에 대한 참조를 반환합니다.플레이어 A는 변경하기로 결정하고 변경합니다.건물을 추가한다고 가정 해 보겠습니다.행성에 건물을 추가하는 방법은 동기화되어 한 번에 한 명의 플레이어만 추가할 수 있습니다.건물은 자체 건설 시간(있는 경우)을 추적하므로 행성의 추가 건설 방법이 거의 즉시 해제됩니다.이렇게 하면 여러 플레이어가 서로 영향을 주지 않고 동시에 같은 행성에 대한 정보를 요청할 수 있으며 플레이어는 지연 현상 없이 거의 동시에 건물을 추가할 수 있습니다.두 명의 플레이어가 건물을 놓을 장소를 찾고 있는 경우(그것이 게임의 일부인 경우) 위치의 적합성을 확인하는 것은 변경이 아니라 질문이 될 것입니다.

이것이 귀하의 질문에 대한 답변이 아니라면 죄송합니다. 제가 올바르게 이해했는지 잘 모르겠습니다.

파이프 및 필터 아키텍처를 구현하는 것은 어떻습니까?파이프는 필터를 함께 연결하고 필터가 충분히 빠르지 않은 경우 요청을 대기열에 넣습니다.처리는 필터 내부에서 발생합니다.첫 번째 필터는 AI 엔진이고 렌더링 엔진은 일련의 후속 필터에 의해 구현됩니다.

모든 타이머 틱에서 새로운 동적 세계 상태는 모든 입력(시간도 입력임)과 복사 첫 번째 파이프에 삽입됩니다.

가장 간단한 경우 렌더링 엔진은 단일 필터로 구현됩니다.입력 파이프에서 상태 스냅샷을 가져와 정적 상태와 함께 렌더링합니다.라이브 게임에서 렌더링 엔진은 파이프에 상태가 두 개 이상인 경우 상태를 건너뛰고 벤치마크를 수행하거나 비디오를 출력하는 경우 모든 상태를 렌더링하려고 할 수 있습니다.

렌더링 엔진을 분해할 수 있는 필터가 많을수록 병렬 처리가 더 좋아집니다.어쩌면 AI 엔진을 분해하는 것도 가능할 수도 있습니다.동적 상태를 빠르게 변경되는 상태와 느리게 변경되는 상태로 분리할 수 있습니다.

이 아키텍처는 많은 동기화 없이도 우수한 병렬성을 제공합니다.

이 아키텍처의 문제점은 가비지 수집이 매번 모든 스레드를 자주 정지시켜 멀티스레딩에서 얻은 이점을 없앨 수 있다는 것입니다.

모델에 대한 업데이트를 적용하려면 우선순위 대기열이 필요한 것 같습니다. 여기서 사용자의 업데이트는 시뮬레이션 및 기타 입력의 업데이트보다 우선순위가 높습니다.내가 듣는 말은 사용자가 자신의 행동에 대해 항상 즉각적인 피드백이 필요한 반면 다른 입력(시뮬레이션 등)에는 하나의 시뮬레이션 단계보다 오래 걸릴 수 있는 작업자가 있을 수 있다는 것입니다.그런 다음 우선 순위 대기열에서 동기화하십시오.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top