This commit is contained in:
2025-09-26 09:31:46 +08:00
parent f5d5939f29
commit 4f486b367f
64 changed files with 758 additions and 153 deletions

12
docs/perf_hotspots.md Normal file
View File

@@ -0,0 +1,12 @@
# 性能热点初稿
首轮标记:
| 区域 | 说明 | 优先级 | 说明 |
|------|------|--------|------|
GameScreen.render | 多次集合遍历 + 远程玩家循环内更新 | 高 | 后续拆分逻辑/渲染阶段 |
SimpleFighter.update | 物理与攻击状态混杂 | 中 | 拆分到组件式(动作/物理) |
网络同步(待补) | sendPosition 每帧发送 | 中 | 引入位置压缩/频率限制 |
清屏/批处理 | 现已静态化 | 已改善 | 继续合并渲染批次 |
后续收集: 帧时间分布 / GC 次数。

125
pom.xml
View File

@@ -14,71 +14,87 @@
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gdx.version>1.12.1</gdx.version>
</properties>
<dependencies>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>1.12.1</version> <!-- 替换为你的 LibGDX 版本 -->
<!-- Core LibGDX -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>${gdx.version}</version>
</dependency>
<!-- LWJGL2 Desktop Backend (使用 LwjglApplication 而不是 Lwjgl3Application) -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl</artifactId>
<version>${gdx.version}</version>
</dependency>
<!-- 平台原生库(桌面) -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>${gdx.version}</version>
<classifier>natives-desktop</classifier>
</dependency>
<!-- FreeType 字体扩展 -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype</artifactId>
<version>1.12.1</version> <!-- 与 LibGDX 版本保持一致 -->
</dependency>
<!-- 桌面平台的 FreeType 原生库 -->
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-freetype-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl</artifactId>
<version>1.12.1</version> <!-- 使用你的 LibGDX 版本号 -->
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.12.1</version>
<classifier>natives-desktop</classifier>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>1.12.1</version>
<artifactId>gdx-freetype</artifactId>
<version>${gdx.version}</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl3</artifactId>
<version>1.12.1</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>1.12.1</version>
<artifactId>gdx-freetype-platform</artifactId>
<version>${gdx.version}</version>
<classifier>natives-desktop</classifier>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Enforcer 防止依赖冲突 / 旧 JDK -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>enforce</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireJavaVersion>
<version>[21,)</version>
</requireJavaVersion>
<dependencyConvergence/>
</rules>
<fail>false</fail>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<!-- 建议使用与LibGDX兼容的JDK版本11或17比较合适 -->
<source>17</source>
<target>17</target>
<!-- 移除不必要的预览特性,除非确实需要 -->
<!-- 与 properties 中保持一致 -->
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<release>${maven.compiler.release}</release>
</configuration>
</plugin>
@@ -87,8 +103,6 @@
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- 确保这个类路径与你实际的Launcher类位置一致 -->
<!-- 例如如果你的类文件在src/main/java/uno/mloluyu/Launcher.java -->
<mainClass>uno.mloluyu.desktop.DesktopLauncher</mainClass>
</configuration>
</plugin>
@@ -107,6 +121,31 @@
</archive>
</configuration>
</plugin>
<!-- 运行 JUnit 5 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
<!-- 初步覆盖率,仅生成报告,不设阈值 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@@ -0,0 +1,20 @@
package uno.mloluyu.characters;
/** 提供状态切换验证。 */
public final class ActionStateGuard {
private ActionStateGuard() {
}
public static Action transition(FighterBase fighter, Action next) {
Action current = fighter.getCurrentAction();
if (current == next)
return current;
if (!ActionTransitionMap.can(current, next)) {
return current;
}
if (fighter instanceof uno.mloluyu.characters.SimpleFighter sf) {
sf.directSetAction(next);
}
return next;
}
}

View File

@@ -0,0 +1,25 @@
package uno.mloluyu.characters;
import java.util.*;
/** 定义合法动作迁移,供校验/日志使用。 */
public final class ActionTransitionMap {
private static final Map<Action, Set<Action>> ALLOWED = new EnumMap<>(Action.class);
static {
allow(Action.IDLE, Action.MOVE, Action.JUMP, Action.ATTACK, Action.DEFEND, Action.HIT, Action.DEAD);
allow(Action.MOVE, Action.IDLE, Action.JUMP, Action.ATTACK, Action.HIT, Action.DEAD);
allow(Action.JUMP, Action.HIT, Action.ATTACK, Action.DEAD, Action.IDLE, Action.MOVE);
allow(Action.ATTACK, Action.HIT, Action.DEAD, Action.IDLE, Action.MOVE);
allow(Action.DEFEND, Action.HIT, Action.IDLE, Action.MOVE, Action.DEAD);
allow(Action.HIT, Action.IDLE, Action.MOVE, Action.DEAD);
allow(Action.DEAD); // 终止状态
}
private static void allow(Action from, Action... tos) {
ALLOWED.put(from, new HashSet<>(Arrays.asList(tos)));
}
public static boolean can(Action from, Action to) {
return ALLOWED.getOrDefault(from, Collections.emptySet()).contains(to);
}
}

View File

@@ -3,14 +3,12 @@ package uno.mloluyu.characters;
public class AdvancedFighter extends SimpleFighter {
public AdvancedFighter(String name) {
super(name); // 调用父类构造函数
super(name);
}
@Override
public void attack(String attackType) {
// 先使用父类的攻击逻辑来保证 isAttacking/attackTimer/attackbox 等状态被正确设置
super.attack(attackType);
// 在这里可以添加 AdvancedFighter 特有的扩展行为(攻击力、特效等)
// 例如:根据 attackType 调整伤害或触发粒子/声音,但不要忘记保留父类的状态设置
}
}

View File

