Merge remote-tracking branch 'origin/test' into test

# Conflicts:
#	src/main/java/uno/mloluyu/characters/Action.java
#	src/main/java/uno/mloluyu/characters/character/Alice.java
This commit is contained in:
2025-09-25 15:02:32 +08:00
43 changed files with 707 additions and 513 deletions

BIN
Game/.gitignore vendored Normal file

Binary file not shown.

25
Game/README.md Normal file
View File

@@ -0,0 +1,25 @@
# 有关本项目的说明
本项目为AVCEIT2025届软件技术专业软件241班Java实训小组项目
This project is about Java Project Training 2025, Major.Software Technology
### 本项目目前需要做的
- 实现游戏主界面,添加素材
- 完成游戏的核心功能,即双人格斗
- 实现基本的局域网内联机游戏功能
- *~~打则死路一条~~*
### 目前正在做的
- 编写单个角色并实现其状态 (@wsj)
- 将游戏素材整理为精灵图 (@wsj)
- 给游戏进行界面类添加BUG (@mloluyu)
- 给标题界面类添加BUG (@mloluyu)
- 寻找一个合适的数据处理类编写方法
- *~~丰矿地打则~~*
### 参与项目的人
- 武术家 [@wsj](varia.mloluyu.uno/wsj)
- 大货车 [@mloluyu](varia.mloluyu.uno/mloluyu)

114
Game/pom.xml Normal file
View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>uno.mloluyu</groupId>
<artifactId>game</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<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 版本 -->
</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>${gdx.version}</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl3</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>
</dependencies>
<build>
<plugins>
<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>
<!-- 移除不必要的预览特性,除非确实需要 -->
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- 确保这个类路径与你实际的Launcher类位置一致 -->
<!-- 例如如果你的类文件在src/main/java/uno/mloluyu/Launcher.java -->
<mainClass>uno.mloluyu.desktop.Launcher</mainClass>
</configuration>
</plugin>
<!-- 添加运行时类路径配置,解决类找不到问题 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>uno.mloluyu.desktop.Launcher</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,9 +1,11 @@
package uno.mloluyu.characters;
public enum Action {
IDLE, WALK, JUMP, FALL,
ATTACK1, ATTACK2, ATTACK3, ATTACK4,
HIT, DEFEND,
SPECIAL1, SPECIAL2,
DEATH
}
IDLE, // 待机状态:角色未执行任何动作,静止或准备中
JUMP, // 跳跃状态:角色正在空中跳跃或上升
MOVE, // 移动状态:角色正在左右移动
ATTACK, // 攻击状态:角色正在执行攻击动作
DEFEND, // 防御状态:角色正在格挡或防御中
HIT, // 受击状态:角色被攻击命中,进入硬直或受伤动画
DEAD // 死亡状态:角色生命值为 0进入死亡处理
}

View File

@@ -0,0 +1,30 @@
package uno.mloluyu.characters;
public class AdvancedFighter extends SimpleFighter {
public AdvancedFighter(String name) {
super(name); // 调用父类构造函数
}
@Override
public void attack(String attackType) {
// 根据攻击类型设置不同攻击力或状态
switch (attackType.toLowerCase()) {
case "light":
changeAction(Action.ATTACK);
System.out.println(getName() + " 发起轻攻击!");
break;
case "heavy":
changeAction(Action.ATTACK);
System.out.println(getName() + " 发起重攻击!");
break;
case "special":
changeAction(Action.ATTACK);
System.out.println(getName() + " 发动特殊技能!");
break;
default:
super.attack(attackType); // 默认调用父类攻击逻辑
break;
}
}
}

View File

@@ -1,82 +0,0 @@
package uno.mloluyu.characters;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
/**
* Alice角色类继承自Fighter父类定义其专属属性和动画
*/
public class Alice extends Fighter {
public Alice() {
super("Alice", new TextureAtlas(Gdx.files.internal("src/main/resources/character/alice/精灵1.2.atlas")));
// 设置角色属性
speed = 350f; // 更快的移动速度
maxHealth = 90; // 较低的生命值
health = maxHealth;
attackPower = 12; // 中等攻击力
}
@Override
protected void loadAnimations() {
// 基础动作
loadLoopingAnimation(Action.IDLE, "stand/stand", 15);
loadLoopingAnimation(Action.WALK, "walkFront/walkFront", 9);
loadOneShotAnimation(Action.JUMP, "jump/jump", 8);
loadOneShotAnimation(Action.FALL, "hitSpin/hitSpin", 5);
// 攻击动作
loadOneShotAnimation(Action.ATTACK1, "attackAa/attackAa", 6);
loadOneShotAnimation(Action.ATTACK2, "attackAb/attackAb", 6);
loadOneShotAnimation(Action.ATTACK3, "attackAc/attackAc", 6);
loadOneShotAnimation(Action.ATTACK4, "attackAd/attackAd", 6);
// // 特殊动作(可扩展)
// loadOneShotAnimation(Action.SPECIAL1, "special/special1", 6);
// loadOneShotAnimation(Action.SPECIAL2, "special/special2", 6);
// 受击与死亡
loadOneShotAnimation(Action.HIT, "hitSpin/hitSpin", 5);
// loadOneShotAnimation(Action.DEATH, "death/death", 8);
// 帧速率调整
setFrameDuration(Action.IDLE, 0.04f);
setFrameDuration(Action.WALK, 0.08f);
setFrameDuration(Action.ATTACK1, 0.07f);
setFrameDuration(Action.SPECIAL2, 0.06f);
}
/**
* 特定移动状态处理:允许跳跃状态下移动
*/
@Override
protected void handleMoveState() {
if (currentAction != Action.ATTACK1 &&
currentAction != Action.ATTACK2 &&
currentAction != Action.ATTACK3 &&
currentAction != Action.ATTACK4 &&
currentAction != Action.SPECIAL1 &&
currentAction != Action.SPECIAL2 &&
currentAction != Action.DEFEND &&
currentAction != Action.JUMP &&
currentAction != Action.FALL) {
changeAction(Action.WALK);
}
}
/**
* 空中也可以攻击
*/
@Override
protected boolean canAttack() {
return super.canAttack() || currentAction == Action.JUMP || currentAction == Action.FALL;
}
/**
* 获取当前生命值
*/
public int getHp() {
return health;
}
}

