From 9be6b1759370c5ee5bc450b316cb0465c287e36e Mon Sep 17 00:00:00 2001 From: wfz <1040079213@qq.com> Date: Wed, 15 Apr 2026 23:59:43 +0800 Subject: [PATCH] 1 --- backend/mvnw.cmd | 61 ---- .../java/com/zombie/game/model/GameMap.java | 123 ++++++++ .../java/com/zombie/game/model/GameWorld.java | 37 ++- .../java/com/zombie/game/model/Zombie.java | 267 +++++++++++++++--- 4 files changed, 370 insertions(+), 118 deletions(-) delete mode 100644 backend/mvnw.cmd diff --git a/backend/mvnw.cmd b/backend/mvnw.cmd deleted file mode 100644 index 5735015..0000000 --- a/backend/mvnw.cmd +++ /dev/null @@ -1,61 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.2.0 -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $proxy=''; [Net.WebRequest]::DefaultWebProxy.Credentials=[Net.CredentialCache]::DefaultCredentials; if('%MVNW_USERNAME%' -ne '' -and '%MVNW_PASSWORD%' -ne ''){$proxy=[Net.WebRequest]::GetSystemWebProxy(); $proxy.Credentials=[Net.NetworkCredential]::new('%MVNW_USERNAME%','%MVNW_PASSWORD%');} try{Invoke-WebRequest -Uri ('https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar') -Proxy $proxy -ProxyUseDefaultCredentials -OutFile ('%__MVNW_JAR__%')}catch{Write-Error $_}}" 2>&1`) DO @( - IF "%%A"=="MVNW_ERROR" ( - SET __MVNW_ERROR__=%%B - ) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% - -@SET MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -@IF NOT "%MAVEN_PROJECTBASEDIR%"=="" GOTO endDetectBaseDir -@SET MAVEN_PROJECTBASEDIR=%~dp0.. -:endDetectBaseDir - -@IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" ( - powershell -noprofile "& {Invoke-WebRequest -Uri 'https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar' -OutFile '%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar'}" -) - -@SET MAVEN_CMD_LINE_ARGS=%* - -@REM Find Java -@SET JAVA_EXE=java.exe -@IF DEFINED JAVA_HOME ( - @SET JAVA_HOME=%JAVA_HOME:"=% - @SET JAVA_EXE=%JAVA_HOME%/bin/java.exe -) - -@REM Execute Maven wrapper -"%JAVA_EXE%" ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath "%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - org.apache.maven.wrapper.MavenWrapperMain %MAVEN_CMD_LINE_ARGS% diff --git a/backend/src/main/java/com/zombie/game/model/GameMap.java b/backend/src/main/java/com/zombie/game/model/GameMap.java index eb9a5da..50c9832 100644 --- a/backend/src/main/java/com/zombie/game/model/GameMap.java +++ b/backend/src/main/java/com/zombie/game/model/GameMap.java @@ -6,11 +6,19 @@ public class GameMap { private final int[][] cells; private final int width; private final int height; + private float[][] distanceField; + private float[][] flowFieldX; + private float[][] flowFieldY; + private boolean flowFieldValid; public GameMap() { this.width = Constants.GRID_SIZE; this.height = Constants.GRID_SIZE; this.cells = new int[height][width]; + this.distanceField = new float[height][width]; + this.flowFieldX = new float[height][width]; + this.flowFieldY = new float[height][width]; + this.flowFieldValid = false; generateDefaultMap(); } @@ -132,6 +140,121 @@ public class GameMap { return points; } + public void updateFlowField(List playerPositions) { + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + distanceField[y][x] = Float.MAX_VALUE; + } + } + + Queue queue = new LinkedList<>(); + for (float[] pos : playerPositions) { + int px = (int) Math.floor(pos[0]); + int py = (int) Math.floor(pos[1]); + if (px >= 0 && px < width && py >= 0 && py < height && !isWall(px, py)) { + distanceField[py][px] = 0; + queue.add(new int[]{px, py}); + } + } + + int[][] dirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}}; + + while (!queue.isEmpty()) { + int[] current = queue.poll(); + int cx = current[0]; + int cy = current[1]; + float currentDist = distanceField[cy][cx]; + + for (int[] dir : dirs) { + int nx = cx + dir[0]; + int ny = cy + dir[1]; + + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + if (isWall(nx, ny)) continue; + + if (dir[0] != 0 && dir[1] != 0) { + if (isWall(cx + dir[0], cy) || isWall(cx, cy + dir[1])) { + continue; + } + } + + float moveCost = (dir[0] != 0 && dir[1] != 0) ? 1.414f : 1.0f; + float newDist = currentDist + moveCost; + + if (newDist < distanceField[ny][nx]) { + distanceField[ny][nx] = newDist; + queue.add(new int[]{nx, ny}); + } + } + } + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (isWall(x, y) || distanceField[y][x] == Float.MAX_VALUE) { + flowFieldX[y][x] = 0; + flowFieldY[y][x] = 0; + continue; + } + + float bestDist = distanceField[y][x]; + float bestDirX = 0; + float bestDirY = 0; + + for (int[] dir : dirs) { + int nx = x + dir[0]; + int ny = y + dir[1]; + + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + if (isWall(nx, ny)) continue; + + if (dir[0] != 0 && dir[1] != 0) { + if (isWall(x + dir[0], y) || isWall(x, y + dir[1])) { + continue; + } + } + + if (distanceField[ny][nx] < bestDist) { + bestDist = distanceField[ny][nx]; + float len = (float) Math.sqrt(dir[0] * dir[0] + dir[1] * dir[1]); + bestDirX = dir[0] / len; + bestDirY = dir[1] / len; + } + } + + flowFieldX[y][x] = bestDirX; + flowFieldY[y][x] = bestDirY; + } + } + + flowFieldValid = true; + } + + public float[] getFlowDirection(float wx, float wy) { + int gx = (int) Math.floor(wx); + int gy = (int) Math.floor(wy); + + if (gx < 0 || gx >= width || gy < 0 || gy >= height) { + return new float[]{0, 0}; + } + + return new float[]{flowFieldX[gy][gx], flowFieldY[gy][gx]}; + } + + public boolean isFlowFieldValid() { + return flowFieldValid; + } + + public float getDistance(float wx, float wy) { + int gx = (int) Math.floor(wx); + int gy = (int) Math.floor(wy); + + if (gx < 0 || gx >= width || gy < 0 || gy >= height) { + return Float.MAX_VALUE; + } + + return distanceField[gy][gx]; + } + public List findPath(float startX, float startY, float endX, float endY) { int sgx = (int) Math.floor(startX); int sgy = (int) Math.floor(startY); diff --git a/backend/src/main/java/com/zombie/game/model/GameWorld.java b/backend/src/main/java/com/zombie/game/model/GameWorld.java index 4a7a9b4..0458416 100644 --- a/backend/src/main/java/com/zombie/game/model/GameWorld.java +++ b/backend/src/main/java/com/zombie/game/model/GameWorld.java @@ -82,6 +82,8 @@ public class GameWorld { spawnTimer += dt; difficultyTimer += dt; + updateFlowField(); + if (difficultyTimer >= Constants.DIFFICULTY_INCREASE_INTERVAL) { difficultyTimer -= Constants.DIFFICULTY_INCREASE_INTERVAL; waveNumber++; @@ -103,15 +105,24 @@ public class GameWorld { checkLootCollection(); } + private void updateFlowField() { + List playerPositions = new ArrayList<>(); + for (Player p : players.values()) { + if (p.isAlive()) { + playerPositions.add(new float[]{p.getX(), p.getY()}); + } + } + if (!playerPositions.isEmpty()) { + map.updateFlowField(playerPositions); + } + } + private void spawnZombie() { List spawnPoints = map.getZombieSpawnPoints(); int[] sp = spawnPoints.get(random.nextInt(spawnPoints.size())); float wx = sp[0] + 0.5f; float wy = sp[1] + 0.5f; - Player nearest = findNearestPlayer(wx, wy); - if (nearest == null) return; - boolean isElite = random.nextFloat() < Constants.ELITE_ZOMBIE_SPAWN_CHANCE; Zombie zombie; if (isElite) { @@ -119,7 +130,6 @@ public class GameWorld { } else { zombie = new Zombie(nextZombieId++, wx, wy, zombieHealth, zombieSpeed, false); } - zombie.updatePath(map, nearest.getX(), nearest.getY()); zombies.put(zombie.getId(), zombie); } @@ -139,21 +149,20 @@ public class GameWorld { private void updateZombies(float dt) { long now = System.currentTimeMillis(); - for (Zombie z : new ArrayList<>(zombies.values())) { + + List sortedZombies = new ArrayList<>(zombies.values()); + sortedZombies.sort((a, b) -> Integer.compare(a.getId(), b.getId())); + + for (Zombie z : sortedZombies) { if (!z.isAlive()) { onZombieKilled(z); zombies.remove(z.getId()); continue; } - Player nearest = findNearestPlayer(z.getX(), z.getY()); - if (nearest != null) { - if (z.getPath() == null || z.getPathIndex() >= (z.getPath() != null ? z.getPath().size() : 0) - || random.nextFloat() < 0.02f) { - z.updatePath(map, nearest.getX(), nearest.getY()); - } - - if (z.isElite()) { + if (z.isElite()) { + Player nearest = findNearestPlayer(z.getX(), z.getY()); + if (nearest != null) { float dist = z.distanceTo(nearest.getX(), nearest.getY()); if (dist <= Constants.ELITE_ZOMBIE_ATTACK_RANGE && z.canRangedAttack(now)) { fireZombieBullet(z, nearest); @@ -162,7 +171,7 @@ public class GameWorld { } } - z.move(map, dt); + z.move(map, dt, zombies.values()); } } diff --git a/backend/src/main/java/com/zombie/game/model/Zombie.java b/backend/src/main/java/com/zombie/game/model/Zombie.java index 0befa37..d6bdf0d 100644 --- a/backend/src/main/java/com/zombie/game/model/Zombie.java +++ b/backend/src/main/java/com/zombie/game/model/Zombie.java @@ -10,11 +10,12 @@ public class Zombie { private float maxHealth; private float speed; private long lastAttackTime; - private List path; - private int pathIndex; - private float targetX, targetY; private boolean isElite; private long lastRangedAttackTime; + private float targetX, targetY; + private boolean hasTarget; + private int reservedGridX, reservedGridY; + private boolean hasReservation; public Zombie(int id, float x, float y, float health, float speed) { this(id, x, y, health, speed, false); @@ -29,10 +30,14 @@ public class Zombie { this.maxHealth = health; this.speed = speed; this.lastAttackTime = 0; - this.path = null; - this.pathIndex = 0; this.isElite = isElite; this.lastRangedAttackTime = 0; + this.targetX = 0; + this.targetY = 0; + this.hasTarget = false; + this.reservedGridX = -1; + this.reservedGridY = -1; + this.hasReservation = false; } public int getId() { return id; } @@ -41,9 +46,10 @@ public class Zombie { public float getAngle() { return angle; } public float getHealth() { return health; } public float getMaxHealth() { return maxHealth; } - public List getPath() { return path; } - public int getPathIndex() { return pathIndex; } public boolean isElite() { return isElite; } + public int getReservedGridX() { return reservedGridX; } + public int getReservedGridY() { return reservedGridY; } + public boolean hasReservation() { return hasReservation; } public void takeDamage(float damage) { this.health -= damage; @@ -54,43 +60,218 @@ public class Zombie { return health > 0; } - public void updatePath(GameMap map, float targetX, float targetY) { - this.targetX = targetX; - this.targetY = targetY; - this.path = map.findPath(x, y, targetX, targetY); - this.pathIndex = 0; - } - - public void move(GameMap map, float dt) { - if (path != null && pathIndex < path.size()) { - float[] target = path.get(pathIndex); - float dx = target[0] - x; - float dy = target[1] - y; - float dist = (float) Math.sqrt(dx * dx + dy * dy); - - if (dist < 0.2f) { - pathIndex++; - if (pathIndex >= path.size()) { - path = null; - } - return; - } - - float moveX = (dx / dist) * speed * dt; - float moveY = (dy / dist) * speed * dt; - - float newX = x + moveX; - float newY = y + moveY; - - if (map.isWalkable(newX, y, Constants.ZOMBIE_SIZE)) { - x = newX; - } - if (map.isWalkable(x, newY, Constants.ZOMBIE_SIZE)) { - y = newY; - } - - angle = (float) Math.atan2(dx, dy); + public void move(GameMap map, float dt, Collection otherZombies) { + if (!map.isFlowFieldValid()) return; + + int currentGridX = (int) Math.floor(x); + int currentGridY = (int) Math.floor(y); + + float centerDist = Float.MAX_VALUE; + if (hasTarget) { + float dx = targetX - x; + float dy = targetY - y; + centerDist = (float) Math.sqrt(dx * dx + dy * dy); } + + if (!hasTarget || centerDist < 0.15f) { + float[] flowDir = map.getFlowDirection(x, y); + float dirX = flowDir[0]; + float dirY = flowDir[1]; + + if (dirX == 0 && dirY == 0) return; + + float len = (float) Math.sqrt(dirX * dirX + dirY * dirY); + if (len > 0) { + dirX /= len; + dirY /= len; + } + + int nextGridX = currentGridX + (int) Math.round(dirX); + int nextGridY = currentGridY + (int) Math.round(dirY); + + if (map.isWall(nextGridX, nextGridY)) { + nextGridX = currentGridX + (int) Math.signum(dirX); + nextGridY = currentGridY + (int) Math.signum(dirY); + + if (map.isWall(nextGridX, nextGridY)) { + if (!map.isWall(currentGridX + (int) Math.signum(dirX), currentGridY)) { + nextGridX = currentGridX + (int) Math.signum(dirX); + nextGridY = currentGridY; + } else if (!map.isWall(currentGridX, currentGridY + (int) Math.signum(dirY))) { + nextGridX = currentGridX; + nextGridY = currentGridY + (int) Math.signum(dirY); + } else { + hasReservation = false; + return; + } + } + } + + if (isGridOccupiedOrReserved(nextGridX, nextGridY, otherZombies)) { + int[] altDirs = findAlternativeDirection(currentGridX, currentGridY, dirX, dirY, map, otherZombies); + if (altDirs != null) { + nextGridX = altDirs[0]; + nextGridY = altDirs[1]; + } else { + hasReservation = false; + return; + } + } + + reservedGridX = nextGridX; + reservedGridY = nextGridY; + hasReservation = true; + + targetX = nextGridX + 0.5f; + targetY = nextGridY + 0.5f; + hasTarget = true; + } + + float dx = targetX - x; + float dy = targetY - y; + float dist = (float) Math.sqrt(dx * dx + dy * dy); + + if (dist < 0.01f) { + hasTarget = false; + return; + } + + float dirX = dx / dist; + float dirY = dy / dist; + + float moveX = dirX * speed * dt; + float moveY = dirY * speed * dt; + + float newX = x + moveX; + float newY = y + moveY; + + boolean canMoveX = map.isWalkable(newX, y, Constants.ZOMBIE_SIZE); + boolean canMoveY = map.isWalkable(x, newY, Constants.ZOMBIE_SIZE); + boolean canMoveDiagonal = map.isWalkable(newX, newY, Constants.ZOMBIE_SIZE); + + if (moveX != 0 && moveY != 0) { + int checkX = (int) Math.floor(newX); + int checkY = (int) Math.floor(newY); + int checkCurrentX = (int) Math.floor(x); + int checkCurrentY = (int) Math.floor(y); + + boolean blockedByCorner = false; + if (checkX != checkCurrentX && checkY != checkCurrentY) { + boolean wallInX = map.isWall(checkX, checkCurrentY); + boolean wallInY = map.isWall(checkCurrentX, checkY); + + if (wallInX || wallInY) { + blockedByCorner = true; + + if (!wallInX && canMoveX) { + x = newX; + canMoveY = map.isWalkable(x, newY, Constants.ZOMBIE_SIZE); + } else if (!wallInY && canMoveY) { + y = newY; + canMoveX = map.isWalkable(newX, y, Constants.ZOMBIE_SIZE); + } + } + } + + if (!blockedByCorner && canMoveDiagonal) { + x = newX; + y = newY; + } else if (!blockedByCorner) { + if (canMoveX) x = newX; + if (canMoveY) y = newY; + } + } else { + if (canMoveX) x = newX; + if (canMoveY) y = newY; + } + + float minSeparationDist = Constants.ZOMBIE_SIZE; + for (Zombie other : otherZombies) { + if (other.getId() == this.id) continue; + if (!other.isAlive()) continue; + + float ox = other.getX(); + float oy = other.getY(); + float sepDx = x - ox; + float sepDy = y - oy; + float sepDist = (float) Math.sqrt(sepDx * sepDx + sepDy * sepDy); + + if (sepDist < minSeparationDist && sepDist > 0.01f) { + float overlap = minSeparationDist - sepDist; + float pushX = (sepDx / sepDist) * overlap * 0.5f; + float pushY = (sepDy / sepDist) * overlap * 0.5f; + + float pushedX = x + pushX; + float pushedY = y + pushY; + + if (map.isWalkable(pushedX, pushedY, Constants.ZOMBIE_SIZE)) { + x = pushedX; + y = pushedY; + } else { + if (map.isWalkable(x + pushX, y, Constants.ZOMBIE_SIZE)) { + x = x + pushX; + } + if (map.isWalkable(x, y + pushY, Constants.ZOMBIE_SIZE)) { + y = y + pushY; + } + } + } + } + + if (dirX != 0 || dirY != 0) { + angle = (float) Math.atan2(dirX, dirY); + } + } + + private boolean isGridOccupiedOrReserved(int gridX, int gridY, Collection otherZombies) { + for (Zombie other : otherZombies) { + if (other.getId() == this.id) continue; + if (!other.isAlive()) continue; + + int otherGridX = (int) Math.floor(other.getX()); + int otherGridY = (int) Math.floor(other.getY()); + + if (otherGridX == gridX && otherGridY == gridY) { + return true; + } + + if (other.hasReservation() && other.getReservedGridX() == gridX && other.getReservedGridY() == gridY) { + return true; + } + } + return false; + } + + private int[] findAlternativeDirection(int currentGridX, int currentGridY, float dirX, float dirY, + GameMap map, Collection otherZombies) { + int[][] allDirs = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}, {-1, -1}, {1, -1}, {-1, 1}, {1, 1}}; + + java.util.List candidates = new java.util.ArrayList<>(); + + for (int[] dir : allDirs) { + int nx = currentGridX + dir[0]; + int ny = currentGridY + dir[1]; + + if (map.isWall(nx, ny)) continue; + + if (dir[0] != 0 && dir[1] != 0) { + if (map.isWall(currentGridX + dir[0], currentGridY) || + map.isWall(currentGridX, currentGridY + dir[1])) { + continue; + } + } + + if (isGridOccupiedOrReserved(nx, ny, otherZombies)) continue; + + float dotProduct = dir[0] * dirX + dir[1] * dirY; + candidates.add(new int[]{nx, ny, (int) (dotProduct * 1000)}); + } + + if (candidates.isEmpty()) return null; + + candidates.sort((a, b) -> b[2] - a[2]); + + return new int[]{candidates.get(0)[0], candidates.get(0)[1]}; } public boolean canAttack(long now) {