@@ -0,0 +1,47 @@
package uno.mloluyu.characters;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.graphics.Color;
import java.util.concurrent.ThreadLocalRandom;
/** 公共角色基类:后续 Simple/Advanced 统一继承。 */
public abstract class FighterBase {
protected String name;
protected Action currentAction = Action.IDLE;
protected Rectangle hitbox = new Rectangle(0, 0, 64, 128);
protected Rectangle attackbox = new Rectangle(0, 0, 80, 80);
protected final Color debugColor;
public FighterBase(String name) {
this.name = name;
ThreadLocalRandom r = ThreadLocalRandom.current();
float rr = 0.35f + r.nextFloat() * 0.65f;
float gg = 0.35f + r.nextFloat() * 0.65f;
float bb = 0.35f + r.nextFloat() * 0.65f;
this.debugColor = new Color(rr, gg, bb, 1f);
}
public Action getCurrentAction() {
return currentAction;
}
public Rectangle getHitbox() {
return hitbox;
}
public Rectangle getAttackbox() {
return attackbox;
}
public String getName() {
return name;
}
public Color getDebugColor() {
return debugColor;
}
protected void setAction(Action next) {
this.currentAction = next;
}
}

View File

@@ -10,21 +10,21 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Rectangle;
import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.util.GameConstants;
/**
* 简化版角色类,仅包含移动、攻击、受击等基础功能。
*/
public class SimpleFighter {
public class SimpleFighter extends FighterBase {
private String name; // 角色名称
private Action currentAction = Action.IDLE; // 当前动作状态
// 继承: name, currentAction, hitbox, attackbox
private float verticalSpeed = 0f; // 垂直速度(跳跃/下落)
private boolean isGrounded = true; // 是否着地
private Rectangle hitbox = new Rectangle(0, 0, 64, 128); // 碰撞盒
private Rectangle attackbox = new Rectangle(0, 0, 80, 80); // 攻击判定盒
private boolean isFacingRight = true; // 朝向(右/左)
private float speed = 300f; // 水平移动速度
private int health = 100; // 生命值
private float speed = GameConstants.MOVE_SPEED; // 水平移动速度
private int health = 200; // 生命值
private boolean isAttacking = false; // 是否正在攻击
private boolean attackJustStarted = false; // 攻击是否刚开始
private float attackTimer = 0f; // 攻击计时器
@@ -41,7 +41,7 @@ public class SimpleFighter {
private static final float KNOCKBACK_DURATION = 0.12f;
public SimpleFighter(String name) {
this.name = name;
super(name);
}
public void update(float deltaTime) {
@@ -76,10 +76,10 @@ public class SimpleFighter {
}
if (!isGrounded) {
verticalSpeed -= 2500 * deltaTime;
verticalSpeed -= GameConstants.GRAVITY * deltaTime;
hitbox.y += verticalSpeed * deltaTime;
if (hitbox.y <= 0) {
hitbox.y = 0;
if (hitbox.y <= GameConstants.GROUND_Y) {
hitbox.y = GameConstants.GROUND_Y;
verticalSpeed = 0;
isGrounded = true;
changeAction(Action.IDLE);
@@ -103,7 +103,7 @@ public class SimpleFighter {
hitbox.y + hitbox.height * 0.7f);
}
public void handleInput(int keycode, boolean isPressed) {
public void handleInput(int keycode, boolean isPressed, float duration) {
if (isPressed) {
if (keycode == Input.Keys.LEFT || keycode == Input.Keys.A) {
move(-1, Gdx.graphics.getDeltaTime());
@@ -138,12 +138,16 @@ public class SimpleFighter {
}
public void changeAction(Action newAction) {
this.currentAction = newAction;
this.currentAction = ActionStateGuard.transition(this, newAction);
}
void directSetAction(Action a) {
this.currentAction = a;
}
public void jump() {
if (isGrounded) {
verticalSpeed = 1000f;
verticalSpeed = GameConstants.JUMP_SPEED;
isGrounded = false;
changeAction(Action.JUMP);
}
@@ -181,28 +185,32 @@ public class SimpleFighter {
}
private void updateAttackbox(String attackType) {
float offsetX, offsetY = 20, width = 80, height = 80;
float baseOffsetY = 20f;
float width = 80f, height = 80f;
float offsetX;
float offsetY = baseOffsetY;
// 先决定尺寸,再根据朝向计算 offset不再用旧 attackbox.width 避免漂移)
switch (attackType) {
case "heavy":
offsetX = isFacingRight ? hitbox.width : -100;
offsetY = 40;
width = 100;
height = 100;
break;
case "light":
offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
width = 100f;
height = 100f;
offsetY = 40f;
offsetX = isFacingRight ? hitbox.width : -width; // 重击靠近身体或覆盖前方
break;
case "special":
offsetX = isFacingRight ? hitbox.width + 20 : -attackbox.width - 20;
offsetY = 50;
width = 120;
height = 60;
width = 120f;
height = 60f;
offsetY = 50f;
offsetX = isFacingRight ? hitbox.width + 20f : -width - 20f;
break;
case "light":
default:
offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
// 轻击稍微往前,不再参考旧 attackbox.width
offsetX = isFacingRight ? hitbox.width - 10f : -80f + 10f;
break;
}
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + offsetY);
attackbox.setSize(width, height);
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + offsetY);
}
public void attack(String attackType) {
@@ -304,6 +312,13 @@ public class SimpleFighter {
hitbox.setPosition(x, y);
}
/** 若当前 Y 低于地面则贴到地面。 */
public void alignToGround() {
if (hitbox.y < GameConstants.GROUND_Y) {
hitbox.y = GameConstants.GROUND_Y;
}
}
public float getAttackTimer() {
return attackTimer;
}

View File

@@ -0,0 +1,16 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.ScreenAdapter;
/** 所有 Screen 的基础类,集中生命周期钩子扩展。 */
public abstract class BaseScreen extends ScreenAdapter {
protected final MainGame game;
protected BaseScreen(MainGame game) {
this.game = game;
}
/** 提供可选的资源预加载完成回调 */
public void onAssetsReady() {
}
}

View File

@@ -33,14 +33,12 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
private SimpleFighter selectedFighter2;
private final List<Texture> bgs = Arrays.asList(
new Texture(Gdx.files.internal("src/main/resources/selectpage/10b_back_blue2p.png")),
//new Texture(Gdx.files.internal("src/main/resources/selectpage/back_door.png")),
new Texture(Gdx.files.internal("src/main/resources/selectpage/11b_back_red1p.png"))
);
new Texture(Gdx.files.internal("selectpage/10b_back_blue2p.png")),
// new Texture(Gdx.files.internal("selectpage/back_door.png")),
new Texture(Gdx.files.internal("selectpage/11b_back_red1p.png")));
private final List<Texture> charsTexts = Arrays.asList(
new Texture(Gdx.files.internal("src/main/resources/selectpage/character_03.png")),
new Texture(Gdx.files.internal("src/main/resources/selectpage/character_00.png"))
);
new Texture(Gdx.files.internal("selectpage/character_03.png")),
new Texture(Gdx.files.internal("selectpage/character_00.png")));
private final List<String> characters = Arrays.asList("Alice", "Reimu", "暂定");
private Texture profile1p = charsTexts.get(0);
private Texture profile2p = charsTexts.get(1);
@@ -49,11 +47,13 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
private static int selectedIndex = 0;
private static boolean is1P = true;
private static final int BUTTON_WIDTH = 300;
private static final int BUTTON_HEIGHT = 80;
private static final int BUTTON_X = 800;
private static final int CONFIRM_Y = 200;
private static final int BACK_Y = 100;
// 下面这些按钮常量原本用于文本/按钮绘制,当前 UI 逻辑已注释。
// 如果后续恢复 renderTexts() 可重新启用;为减少无用警告暂时注释。
// private static final int BUTTON_WIDTH = 300;
// private static final int BUTTON_HEIGHT = 80;
// private static final int BUTTON_X = 800;
// private static final int CONFIRM_Y = 200;
// private static final int BACK_Y = 100;
public CharacterSelectScreen(MainGame game) {
this.game = game;
@@ -75,13 +75,14 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
@Override
public void render(float delta) {
new ClearScreen();
// 清屏:使用工具静态方法,避免误用私有构造器
ClearScreen.clear();
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
renderBackground();
renderCharacters(multiplayerMode);
// renderTexts();
// renderTexts();
handleInput(mouseX, mouseY);
if (multiplayerMode) {
@@ -99,7 +100,7 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
private void renderBackground() {
batch.begin();
for (int i = 0; i < bgs.size(); i ++) {
for (int i = 0; i < bgs.size(); i++) {
batch.draw(bgs.get(i), 0, 528 * i, 1920, 528);
}
batch.end();
@@ -107,45 +108,47 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
private void renderCharacters(boolean multiplayerMode) {
batch.begin();
batch.draw(profile1p, 0, 0, profile1p.getWidth()*3, profile1p.getHeight()*3);
batch.draw(profile2p, 0, 528, profile2p.getWidth()*3, profile2p.getHeight()*3);
batch.draw(profile1p, 0, 0, profile1p.getWidth() * 3, profile1p.getHeight() * 3);
batch.draw(profile2p, 0, 528, profile2p.getWidth() * 3, profile2p.getHeight() * 3);
batch.end();
}
//
// private void renderTexts() {
// batch.begin();
// font.draw(batch, "选择你的角色", 200, 650);
// for (int i = 0; i < characters.size(); i++) {
// int x = 200 + 30;
// int y = 500 - i * 120 + 50;
// font.draw(batch, characters.get(i), x, y);
//
// }
// if (selectedIndex != -1) {
// font.draw(batch, "已选择: " + characters.get(selectedIndex), 200, 100);
// }
// drawButtonText(CONFIRM_Y, "确认");
// drawButtonText(BACK_Y, "返回");
// batch.end();
// }
//
//
// private void renderTexts() {
// batch.begin();
// font.draw(batch, "选择你的角色", 200, 650);
// for (int i = 0; i < characters.size(); i++) {
// int x = 200 + 30;
// int y = 500 - i * 120 + 50;
// font.draw(batch, characters.get(i), x, y);
//
// }
// if (selectedIndex != -1) {
// font.draw(batch, "已选择: " + characters.get(selectedIndex), 200, 100);
// }
// drawButtonText(CONFIRM_Y, "确认");
// drawButtonText(BACK_Y, "返回");
// batch.end();
// }
//
private void handleInput(int mouseX, int mouseY) {
if (selectedFighter1 != null && selectedFighter2 != null) {
if (multiplayerMode) {
// 设置唯一玩家 ID 并发送角色选择
if (NetworkManager.getInstance().getLocalPlayerId() == null) {
String playerId = UUID.randomUUID().toString();
NetworkManager.getInstance().setLocalPlayerId(playerId);
Gdx.app.log("Network", "设置玩家ID: " + playerId);
}
NetworkManager.getInstance().sendCharacterSelection(selectedFighter1.getName());
// 单人模式只要1P选择了角色就进入游戏
if (!multiplayerMode && selectedFighter1 != null) {
game.setScreen(new GameScreen(game, selectedFighter1));
return;
}
// 联机模式等待双方选择当前逻辑仍采用2人都选才进入
if (multiplayerMode && selectedFighter1 != null && selectedFighter2 != null) {
if (NetworkManager.getInstance().getLocalPlayerId() == null) {
String playerId = UUID.randomUUID().toString();
NetworkManager.getInstance().setLocalPlayerId(playerId);
Gdx.app.log("Network", "设置玩家ID: " + playerId);
}
NetworkManager.getInstance().sendCharacterSelection(selectedFighter1.getName());
game.setScreen(new GameScreen(game, selectedFighter1));
}
}
@Override
public void dispose() {
batch.dispose();
@@ -168,7 +171,7 @@ public class CharacterSelectScreen extends ScreenAdapter implements InputProcess
}
value = true;
} else {
//占坑说是
// 占坑说是
}
if (i == Input.Keys.Z) {
if (is1P) {

View File

@@ -3,6 +3,8 @@ package uno.mloluyu.desktop;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
// 日志功能已移除
/**
* Desktop 平台启动器
*/
@@ -10,6 +12,8 @@ public class DesktopLauncher {
public static void main(String[] args) {
// 若需要异常抓取,可在此处添加简单的 Thread.setDefaultUncaughtExceptionHandler
LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
float scale = 1.0F;

View File

@@ -5,6 +5,7 @@ import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.GL20;
import uno.mloluyu.characters.Action;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
@@ -18,6 +19,9 @@ import uno.mloluyu.characters.SimpleFighter;
import uno.mloluyu.characters.AdvancedFighter;
import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.util.ClearScreen;
import uno.mloluyu.perf.PerfMetrics;
import uno.mloluyu.util.TimeStepLimiter;
import uno.mloluyu.util.GameConstants;
import uno.mloluyu.versatile.FighterController;
public class GameScreen extends ScreenAdapter {
@@ -30,6 +34,37 @@ public class GameScreen extends ScreenAdapter {
private SpriteBatch batch;
private ShapeRenderer shapeRenderer;
private OrthographicCamera camera;
private Texture background;
// 世界尺寸(基于背景原始尺寸 * 缩放)。如果需要可读取 Texture 宽高后动态设。
private float worldWidth;
private float worldHeight;
// 背景整体缩放倍数:>1 表示背景比屏幕大,可只显示局部
private static final float BACKGROUND_SCALE = 1.5f;
// 摄像机竖直偏移(正值=镜头上移,让玩家更靠下;这里改为较小的正值防止看不到下方)
private static final float CAMERA_Y_OFFSET = 60f;
// 允许相机向下多看到的底部扩展(不被 clamp 过早挡住),解决放大 hitbox 下半部分出框
private static final float CAMERA_BOTTOM_MARGIN = -30f;
// 平滑跟随的垂直插值系数(独立控制 y防止瞬间跳
private static final float CAMERA_LERP_ALPHA = 0.12f;
// ========== 摄像机缩放配置 ==========
// 是否使用动态缩放(多人时根据距离自动拉远 / 靠近)
private static final boolean CAMERA_DYNAMIC_ZOOM = true;
// (保留)固定缩放模式开关 & 数值;若需要强制固定视角可把 CAMERA_DYNAMIC_ZOOM 设 false
private static final boolean CAMERA_USE_FIXED_ZOOM = false;
private static final float CAMERA_FIXED_ZOOM = 0.80f;
// 动态缩放参数最小与最大OrthographicCamera: <1 视角更近,>1 更远)
private static final float CAMERA_MIN_ZOOM = 0.55f; // 角色很近时
private static final float CAMERA_MAX_ZOOM = 1.25f; // 距离很远时
// 达到最大缩放所对应的“玩家距离”基准(屏幕世界单位,按你的角色移动范围调)
private static final float CAMERA_MAX_DISTANCE = 1800f;
// 缩放插值速度(越大越快贴近目标)
private static final float CAMERA_ZOOM_LERP = 0.10f;
// 显示地面参考线
private static final boolean SHOW_GROUND_LINE = true;
// 半透明地面条带显示
private static final boolean SHOW_GROUND_STRIP = true;
private static final float GROUND_STRIP_HEIGHT = 14f; // 条带厚度
private static final float GROUND_STRIP_ALPHA = 0.20f; // 透明度 (0~1)
public GameScreen(MainGame game, SimpleFighter player) {
this.player = player;
@@ -38,18 +73,33 @@ public class GameScreen extends ScreenAdapter {
@Override
public void show() {
// 确保角色初始贴地(地面抬高后老存档/默认 0 需要调整)
player.alignToGround();
camera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
camera.position.set(player.getHitbox().x, player.getHitbox().y, 0); // 初始化摄像机位置
// 初始化缩放:动态优先,否则固定
if (CAMERA_DYNAMIC_ZOOM) {
camera.zoom = CAMERA_MIN_ZOOM;
} else if (CAMERA_USE_FIXED_ZOOM) {
camera.zoom = CAMERA_FIXED_ZOOM;
}
camera.update();
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
Gdx.input.setInputProcessor(controller);
// 背景图(与主菜单共用 bg.png可后续扩展成多关卡背景
background = new Texture(Gdx.files.internal("innerbg.png"));
worldWidth = background.getWidth() * BACKGROUND_SCALE;
worldHeight = background.getHeight() * BACKGROUND_SCALE;
}
@Override
public void render(float delta) {
new ClearScreen();
delta = TimeStepLimiter.clamp(delta);
ClearScreen.clear();
PerfMetrics.frame(delta);
// (原先背景在摄像机更新前绘制,会出现一帧“滞后”错位;已移到摄像机更新后)
// 输入 / 逻辑
player.update(delta);
@@ -150,7 +200,7 @@ public class GameScreen extends ScreenAdapter {
String pid = dt.getKey();
// 重生位置简单放原点附近随机
float rx = (float) (Math.random() * 200 - 100);
float ry = 0;
float ry = GameConstants.GROUND_Y;
NetworkManager.getInstance().sendRespawn(pid, rx, ry);
deathTimers.remove(pid);
}
@@ -197,6 +247,9 @@ public class GameScreen extends ScreenAdapter {
}
}
// 本地与远程玩家之间简单碰撞(防穿人)——放在摄像机更新前
resolvePlayerCollisions();
// 摄像机跟随
// 摄像头跟随:若有一个远程玩家,则居中于本地和远程玩家中点
Vector3 targetPos;
@@ -204,32 +257,87 @@ public class GameScreen extends ScreenAdapter {
SimpleFighter remote = otherPlayers.values().iterator().next();
float midX = (player.getHitbox().x + remote.getHitbox().x) * 0.5f;
float midY = (player.getHitbox().y + remote.getHitbox().y) * 0.5f;
targetPos = new Vector3(midX, midY, 0);
targetPos = new Vector3(midX, midY + CAMERA_Y_OFFSET, 0);
} else {
// 默认为跟随本地玩家
targetPos = new Vector3(player.getHitbox().x, player.getHitbox().y, 0);
targetPos = new Vector3(player.getHitbox().x, player.getHitbox().y + CAMERA_Y_OFFSET, 0);
}
camera.position.lerp(targetPos, 0.1f);
// 仅对 x,y 做分量插值,可单独调节速度
camera.position.x += (targetPos.x - camera.position.x) * CAMERA_LERP_ALPHA;
camera.position.y += (targetPos.y - camera.position.y) * (CAMERA_LERP_ALPHA * 1.1f);
// 计算动态缩放目标
if (CAMERA_DYNAMIC_ZOOM && otherPlayers.size() >= 1) {
// 取所有玩家(本地 + 远程x 坐标的最大跨度作为距离依据,可扩展为对角线距离
float minX = player.getHitbox().x;
float maxX = player.getHitbox().x;
float minY = player.getHitbox().y;
float maxY = player.getHitbox().y;
for (SimpleFighter r : otherPlayers.values()) {
Rectangle hb = r.getHitbox();
if (hb.x < minX)
minX = hb.x;
if (hb.x > maxX)
maxX = hb.x;
if (hb.y < minY)
minY = hb.y;
if (hb.y > maxY)
maxY = hb.y;
}
float dx = maxX - minX;
float dy = maxY - minY;
// 这里主要横向对战,优先 dx若想考虑纵向可用距离 = max(dx, dy*系数)
float dist = Math.max(dx, dy * 0.6f);
float t = Math.min(1f, dist / CAMERA_MAX_DISTANCE); // 0~1
float targetZoom = CAMERA_MIN_ZOOM + (CAMERA_MAX_ZOOM - CAMERA_MIN_ZOOM) * t;
camera.zoom += (targetZoom - camera.zoom) * CAMERA_ZOOM_LERP;
} else if (CAMERA_USE_FIXED_ZOOM) {
// 固定缩放平滑
if (Math.abs(camera.zoom - CAMERA_FIXED_ZOOM) > 0.0001f) {
camera.zoom += (CAMERA_FIXED_ZOOM - camera.zoom) * 0.25f;
}
}
// 约束摄像机在世界边界内(视口以中心为基准)
float halfW = (camera.viewportWidth * camera.zoom) / 2f;
float halfH = (camera.viewportHeight * camera.zoom) / 2f;
camera.position.x = Math.max(halfW, Math.min(worldWidth - halfW, camera.position.x));
float minY = halfH - CAMERA_BOTTOM_MARGIN; // 允许比世界底部再低一些显示底部区域
camera.position.y = Math.max(minY, Math.min(worldHeight - halfH, camera.position.y));
camera.update();
batch.setProjectionMatrix(camera.combined);
shapeRenderer.setProjectionMatrix(camera.combined);
// -------- Background pass --------
batch.begin();
// 仅绘制背景的局部:通过在更大的缩放空间中直接拉伸整张图并限制摄像机
// 若想真正裁剪一部分,可改用纹理区域;此处使用整图放大后让摄像机在其内游走
batch.draw(background,
0, 0,
worldWidth, worldHeight);
batch.end();
// 混合
Gdx.gl.glEnable(GL20.GL_BLEND);
Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
// -------- Filled pass --------
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
drawHitbox(player, Color.BLUE);
// 半透明地面条带(先于 hitbox 绘制,这样人物在其上方)
if (SHOW_GROUND_STRIP) {
float y = GameConstants.GROUND_Y - GROUND_STRIP_HEIGHT * 0.5f; // 居中穿过地面线
shapeRenderer.setColor(0.9f, 0.9f, 0.9f, GROUND_STRIP_ALPHA); // 近白轻薄层
shapeRenderer.rect(0f, y, worldWidth, GROUND_STRIP_HEIGHT);
}
drawHitbox(player, player.getDebugColor());
boolean showPlayerAttack = player.isAttacking()
|| (player.getCurrentAction() == Action.ATTACK && player.getAttackTimer() > 0);
if (showPlayerAttack)
drawAttackBox(player, 1f, 0f, 0f, 0.35f);
// 攻击框颜色改为蓝色(原红色)
drawAttackBox(player, 0.0f, 0.45f, 1f, 0.35f);
for (SimpleFighter remote : otherPlayers.values()) {
drawHitbox(remote, Color.GREEN);
drawHitbox(remote, remote.getDebugColor());
if (remote.isAttacking())
drawAttackBox(remote, 1f, 0f, 0f, 0.25f);
drawAttackBox(remote, 0.0f, 0.45f, 1f, 0.25f);
}
shapeRenderer.end();
@@ -240,11 +348,13 @@ public class GameScreen extends ScreenAdapter {
// -------- Debug line pass --------
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
if (SHOW_GROUND_LINE) {
shapeRenderer.setColor(0.65f, 0.65f, 0.65f, 1f); // 浅灰色参考线
shapeRenderer.line(0f, GameConstants.GROUND_Y, worldWidth, GameConstants.GROUND_Y);
}
player.renderDebug(shapeRenderer);
for (SimpleFighter remote : otherPlayers.values())
remote.renderDebug(shapeRenderer);
shapeRenderer.setColor(Color.WHITE);
shapeRenderer.rect(0, 0, 1000, 1000);
shapeRenderer.end();
// -------- UI health bar pass --------
// 使用屏幕坐标绘制血条
@@ -274,18 +384,45 @@ public class GameScreen extends ScreenAdapter {
idx++;
}
shapeRenderer.end();
// 原定期性能日志已移除(日志系统删除)
}
private void drawHitbox(SimpleFighter fighter, Color color) {
shapeRenderer.setColor(color);
Rectangle r = fighter.getHitbox();
shapeRenderer.rect(r.x, r.y, r.width, r.height);
float scale = GameConstants.DEBUG_BOX_SCALE;
if (scale <= 1.0001f && scale >= 0.9999f) { // 视为 1
shapeRenderer.rect(r.x, r.y, r.width, r.height);
return;
}
float cx = r.x + r.width / 2f;
float cy = r.y + r.height / 2f;
float w = r.width * scale;
float h = r.height * scale;
if (GameConstants.DEBUG_SCALE_FROM_CENTER) {
shapeRenderer.rect(cx - w / 2f, cy - h / 2f, w, h);
} else {
shapeRenderer.rect(r.x, r.y, w, h);
}
}
private void drawAttackBox(SimpleFighter fighter, float r, float g, float b, float a) {
shapeRenderer.setColor(r, g, b, a);
Rectangle box = fighter.getAttackbox();
shapeRenderer.rect(box.x, box.y, box.width, box.height);
float scale = GameConstants.DEBUG_BOX_SCALE;
if (scale <= 1.0001f && scale >= 0.9999f) {
shapeRenderer.rect(box.x, box.y, box.width, box.height);
return;
}
float cx = box.x + box.width / 2f;
float cy = box.y + box.height / 2f;
float w = box.width * scale;
float h = box.height * scale;
if (GameConstants.DEBUG_SCALE_FROM_CENTER) {
shapeRenderer.rect(cx - w / 2f, cy - h / 2f, w, h);
} else {
shapeRenderer.rect(box.x, box.y, w, h);
}
}
// private void checkPlayerAttacks() {
@@ -303,6 +440,41 @@ public class GameScreen extends ScreenAdapter {
public void dispose() {
batch.dispose();
shapeRenderer.dispose();
if (background != null)
background.dispose();
NetworkManager.getInstance().disconnect();
}
/**
* 解决本地玩家与每个远程玩家的水平重叠,避免“穿人”视觉效果。
* 当前策略:仅移动本地玩家(不修改远程玩家坐标,避免产生需要网络回传的状态)。
* 若需要更严格的对等碰撞,可在主机端双向分离并广播,但此处先满足基本需求。
*/
private void resolvePlayerCollisions() {
if (otherPlayers.isEmpty())
return;
Rectangle a = player.getHitbox();
for (SimpleFighter remote : otherPlayers.values()) {
Rectangle b = remote.getHitbox();
if (!a.overlaps(b))
continue;
// 仅考虑水平最小位移分离2D 侧向格斗常见做法)
float axCenter = a.x + a.width / 2f;
float bxCenter = b.x + b.width / 2f;
float dx = axCenter - bxCenter; // 正值:本地在右侧
float overlapX = (a.width + b.width) / 2f - Math.abs(dx);
if (overlapX > 0) {
if (dx >= 0) {
a.x += overlapX; // 本地向右推
} else {
a.x -= overlapX; // 本地向左推
}
// 世界边界限制(假设世界从 0 开始到 worldWidth
if (a.x < 0)
a.x = 0;
if (a.x + a.width > worldWidth)
a.x = worldWidth - a.width;
}
}
}
}

View File

@@ -1,8 +1,6 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
public class MainGame extends Game {
public static final float WORLD_WIDTH = 1920;

View File

@@ -9,8 +9,6 @@ import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import uno.mloluyu.versatile.FighterController;
import static uno.mloluyu.util.Font.loadChineseFont;
public class MainMenuScreen extends ScreenAdapter {
@@ -32,7 +30,7 @@ public class MainMenuScreen extends ScreenAdapter {
public MainMenuScreen(MainGame game) {
this.game = game;
texture = new Texture(Gdx.files.internal("src\\main\\resources\\bg.png"));
texture = new Texture(Gdx.files.internal("bg.png"));
}
@Override
@@ -46,7 +44,7 @@ public class MainMenuScreen extends ScreenAdapter {
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0,0,0,0);
Gdx.gl.glClearColor(0, 0, 0, 0);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
int mouseX = Gdx.input.getX();
@@ -76,6 +74,7 @@ public class MainMenuScreen extends ScreenAdapter {
game.setScreen(new CharacterSelectScreen(game));
} else if (isTouched(mouseX, mouseY, buttonX, settingsY)) {
Gdx.app.log("Button", "设置按钮被点击!");
game.setScreen(new SettingsScreen(game));
} else if (isTouched(mouseX, mouseY, buttonX, networkY)) {
Gdx.app.log("Button", "联网设置按钮被点击!");
game.setScreen(new NetworkSettingsScreen(game));
@@ -92,8 +91,7 @@ public class MainMenuScreen extends ScreenAdapter {
}
private void drawButtonText(int y, String text) {
float textWidth = font.getRegion().getRegionWidth(); // 粗略估算
float textX = buttonX + buttonWidth / 2f - text.length() * 20; // 居中估算
float textX = buttonX + buttonWidth / 2f - text.length() * 20; // 简单估算居中
float textY = y + buttonHeight / 2f + 20;
font.draw(batch, text, textX, textY);
}
@@ -104,8 +102,11 @@ public class MainMenuScreen extends ScreenAdapter {
@Override
public void dispose() {
if (batch != null) batch.dispose();
if (font != null) font.dispose();
if (shapeRenderer != null) shapeRenderer.dispose();
if (batch != null)
batch.dispose();
if (font != null)
font.dispose();
if (shapeRenderer != null)
shapeRenderer.dispose();
}
}

View File

@@ -3,7 +3,6 @@ package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
@@ -40,7 +39,8 @@ public class NetworkSettingsScreen extends ScreenAdapter {
@Override
public void render(float delta) {
new ClearScreen();
// 使用静态工具方法清屏
ClearScreen.clear();
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
@@ -64,6 +64,12 @@ public class NetworkSettingsScreen extends ScreenAdapter {
drawButtonText(CREATE_ROOM_Y, "创建房间");
drawButtonText(JOIN_ROOM_Y, "加入房间");
drawButtonText(EXIT_Y, "返回");
// 状态信息
NetworkManager nm = NetworkManager.getInstance();
String id = nm.getLocalPlayerId();
font.draw(batch, "本机ID: " + (id == null ? "(未分配)" : id.substring(0, Math.min(8, id.length()))), 50, 200);
font.draw(batch, nm.isHost() ? "当前: 房主" : (nm.isConnected() ? "当前: 已连接客户端" : "当前: 未连接"), 50, 160);
font.draw(batch, "在线玩家: " + (nm.getPlayerPositions() == null ? 0 : nm.getPlayerPositions().size()), 50, 120);
batch.end();
}

View File

@@ -0,0 +1,25 @@
package uno.mloluyu.desktop;
/** 简易屏幕管理器,负责切换与异常保护。 */
public class ScreenManager {
private final MainGame game;
private BaseScreen current;
public ScreenManager(MainGame game) {
this.game = game;
}
public void set(BaseScreen next) {
try {
if (current != null) {
current.hide();
current.dispose();
}
current = next;
game.setScreen(next);
} catch (Throwable t) {
// 忽略或可加简单 System.err
t.printStackTrace();
}
}
}

View File

@@ -0,0 +1,75 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import uno.mloluyu.util.ClearScreen;
import static uno.mloluyu.util.Font.loadChineseFont;
/**
* 简单设置界面占位:未来可扩展(音量/按键设置)。
*/
public class SettingsScreen extends ScreenAdapter {
private final MainGame game;
private SpriteBatch batch;
private BitmapFont font;
private ShapeRenderer shapeRenderer;
private static final int BACK_X = 100; // 返回按钮区域
private static final int BACK_Y = 100;
private static final int BACK_W = 220;
private static final int BACK_H = 70;
public SettingsScreen(MainGame game) {
this.game = game;
}
@Override
public void show() {
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
font = loadChineseFont();
font.setColor(Color.WHITE);
font.getData().setScale(2f);
}
@Override
public void render(float delta) {
ClearScreen.clear();
int mouseX = Gdx.input.getX();
int mouseY = Gdx.graphics.getHeight() - Gdx.input.getY();
// 绘制背景与返回按钮
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
shapeRenderer.setColor(Color.DARK_GRAY);
shapeRenderer.rect(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
boolean backHover = isHovered(mouseX, mouseY, BACK_X, BACK_Y, BACK_W, BACK_H);
shapeRenderer.setColor(backHover ? Color.LIGHT_GRAY : Color.GRAY);
shapeRenderer.rect(BACK_X, BACK_Y, BACK_W, BACK_H);
shapeRenderer.end();
batch.begin();
font.draw(batch, "设置 (占位界面)", 100, Gdx.graphics.getHeight() - 120);
font.draw(batch, "此处可添加: 音量 / 按键 / 分辨率 / 语言 等", 100, Gdx.graphics.getHeight() - 180);
font.draw(batch, "返回", BACK_X + 50, BACK_Y + 45);
batch.end();
if (Gdx.input.justTouched() && backHover) {
game.setScreen(new MainMenuScreen(game));
}
}
private boolean isHovered(int x, int y, int bx, int by, int bw, int bh) {
return x >= bx && x <= bx + bw && y >= by && y <= by + bh;
}
@Override
public void dispose() {
batch.dispose();
font.dispose();
shapeRenderer.dispose();
}
}

View File

@@ -1,17 +1,17 @@
package uno.mloluyu.desktop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import uno.mloluyu.util.ResourcePaths;
/**
* 启动屏幕类
* 显示游戏Logo并在3秒后切换到主菜单界面
*/
public class StartScreen implements Screen {
public class StartScreen extends BaseScreen {
private MainGame mainGame;
private MainGame mainGame; // TODO: 后续可直接用BaseScreen.game
private Texture logoTexture;
private com.badlogic.gdx.graphics.g2d.SpriteBatch batch;
@@ -19,8 +19,9 @@ public class StartScreen implements Screen {
private float deltaSum;
public StartScreen(MainGame mainGame) {
super(mainGame);
this.mainGame = mainGame;
logoTexture = new Texture(Gdx.files.internal("logo.png"));
logoTexture = new Texture(Gdx.files.internal(ResourcePaths.LOGO));
batch = new com.badlogic.gdx.graphics.g2d.SpriteBatch();
}
@@ -36,7 +37,6 @@ public class StartScreen implements Screen {
if (deltaSum >= .01F) {
if (mainGame != null) {
mainGame.showGameScreen();
System.out.println("已经切换到主菜单");
return;
}
}

View File

@@ -73,6 +73,14 @@ public class NetworkManager {
}
}
/**
* 本地玩家所选角色(仅本地缓存,远程映射存于 playerCharacters
* 供界面或后续同步逻辑查询。
*/
public String getLocalCharacter() {
return localCharacter;
}
public void receiveMessage(String message) {// 解析消息
if (message.startsWith("POS:")) {

View File

@@ -0,0 +1,20 @@
package uno.mloluyu.perf;
/** 简单性能指标收集(首版)。 */
public final class PerfMetrics {
private static long frameCount;
private static double accumTime;
private static double maxFrame = 0;
public static void frame(double delta) {
frameCount++;
accumTime += delta;
if (delta > maxFrame)
maxFrame = delta;
}
public static String summary() {
double avg = frameCount == 0 ? 0 : accumTime / frameCount;
return String.format("frames=%d avg=%.4f max=%.4f", frameCount, avg, maxFrame);
}
}

View File

@@ -3,8 +3,12 @@ package uno.mloluyu.util;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
public class ClearScreen {
public ClearScreen() {
/** 清屏工具:改为静态方法避免每帧 new 对象。 */
public final class ClearScreen {
private ClearScreen() {
}
public static void clear() {
Gdx.gl.glClearColor(0.3F, 0.3F, 0.5F, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
}

View File

@@ -0,0 +1,32 @@
package uno.mloluyu.util;
/**
* 全局游戏常量集中放置。
*/
public final class GameConstants {
/**
* 地面空气墙Y 坐标。抬高后便于更好构图。
*/
public static final float GROUND_Y = 180f; // 可按需要再调
// 角色移动/物理参数(集中配置便于统一手感调节)
// 用户要求:速度 *2跳跃 *3基于当前 520 / 1250
public static final float MOVE_SPEED = 1040f; // 520 *2 (最初 ~300)
// 调低跳跃:更低高度 + 更短滞空v0 ↓,同时重力 ↑
// 说明:上升时间 t_up = v0 / g本次取 v0=1500, g=3200 =>
// t_up≈0.47s总滞空≈0.94s,高度≈(v0^2)/(2g)≈351
// 若想再更低JUMP_SPEED 1400 + GRAVITY 3400再更高一点JUMP_SPEED 1600 + GRAVITY 3000。
public static final float JUMP_SPEED = 1500f;
public static final float GRAVITY = 3200f; // 加大重力让落地更快
// 调试命中盒渲染缩放(=1 表示真实大小;之前放大 3.6 现在回归可控)
// 调试盒缩放1 = 实际大小;若想放大显示结构,可调大。
public static final float DEBUG_BOX_SCALE = 1.0f;
// 是否按中心放大true 则保持角色中心位置,不会视觉漂移)
public static final boolean DEBUG_SCALE_FROM_CENTER = true;
// (可选)相机或后续平衡参数也可集中放这里
private GameConstants() {
}
}

View File

@@ -0,0 +1,12 @@
package uno.mloluyu.util;
/** 统一资源常量,避免魔法字符串散落。 */
public final class ResourcePaths {
private ResourcePaths() {
}
public static final String LOGO = "logo.png";
public static final String FONT_MAIN = "FLyouzichati-Regular-2.ttf";
public static final String CHARACTER_ROOT = "character/";
public static final String UI_SKIN_JSON = "ui/uiskin.json";
}

View File

@@ -0,0 +1,13 @@
package uno.mloluyu.util;
/** 限制delta时间防止窗口拖拽/卡顿后出现物理跳跃。 */
public final class TimeStepLimiter {
private TimeStepLimiter() {
}
private static final float MAX_DELTA = 1f / 30f; // 上限: 相当于最低30FPS
public static float clamp(float delta) {
return delta > MAX_DELTA ? MAX_DELTA : Math.max(delta, 0f);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -0,0 +1,22 @@
package uno.mloluyu.assets;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class AssetsExistenceTest {
private static final Path RES = Path.of("src", "main", "resources");
@Test
void testCoreAssetsPresent() {
List<String> required = List.of("logo.png", "character/alice/alice.png", "character/reimu/reimu-0.png",
"ui/uiskin.json");
for (String r : required) {
Path p = RES.resolve(r);
Assertions.assertTrue(Files.exists(p), "缺失资源: " + r);
}
}
}

View File

@@ -0,0 +1,21 @@
package uno.mloluyu.characters;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class ActionStateTest {
@Test
void testLegalTransitionIdleToMove() {
SimpleFighter f = new SimpleFighter("Test");
f.changeAction(Action.MOVE);
Assertions.assertEquals(Action.MOVE, f.getCurrentAction());
}
@Test
void testIllegalTransitionDeadToMove() {
SimpleFighter f = new SimpleFighter("Test");
f.changeAction(Action.DEAD);
f.changeAction(Action.MOVE); // 应被拒绝
Assertions.assertEquals(Action.DEAD, f.getCurrentAction());
}
}

BIN
target/classes/innerbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
artifactId=game
groupId=uno.mloluyu
version=1.0-SNAPSHOT

View File

@@ -1,6 +1,7 @@
uno\mloluyu\desktop\StartScreen.class
uno\mloluyu\desktop\NetworkSettingsScreen$1.class
uno\mloluyu\network\NetworkManager.class
uno\mloluyu\characters\FighterAnimationManager.class
uno\mloluyu\desktop\CharacterSelectScreen.class
uno\mloluyu\versatile\FighterController.class
uno\mloluyu\desktop\MainGame.class

View File

@@ -1,17 +1,28 @@
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\Action.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\ActionStateGuard.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\ActionTransitionMap.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\AdvancedFighter.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\FighterAnimationManager.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\FighterBase.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\characters\SimpleFighter.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\BaseScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\CharacterSelectScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\DesktopLauncher.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\GameScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\MainGame.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\MainMenuScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\NetworkSettingsScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\ScreenManager.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\SettingsScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\desktop\StartScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\network\ConnectClient.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\network\ConnectServer.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\network\NetworkManager.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\perf\PerfMetrics.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\ClearScreen.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\Font.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\GameConstants.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\ResourcePaths.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\SimpleFormatter.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\util\TimeStepLimiter.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\main\java\uno\mloluyu\versatile\FighterController.java

View File

@@ -0,0 +1,2 @@
C:\Users\www\Documents\Game\格斗游戏\Game\src\test\java\uno\mloluyu\assets\AssetsExistenceTest.java
C:\Users\www\Documents\Game\格斗游戏\Game\src\test\java\uno\mloluyu\characters\ActionStateTest.java