View File

@@ -1,10 +0,0 @@
package uno.mloluyu.characters;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
public class FighterList {
public static final TextureAtlas aliceAtlas = new TextureAtlas(Gdx.files.internal("src\\main\\resources\\character\\alice\\alice.atlas"));
}

View File

@@ -0,0 +1,203 @@
package uno.mloluyu.characters;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Rectangle;
/**
* 简化版角色类,仅包含移动、攻击、受击等基础功能。
*/
public class SimpleFighter {
private String name; // 角色名称
private final Map<Integer, Float> keyPressDuration = new HashMap<>();
private Action currentAction = Action.IDLE; // 当前动作状态(待机、攻击、受击等)
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 int attackPower = 10; // 攻击力(暂未使用)
private boolean isAttacking = false; // 是否正在攻击(攻击状态标志)
private SimpleFighter fighter; // 添加 fighter 的声明
private Iterable<Integer> pressedKeys = new HashMap<Integer, Float>().keySet(); // 初始化 pressedKeys
public SimpleFighter(String name) {
this.name = name; // 构造函数,初始化角色名称
this.fighter = this; // 初始化 fighter 为当前实例
}
public void update(float deltaTime) {
updateAttackbox();
// 攻击只持续一帧
if (isAttacking) {
isAttacking = false;
changeAction(Action.IDLE);
}
for (int keycode : pressedKeys) {
keyPressDuration.put(keycode, keyPressDuration.getOrDefault(keycode, 0f) + deltaTime); // 更新持续时间
fighter.handleInput(keycode, true); // 持续按下的键
}
// 垂直移动(跳跃或重力)
if (!isGrounded) {
verticalSpeed -= 980 * deltaTime; // 简单重力模拟
hitbox.y += verticalSpeed * deltaTime;
if (hitbox.y <= 0) {
hitbox.y = 0;
verticalSpeed = 0;
isGrounded = true;
changeAction(Action.IDLE);
}
}
}
public void render(SpriteBatch batch, ShapeRenderer shapeRenderer) {
batch.end(); // 暂停 SpriteBatch 渲染,切换到 ShapeRenderer
System.out.println("人物状态" + currentAction);
boolean isAttacking = currentAction == Action.ATTACK;
shapeRenderer.begin(ShapeRenderer.ShapeType.Line); // 开始绘制线框
shapeRenderer.setColor(Color.BLUE); // 设置颜色为蓝色
shapeRenderer.rect(hitbox.x, hitbox.y, hitbox.width, hitbox.height); // 绘制碰撞盒
if (isAttacking) {
shapeRenderer.setColor(Color.RED); // 设置颜色为红色
shapeRenderer.rect(attackbox.x, attackbox.y, attackbox.width, attackbox.height); // 绘制攻击盒
}
shapeRenderer.end(); // 结束 ShapeRenderer 渲染
batch.begin(); // 恢复 SpriteBatch 渲染
}
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()); // 向左移动
} else if (keycode == Input.Keys.RIGHT || keycode == Input.Keys.D) {
move(1, Gdx.graphics.getDeltaTime()); // 向右移动
}
if (keycode == Input.Keys.Z || keycode == Input.Keys.J) {
attack(""); // 普通攻击
} else if (keycode == Input.Keys.X || keycode == Input.Keys.K) {
attack(""); // 重攻击(暂未区分)
} else if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
attack(""); // 跳跃(暂未实现跳跃逻辑)
} else if (keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) {
attack(""); // 防御(暂未实现防御逻辑)
}
if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
System.out.println("点击了跳跃");
jump();
}
} else {
// 松开防御键时恢复待机状态
if ((keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) &&
getCurrentAction() == Action.DEFEND) {
changeAction(Action.IDLE);
}
}
}
public void handleInput(int keycode, boolean isPressed) {
handleInput(keycode, isPressed, 0f); // 调用已有方法,补充默认持续时间
}
public void handleRelease(int keycode, float duration) {
// 处理按键释放逻辑
System.out.println("按键释放: " + keycode + ", 持续时间: " + duration);
keyPressDuration.remove(keycode);
if (keycode == Input.Keys.LEFT || keycode == Input.Keys.RIGHT || keycode == Input.Keys.A
|| keycode == Input.Keys.D) {
changeAction(Action.IDLE);
}
}
public Action getCurrentAction() {
return currentAction; // 获取当前动作状态
}
public void changeAction(Action newAction) {
this.currentAction = newAction; // 切换角色动作状态
}
public void jump() {
if (isGrounded) {
verticalSpeed = 600f;
isGrounded = false;
changeAction(Action.JUMP);
}
}
public void move(float x, float deltaTime) {
if (x != 0) {
isFacingRight = x > 0;
hitbox.x += x * speed * deltaTime;
changeAction(Action.MOVE); // 移动时切换为 MOVE 状态
} else if (isGrounded && !isAttacking) {
changeAction(Action.IDLE); // 停止移动时恢复待机
}
}
public void attack(String attackType) {
isAttacking = true; // 设置攻击状态
changeAction(Action.ATTACK); // 切换为攻击动作
}
public void takeHit(int damage) {
health = Math.max(0, health - damage); // 扣除生命值,最小为 0
changeAction(health > 0 ? Action.HIT : Action.DEAD); // 根据生命值切换为受击或死亡状态
}
public boolean isAlive() {
return health > 0; // 判断角色是否存活
}
public boolean isAttacking() {
return isAttacking; // 判断是否处于攻击状态
}
private void updateAttackbox() {
// 根据朝向更新攻击盒位置,使其位于角色前方
float offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + 20);
}
// 常用访问器
public Rectangle getHitbox() {
return hitbox; // 获取碰撞盒
}
public Rectangle getAttackbox() {
return attackbox; // 获取攻击盒
}
public int getHealth() {
return health; // 获取当前生命值
}
public String getName() {
return name; // 获取角色名称
}
public void setPosition(float x, float y) {
hitbox.setPosition(x, y); // 设置角色位置
}
}

View File

@@ -0,0 +1,9 @@
package uno.mloluyu.characters.character;
public enum Action {
IDLE, WALK, JUMP, FALL,
ATTACK1, ATTACK2, ATTACK3, ATTACK4,
HIT, DEFEND,
SPECIAL1, SPECIAL2,
DEATH
}

View File

@@ -2,8 +2,6 @@ package uno.mloluyu.characters.character;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import uno.mloluyu.characters.Fighter;
import uno.mloluyu.characters.Action;
/**
* Alice角色类继承自Fighter父类定义其专属属性和动画

View File

@@ -1,146 +1,113 @@
package uno.mloluyu.characters;
package uno.mloluyu.characters.character;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import uno.mloluyu.util.SimpleFormatter;
import java.util.EnumMap;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
/**
* 抽象类 Fighter定义所有角色的基础属性与行为
* 包括动画控制移动攻击受击渲染等核心逻辑
*/
public abstract class Fighter implements Disposable {
public enum Action {
IDLE, WALK, JUMP, FALL,
ATTACK1, ATTACK2, ATTACK3, ATTACK4,
HIT, DEFEND,
SPECIAL1, SPECIAL2,
DEATH
}
// 默认帧持续时间
protected static final float DEFAULT_FRAME_DURATION = 0.1f;
// 默认生命值
protected static final int DEFAULT_HEALTH = 100;
// 默认移动速度像素/
protected static final float DEFAULT_SPEED = 300f;
// 角色名称
protected String name;
// 当前动作状态
protected Action currentAction = Action.IDLE;
// 当前动作已持续时间
protected float stateTime = 0f;
// 是否面向右侧
protected boolean isFacingRight = true;
// 当前动画是否播放完毕
protected boolean isAnimationFinished = false;
protected EnumMap<Action, Animation<TextureRegion>> animations = new EnumMap<>(Action.class);
protected EnumMap<Action, Float> frameDurations = new EnumMap<>(Action.class);
// 碰撞盒用于位置和受击判定
protected Rectangle hitbox = new Rectangle(0, 0, 64, 128);
// 攻击盒用于攻击判定
protected Rectangle attackbox = new Rectangle(0, 0, 80, 80);
// 移动速度
protected float speed = DEFAULT_SPEED;
// 当前生命值
protected int health = DEFAULT_HEALTH;
// 最大生命值
protected int maxHealth = DEFAULT_HEALTH;
// 攻击力
protected int attackPower = 10;
protected TextureAtlas atlas;
protected float scaleX = 1.0f;
protected float scaleY = 1.0f;
public Fighter() {}
// 动画管理器
protected FighterAnimationManager animationManager;
/**
* 构造函数初始化角色名称与动画资源
*
* @param name 角色名称
* @param atlas 动画图集
*/
public Fighter(String name, TextureAtlas atlas) {
this.name = name;
this.atlas = atlas;
for (Action action : Action.values()) {
frameDurations.put(action, DEFAULT_FRAME_DURATION);
}
this.animationManager = new FighterAnimationManager(atlas);
loadAnimations();
}
/**
* 加载角色的所有动画资源由子类实现
*/
protected abstract void loadAnimations();
protected void loadAnimationFromAtlas(Action action, String regionPrefix, int frameCount, boolean loop) {
if (atlas == null)
throw new IllegalStateException("TextureAtlas 未初始化!");
if (frameCount <= 0)
throw new IllegalArgumentException("帧数必须大于0: " + frameCount);
Array<TextureRegion> frames = new Array<>();
for (int i = 0; i < frameCount; i++) {
String regionName = regionPrefix + SimpleFormatter.addLeadingZeros(i, 3);
TextureRegion region = atlas.findRegion(regionName);
if (region == null) {
throw new IllegalArgumentException("未找到区域: " + regionName);
}
frames.add(region);
}
Animation<TextureRegion> animation = new Animation<>(frameDurations.get(action), frames);
animation.setPlayMode(loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL);
animations.put(action, animation);
}
protected void loadLoopingAnimation(Action action, String prefix, int count) {
loadAnimationFromAtlas(action, prefix, count, true);
}
protected void loadOneShotAnimation(Action action, String prefix, int count) {
loadAnimationFromAtlas(action, prefix, count, false);
}
protected void setFrameDuration(Action action, float duration) {
frameDurations.put(action, duration);
Animation<TextureRegion> anim = animations.get(action);
if (anim != null)
anim.setFrameDuration(duration);
}
/**
* 每帧更新角色状态包括动画播放与碰撞盒更新
*
* @param deltaTime 帧间隔时间
*/
public void update(float deltaTime) {
stateTime += deltaTime;
Animation<TextureRegion> anim = animations.get(currentAction);
if (anim != null) {
isAnimationFinished = anim.isAnimationFinished(stateTime);
}
isAnimationFinished = animationManager.isFinished(currentAction, stateTime);
handleAnimationTransitions();
updateHitboxes();
}
/**
* 渲染角色当前帧
*
* @param batch 渲染批处理器
*/
public void render(SpriteBatch batch) {
animationManager.render(batch, currentAction, stateTime, hitbox, isFacingRight);
}
/**
* 动画播放完毕后的动作切换逻辑
*/
protected void handleAnimationTransitions() {
if (!isAnimationFinished) return;
if (!isAnimationFinished)
return;
switch (currentAction) {
case ATTACK1, ATTACK2, ATTACK3, SPECIAL1, SPECIAL2, HIT -> changeAction(Action.IDLE);
case JUMP -> changeAction(Action.FALL);
default -> {}
default -> {
}
}
}
public void render(SpriteBatch batch) {
Animation<TextureRegion> anim = animations.get(currentAction);
if (anim == null) {
Gdx.app.error("Fighter", "动画未初始化: " + currentAction);
return;
}
TextureRegion frame = anim.getKeyFrame(stateTime, anim.getPlayMode() == Animation.PlayMode.LOOP);
if (frame == null) {
Gdx.app.error("Fighter", "动画帧为空: " + currentAction);
return;
}
float frameWidth = frame.getRegionWidth() * scaleX;
float frameHeight = frame.getRegionHeight() * scaleY;
float drawX = hitbox.x + (hitbox.width - frameWidth) / 2;
float drawY = hitbox.y;
boolean wasFlippedX = frame.isFlipX();
frame.flip(!isFacingRight && !wasFlippedX, false);
frame.flip(isFacingRight && wasFlippedX, false);
batch.draw(frame, drawX, drawY, frameWidth / 2, frameHeight / 2, frameWidth, frameHeight, 1f, 1f, 0f);
frame.flip(wasFlippedX != frame.isFlipX(), false);
}
/**
* 切换角色动作状态
*
* @param newAction 新动作
* @return 是否成功切换
*/
public boolean changeAction(Action newAction) {
if (isActionUninterruptible(currentAction)) return false;
if (isActionUninterruptible(currentAction))
return false;
if (currentAction != newAction) {
currentAction = newAction;
stateTime = 0f;
@@ -150,15 +117,30 @@ public abstract class Fighter implements Disposable {
return false;
}
/**
* 判断某个动作是否不可打断如受击或死亡
*
* @param action 动作枚举
* @return 是否不可打断
*/
protected boolean isActionUninterruptible(Action action) {
return action == Action.HIT || action == Action.DEATH;
}
/**
* 更新攻击盒位置根据角色朝向调整
*/
protected void updateHitboxes() {
float offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + 20);
}
/**
* 移动角色
*
* @param x 水平移动方向-1左1右
* @param deltaTime 帧间隔时间
*/
public void move(float x, float deltaTime) {
if (x != 0) {
isFacingRight = x > 0;
@@ -169,19 +151,29 @@ public abstract class Fighter implements Disposable {
}
}
/**
* 移动状态下的动作切换逻辑
*/
protected void handleMoveState() {
if (!isActionUninterruptible(currentAction) &&
currentAction != Action.JUMP &&
currentAction != Action.FALL &&
currentAction != Action.DEFEND &&
!currentAction.name().startsWith("ATTACK") &&
!currentAction.name().startsWith("SPECIAL")) {
currentAction != Action.JUMP &&
currentAction != Action.FALL &&
currentAction != Action.DEFEND &&
!currentAction.name().startsWith("ATTACK") &&
!currentAction.name().startsWith("SPECIAL")) {
changeAction(Action.WALK);
}
}
/**
* 发起攻击动作
*
* @param attackType 攻击类型1~5
* @return 是否成功发起攻击
*/
public boolean attack(int attackType) {
if (!canAttack()) return false;
if (!canAttack())
return false;
Action attackAction = switch (attackType) {
case 1 -> Action.ATTACK1;
@@ -195,25 +187,49 @@ public abstract class Fighter implements Disposable {
return attackAction != null && changeAction(attackAction);
}
/**
* 判断当前是否可以攻击
*
* @return 是否可攻击
*/
protected boolean canAttack() {
return currentAction == Action.IDLE || currentAction == Action.WALK;
}
/**
* 接受伤害
*
* @param damage 伤害值
*/
public void takeHit(int damage) {
if (currentAction == Action.DEATH) return;
if (currentAction == Action.DEATH)
return;
health = Math.max(0, health - damage);
changeAction(health == 0 ? Action.DEATH : Action.HIT);
}
/**
* 设置角色位置
*
* @param x 横坐标
* @param y 纵坐标
*/
public void setPosition(float x, float y) {
hitbox.setPosition(x, y);
}
/**
* 设置角色朝向
*
* @param facingRight 是否面向右
*/
public void setFacingRight(boolean facingRight) {
this.isFacingRight = facingRight;
}
// 以下为常用属性访问器
public String getName() {
return name;
}
@@ -254,8 +270,18 @@ public abstract class Fighter implements Disposable {
return hitbox.y + hitbox.height / 2;
}
/**
* 帧事件监听器接口用于在动画播放到某一帧时触发逻辑
*/
public interface FrameEventListener {
void onFrameEvent(Action action, int frameIndex);
}
/**
* 释放资源如动画图集
*/
@Override
public void dispose() {
if (atlas != null) atlas.dispose();
animationManager.dispose();
}
}

View File

@@ -0,0 +1,83 @@
package uno.mloluyu.characters.character;
import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Array;
import uno.mloluyu.util.SimpleFormatter;
import java.util.EnumMap;
public class FighterAnimationManager {
private EnumMap<Action, Animation<TextureRegion>> animations = new EnumMap<>(Action.class);
private EnumMap<Action, Float> frameDurations = new EnumMap<>(Action.class);
private TextureAtlas atlas;
private float scaleX = 1.0f;
private float scaleY = 1.0f;
public FighterAnimationManager(TextureAtlas atlas) {
this.atlas = atlas;
for (Action action : Action.values()) {
frameDurations.put(action, 0.1f);
}
}
public void loadAnimation(Action action, String prefix, int count, boolean loop) {
Array<TextureRegion> frames = new Array<>();
for (int i = 0; i < count; i++) {
String regionName = prefix + SimpleFormatter.addLeadingZeros(i, 3);
TextureRegion region = atlas.findRegion(regionName);
if (region == null) {
throw new IllegalArgumentException("未找到区域: " + regionName);
}
frames.add(region);
}
Animation<TextureRegion> animation = new Animation<>(frameDurations.get(action), frames);
animation.setPlayMode(loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL);
animations.put(action, animation);
}
public void loadLooping(Action action, String prefix, int count) {
loadAnimation(action, prefix, count, true);
}
public void loadOneShot(Action action, String prefix, int count) {
loadAnimation(action, prefix, count, false);
}
public void setFrameDuration(Action action, float duration) {
frameDurations.put(action, duration);
Animation<TextureRegion> anim = animations.get(action);
if (anim != null) anim.setFrameDuration(duration);
}
public boolean isFinished(Action action, float stateTime) {
Animation<TextureRegion> anim = animations.get(action);
return anim != null && anim.isAnimationFinished(stateTime);
}
public void render(SpriteBatch batch, Action action, float stateTime, Rectangle hitbox, boolean isFacingRight) {
Animation<TextureRegion> anim = animations.get(action);
if (anim == null) return;
TextureRegion frame = anim.getKeyFrame(stateTime, anim.getPlayMode() == Animation.PlayMode.LOOP);
if (frame == null) return;
float frameWidth = frame.getRegionWidth() * scaleX;
float frameHeight = frame.getRegionHeight() * scaleY;
float drawX = hitbox.x + (hitbox.width - frameWidth) / 2;
float drawY = hitbox.y;
boolean wasFlippedX = frame.isFlipX();
frame.flip(!isFacingRight && !wasFlippedX, false);
frame.flip(isFacingRight && wasFlippedX, false);
batch.draw(frame, drawX, drawY, frameWidth / 2, frameHeight / 2, frameWidth, frameHeight, 1f, 1f, 0f);
frame.flip(wasFlippedX != frame.isFlipX(), false);
}
public void dispose() {
if (atlas != null) atlas.dispose();
}
}

View File

@@ -1,8 +1,10 @@
package uno.mloluyu.characters;
package uno.mloluyu.characters.character;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import uno.mloluyu.characters.character.Fighter.Action;
public class Reimu extends Fighter {
public Reimu() {
super(new TextureAtlas(Gdx.files.internal("src\\main\\resources\\character\\reimu\\reimu.atlas")));

View File

@@ -11,9 +11,10 @@ import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.characters.Fighter;
import uno.mloluyu.characters.Reimu;
import uno.mloluyu.characters.character.Alice;
import uno.mloluyu.characters.AdvancedFighter;
import uno.mloluyu.characters.SimpleFighter;
import uno.mloluyu.characters.character.Reimu;
import uno.mloluyu.util.ClearScreen;
import java.util.Arrays;
import java.util.List;
@@ -144,20 +145,19 @@ public class CharacterSelectScreen extends ScreenAdapter {
}
}
// 点击确认按钮
// 点击确认按钮
if (isHovered(mouseX, mouseY, BUTTON_X, CONFIRM_Y, BUTTON_WIDTH, BUTTON_HEIGHT)) {
if (selectedIndex != -1) {
String selectedCharacter = characters.get(selectedIndex);
Gdx.app.log("Character", "确认角色: " + selectedCharacter);
Fighter fighter = null;
SimpleFighter fighter = null;
switch (selectedCharacter) {
case "Alice":
fighter = new Alice();
fighter = new AdvancedFighter("Alice");
break;
case "Reimu":
fighter = new Reimu();
fighter = new AdvancedFighter("Reimu");
break;
}

View File

@@ -1,32 +1,29 @@
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.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Rectangle;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.characters.Fighter;
import uno.mloluyu.characters.Reimu;
import uno.mloluyu.characters.SimpleFighter;
import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.util.ClearScreen;
import uno.mloluyu.versatile.FighterController;
public class GameScreen extends ScreenAdapter {
private final MainGame game;
private final Fighter player;
private final SimpleFighter player;
private final FighterController controller;
private final Map<String, SimpleFighter> otherPlayers = new HashMap<>();
private SpriteBatch batch;
private ShapeRenderer shapeRenderer;
private FighterController controller;
private final Map<String, Fighter> otherPlayers = new HashMap<>();
public GameScreen(MainGame game, Fighter player) {
this.game = game;
public GameScreen(MainGame game, SimpleFighter player) {
this.player = player;
this.controller = new FighterController(player);
}
@@ -42,75 +39,61 @@ public class GameScreen extends ScreenAdapter {
public void render(float delta) {
new ClearScreen();
// 更新角色状态
player.update(delta);
controller.update(delta);
// 发送本机玩家位置
if (NetworkManager.getInstance().isConnected()) {
NetworkManager.getInstance().sendPosition(player.getX(), player.getY());
NetworkManager.getInstance().sendPosition(player.getHitbox().x, player.getHitbox().y);
}
// 渲染角色
batch.begin();
player.render(batch);
batch.end();
// 渲染其他玩家位置(联机模式)
if (NetworkManager.getInstance().isConnected()) {
renderOtherPlayers();
}
}
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
renderFighter(player, Color.BLUE);
if (player.isAttacking()) renderAttackBox(player, Color.RED);
private void renderOtherPlayers() {
Map<String, float[]> positions = NetworkManager.getInstance().getPlayerPositions();
Map<String, String> characters = NetworkManager.getInstance().getPlayerCharacters();
batch.begin();
for (Map.Entry<String, float[]> entry : positions.entrySet()) {
String playerId = entry.getKey();
float[] pos = entry.getValue();
// 跳过本机玩家(可选)
// if (playerId.equals(NetworkManager.getInstance().getLocalPlayerId()))
// continue;
// 获取角色名
String characterName = characters.get(playerId);
if (characterName == null || pos == null)
continue;
// 获取或创建 Fighter 实例
Fighter fighter = otherPlayers.get(playerId);
if (fighter == null) {
switch (characterName) {
case "Alice":
fighter = new Alice();
break;
case "Reimu":
fighter = new Reimu();
default:
fighter = new Alice();
break;
};
if (fighter != null) {
otherPlayers.put(playerId, fighter);
}
}
// 设置位置并渲染
if (fighter != null) {
fighter.setPosition(pos[0], pos[1]);
fighter.render(batch);
if (positions != null) {
for (Map.Entry<String, float[]> entry : positions.entrySet()) {
String id = entry.getKey();
float[] pos = entry.getValue();
if (pos == null) continue;
SimpleFighter remote = otherPlayers.computeIfAbsent(id, k -> new SimpleFighter("Remote-" + k));
remote.setPosition(pos[0], pos[1]);
remote.update(delta);
renderFighter(remote, Color.GREEN);
}
}
batch.end();
shapeRenderer.end();
}
private void renderFighter(SimpleFighter fighter, Color color) {
shapeRenderer.setColor(color);
Rectangle r = fighter.getHitbox();
shapeRenderer.rect(r.x, r.y, r.width, r.height);
}
private void renderAttackBox(SimpleFighter fighter, Color color) {
shapeRenderer.setColor(color);
Rectangle a = fighter.getAttackbox();
shapeRenderer.rect(a.x, a.y, a.width, a.height);
}
// private void checkPlayerAttacks() {
// if (!player.isAttacking()) return;
// for (SimpleFighter target : otherPlayers.values()) {
// if (target.isAlive() && player.getAttackbox().overlaps(target.getHitbox())) {
// target.takeHit(player.getAttackPower()); // 使用访问器方法
// System.out.println("命中远程玩家:" + target.getName());
// }
// }
// }
@Override
public void dispose() {
batch.dispose();
player.dispose();
shapeRenderer.dispose();
// 断开网络连接
NetworkManager.getInstance().disconnect();
}
}
}

View File

@@ -16,7 +16,6 @@ public class MainGame extends Game {
mainMenuScreen = new MainMenuScreen(this);
setScreen(new MainMenuScreen(this));
setScreen(startScreen);
}
public void showGameScreen() {

View File

@@ -8,7 +8,7 @@ import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.characters.character.Alice;
import uno.mloluyu.versatile.FighterController;
import static uno.mloluyu.util.Font.loadChineseFont;

View File

@@ -1,122 +0,0 @@
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.Color;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
/**
* 屏幕过渡效果类
* 用于在不同屏幕之间实现平滑的过渡动画
*/
public class TransitionScreen implements Screen {
private ShapeRenderer shapeRenderer;
private SpriteBatch batch;
private float transitionTime = 0;
private float totalTransitionTime = 0.5f; // 过渡时间为0.5秒
private Runnable targetScreenAction;
private TransitionType transitionType = TransitionType.FADE_OUT_FADE_IN;
/**
* 过渡类型枚举
*/
public enum TransitionType {
FADE_OUT_FADE_IN
}
public TransitionScreen() {
this.shapeRenderer = new ShapeRenderer();
this.batch = new SpriteBatch();
}
/**
* 设置目标屏幕的显示动作
*
* @param targetScreenAction 目标屏幕的显示动作
*/
public void setTargetScreen(Runnable targetScreenAction) {
this.targetScreenAction = targetScreenAction;
this.transitionTime = 0;
}
@Override
public void show() {
// 屏幕显示时的初始化操作
}
@Override
public void render(float delta) {
// 清屏
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
transitionTime += delta;
float progress = Math.min(transitionTime / totalTransitionTime, 1);
// 绘制过渡效果
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
shapeRenderer.setColor(0, 0, 0, getAlpha(progress));
shapeRenderer.rect(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
shapeRenderer.end();
// 当过渡动画完成时,执行目标屏幕的显示动作
if (progress >= 1 && targetScreenAction != null) {
targetScreenAction.run();
}
}
/**
* 根据过渡进度计算透明度
*
* @param progress 过渡进度0-1
* @return 透明度值0-1
*/
private float getAlpha(float progress) {
switch (transitionType) {
case FADE_OUT_FADE_IN:
// 前半段淡出透明度从0到1后半段淡入透明度从1到0
if (progress < 0.5) {
return progress * 2;
} else {
return (1 - progress) * 2;
}
default:
return 1;
}
}
@Override
public void resize(int width, int height) {
// 屏幕尺寸变化时的处理
}
@Override
public void pause() {
// 游戏暂停时的处理
}
@Override
public void resume() {
// 游戏恢复时的处理
}
@Override
public void hide() {
// 屏幕隐藏时的处理
}
@Override
public void dispose() {
// 释放资源
if (shapeRenderer != null) {
shapeRenderer.dispose();
shapeRenderer = null;
}
if (batch != null) {
batch.dispose();
batch = null;
}
}
}

View File

@@ -10,7 +10,6 @@ public class NetworkManager {
private ConnectServer server;
private ConnectClient client;
private boolean isHost = false;
private String localPlayerId;
private String localCharacter;
private final Map<String, float[]> playerPositions = new HashMap<>();
@@ -31,31 +30,31 @@ public class NetworkManager {
return localPlayerId;
}
public void createRoom() {
public void createRoom() {//创建房间
isHost = true;
server = new ConnectServer(11455);
new Thread(server).start();
Gdx.app.log("Network", "房主模式:服务器已启动");
}
public void joinRoom(String ip) {
public void joinRoom(String ip) {//加入房间
isHost = false;
client = new ConnectClient(ip, 11455);
Gdx.app.log("Network", "客户端模式:连接到房主 " + ip);
}
public void sendPosition(float x, float y) {
public void sendPosition(float x, float y) {//发送位置消息
String msg = "POS:" + localPlayerId + "," + x + "," + y;
Gdx.app.log("Network", "发送位置消息: " + msg);
if (isHost && server != null) {
server.broadcastToOthers(null, msg);
receiveMessage(msg); // 房主自己也处理
receiveMessage(msg);
} else if (client != null) {
client.sendMessage(msg);
}
}
public void sendCharacterSelection(String character) {
public void sendCharacterSelection(String character) {//发送角色选择消息
this.localCharacter = character;
String msg = "SELECT:" + localPlayerId + "," + character;
Gdx.app.log("Network", "发送角色选择消息: " + msg);
@@ -67,7 +66,7 @@ public class NetworkManager {
}
}
public void receiveMessage(String message) {
public void receiveMessage(String message) {//解析消息
Gdx.app.log("Network", "收到消息: " + message);
if (message.startsWith("POS:")) {

View File

@@ -1,131 +1,66 @@
package uno.mloluyu.versatile;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.utils.Array;
import uno.mloluyu.characters.Fighter;
import uno.mloluyu.characters.character.Action;
import uno.mloluyu.characters.character.Fighter;
import uno.mloluyu.characters.SimpleFighter;
/**
* 角色控制器,处理玩家输入并映射到角色动作
*/
public class FighterController extends InputAdapter {
private final Fighter fighter;
private final SimpleFighter fighter;
private final Array<Integer> pressedKeys = new Array<>();
// 输入缓冲时间(防止快速按键导致的重复触发)
private static final float INPUT_DELAY = 0.2f;
private float attackCooldown = 0;
private float jumpCooldown = 0;
private final Map<Integer, Float> keyPressDuration = new HashMap<>();
public FighterController(Fighter fighter) {
public FighterController(SimpleFighter fighter) {
this.fighter = fighter;
}
public FighterController() {
this.fighter = null;
}
/**
* 更新控制器状态和冷却时间
* @param deltaTime 帧间隔时间
*/
public void update(float deltaTime) {
// 更新冷却时间
if (attackCooldown > 0) attackCooldown -= deltaTime;
if (jumpCooldown > 0) jumpCooldown -= deltaTime;
// 处理移动输入
handleMovement();
if (fighter == null)
return;
for (int keycode : pressedKeys) {
fighter.handleInput(keycode, true); // 持续按下的键
}
}
/**
* 处理移动输入
*/
private void handleMovement() {
float moveX = 0;
// 左右移动控制支持方向键和WSAD
if (isKeyPressed(Input.Keys.RIGHT) || isKeyPressed(Input.Keys.D)) {
moveX += 1;
}
if (isKeyPressed(Input.Keys.LEFT) || isKeyPressed(Input.Keys.A)) {
moveX -= 1;
}
// 调用角色移动方法
fighter.move(moveX, Gdx.graphics.getDeltaTime());
}
/**
* 处理按键按下事件
*/
@Override
public boolean keyDown(int keycode) {
if (fighter == null)
return false;
if (!pressedKeys.contains(keycode, false)) {
pressedKeys.add(keycode);
}
// 普通攻击Z键或J键
if ((keycode == Input.Keys.Z || keycode == Input.Keys.J) && attackCooldown <= 0) {
fighter.attack(1);
attackCooldown = INPUT_DELAY;
return true;
}
// 第二攻击X键或K键
if ((keycode == Input.Keys.X || keycode == Input.Keys.K) && attackCooldown <= 0) {
fighter.attack(2);
attackCooldown = INPUT_DELAY;
return true;
}
// 跳跃空格、上方向键或W键
if ((keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) && jumpCooldown <= 0) {
// 这里假设你已经实现了跳跃方法
// fighter.jump();
jumpCooldown = INPUT_DELAY;
return true;
}
// 防御左Shift或右Shift
if (keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) {
fighter.changeAction(Fighter.Action.DEFEND);
return true;
}
return false;
fighter.handleInput(keycode, true); // 按下事件
return true;
}
/**
* 处理按键释放事件
*/
@Override
public boolean keyUp(int keycode) {
pressedKeys.removeValue(keycode, false);
// 释放防御键时恢复到 idle 状态
if ((keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) &&
fighter.getCurrentAction() == Fighter.Action.DEFEND) {
fighter.changeAction(Fighter.Action.IDLE);
}
return false;
}
@Override
public boolean keyUp(int keycode) {
if (fighter == null)
return false;
/**
* 检查按键是否处于按下状态
*/
private boolean isKeyPressed(int keycode) {
return pressedKeys.contains(keycode, false);
}
float duration = keyPressDuration.getOrDefault(keycode, 0f);
pressedKeys.removeValue(keycode, false);
keyPressDuration.remove(keycode);
/**
* 获取当前控制的角色
*/
public Fighter getFighter() {
// 传给角色:按键松开 + 按下时长
fighter.handleRelease(keycode, duration);
return true;
}//松开事件
public SimpleFighter getFighter() {
return fighter;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.