添加 UI 资源并重构角色类

添加了新的 UI 资源:logo.png、uiskin.atlas 和 uiskin.json,以改进界面设计。
移除了过时的 FighterController 和 GameCore 类,以精简代码库。
引入了新的角色类:FighterList 和 Reimu,增加了角色选择选项。
实现了新的桌面屏幕:CharacterSelectScreen(角色选择屏幕)、GameScreen(游戏屏幕)、MainMenuScreen(主菜单屏幕)和 StartScreen(开始屏幕),以改善用户导航。
通过新的 ConnectClient、ConnectServer 和 NetworkManager 类建立了网络功能。
更新了工具类:ClearScreen、Font 和 SimpleFormatter,以提升功能。
创建了新的 ButtonActions 类来处理按钮交互。
This commit is contained in:
2025-09-23 21:46:12 +08:00
parent 7a47759cf4
commit 5f080713f8
85 changed files with 7379 additions and 1651 deletions

View File

@@ -4,57 +4,73 @@ import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
/**
* 角色类继承自Fighter父类
* Alice角色类继承自Fighter父类,定义其专属属性和动画
*/
public class Alice extends Fighter {
private TextureAtlas atlas;
public Alice() {
super(new TextureAtlas(Gdx.files.internal("src\\main\\resources\\character\\alice\\精灵1.2.atlas")));
speed = 350f; // 速度更快
maxHealth = 90; // 生命值较低
health = maxHealth;
attackPower = 12; // 攻击力中等\
super(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() {
// 加载基础动作动画
loadAnimationFromAtlas(Action.IDLE, "stand/stand", 15, true);
loadAnimationFromAtlas(Action.WALK, "walkFront/walkFront", 9, true);
loadAnimationFromAtlas(Action.JUMP, "jump/jump", 8, false);
loadAnimationFromAtlas(Action.FALL, "hitSpin/hitSpin", 5, false);
// 加载攻击动作动画
loadAnimationFromAtlas(Action.ATTACK1, "attackAa/attackAa", 6, false);
loadAnimationFromAtlas(Action.ATTACK2, "attackAb/attackAb", 6, false);
loadAnimationFromAtlas(Action.ATTACK3, "attackAc/attackAc", 6, false);
loadAnimationFromAtlas(Action.ATTACK4, "attackAd/attackAd", 6, false);
// 加载受击动画
loadAnimationFromAtlas(Action.HIT, "hitSpin/hitSpin", 5, false);
// 为特定动作设置帧间隔
// 设置帧间隔(动作速度)
setFrameDuration(Action.IDLE, 0.04f);
setFrameDuration(Action.WALK, 0.08f); // 行走更快
setFrameDuration(Action.ATTACK1, 0.07f); // 攻击更快
setFrameDuration(Action.SPECIAL2, 0.06f); // 特殊技能2非常快
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.SPECIAL1 &&
currentAction != Action.SPECIAL2 && currentAction != Action.DEFEND) {
if (currentAction != Action.JUMP && currentAction != Action.FALL) {
changeAction(Action.WALK);
}
if (currentAction != Action.ATTACK1 &&
currentAction != Action.ATTACK2 &&
currentAction != Action.ATTACK3 &&
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,117 +0,0 @@
package uno.mloluyu.characters;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import uno.mloluyu.characters.Alice;
import uno.mloluyu.characters.Fighter;
public class AliceAnimationTest extends ApplicationAdapter {
private SpriteBatch batch;
private OrthographicCamera camera;
private Viewport viewport;
private Alice alice;
private float stateTimer; // 用于切换动作测试
@Override
public void create() {
// 初始化相机和批处理
camera = new OrthographicCamera();
viewport = new FitViewport(800, 600, camera);
batch = new SpriteBatch();
// 创建 Alice 实例
alice = new Alice();
// 初始位置
alice.getHitbox().setPosition(
viewport.getWorldWidth() / 2 - alice.getHitbox().width / 2,
viewport.getWorldHeight() / 2 - alice.getHitbox().height / 2
);
stateTimer = 0;
}
@Override
public void render() {
// 清屏
Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// 更新状态时间
float delta = Gdx.graphics.getDeltaTime();
stateTimer += delta;
// 控制 Alice 执行不同动作,测试动画切换
controlAliceActions();
// 更新 Alice
alice.update(delta);
// 绘制
batch.setProjectionMatrix(camera.combined);
batch.begin();
alice.render(batch);
batch.end();
}
/**
* 自动切换 Alice 动作,测试各种动画
*/
private void controlAliceActions() {
// 每2秒切换一个动作
if (stateTimer < 2) {
alice.changeAction(Fighter.Action.IDLE);
} else if (stateTimer < 4) {
alice.move(1, Gdx.graphics.getDeltaTime()); // 向右走
} else if (stateTimer < 6) {
alice.changeAction(Fighter.Action.JUMP);
} else if (stateTimer < 8) {
alice.attack(1); // 普通攻击1
} else if (stateTimer < 10) {
alice.attack(2); // 普通攻击2
} else if (stateTimer < 12) {
alice.attack(3); // 普通攻击3
} else if (stateTimer < 14) {
alice.takeHit(10); // 受击
} else {
// 循环
stateTimer = 0;
alice.getHitbox().setPosition(
viewport.getWorldWidth() / 2 - alice.getHitbox().width / 2,
viewport.getWorldHeight() / 2 - alice.getHitbox().height / 2
);
}
}
@Override
public void resize(int width, int height) {
viewport.update(width, height);
}
@Override
public void dispose() {
batch.dispose();
alice.dispose();
}
// 直接运行这个 main 方法即可启动测试
// public static void main(String[] args) {
// Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration();
// config.setTitle("Alice Animation Test");
// config.setWindowedMode(800, 600);
// config.setForegroundFPS(60);
// config.useVsync(true);
// new Lwjgl3Application(new AliceAnimationTest(), config);
// }
}

View File

@@ -11,10 +11,13 @@ import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Disposable;
import java.util.EnumMap;
/**
* 格斗角色父类,封装所有角色共有的动画和状态管理逻辑
*/
public abstract class Fighter implements Disposable {
public enum Action {
IDLE, WALK, JUMP, FALL,
ATTACK1, ATTACK2, ATTACK3, ATTACK4,
@@ -22,328 +25,198 @@ public abstract class Fighter implements Disposable {
SPECIAL1, SPECIAL2,
DEATH
}
protected String name;
// 画帧间隔(秒)
protected static final float DEFAULT_FRAME_DURATION = 0.1f;
protected float[] frameDurations;
protected static final int DEFAULT_HEALTH = 100;
protected static final float DEFAULT_SPEED = 300f;
// 当前状态
protected Action currentAction;
protected float stateTime;
protected boolean isFacingRight;
protected boolean isAnimationFinished;
protected String name;
protected Action currentAction = Action.IDLE;
protected float stateTime = 0f;
protected boolean isFacingRight = true;
protected boolean isAnimationFinished = false;
// 动画集合
protected Animation<TextureRegion>[] animations;
protected EnumMap<Action, Animation<TextureRegion>> animations = new EnumMap<>(Action.class);
protected EnumMap<Action, Float> frameDurations = new EnumMap<>(Action.class);
// 碰撞检测
protected Rectangle hitbox;
protected Rectangle attackbox;
protected Rectangle hitbox = new Rectangle(0, 0, 64, 128);
protected Rectangle attackbox = new Rectangle(0, 0, 80, 80);
// 角色属性
protected float speed;
protected int health;
protected int maxHealth;
protected int attackPower;
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;
@SuppressWarnings("unchecked")
public Fighter() {
}
public Fighter(TextureAtlas atlas) {
this.atlas = atlas;
int actionCount = Action.values().length;
animations = new Animation[actionCount];
frameDurations = new float[actionCount];
for (int i = 0; i < actionCount; i++) {
frameDurations[i] = DEFAULT_FRAME_DURATION;
for (Action action : Action.values()) {
frameDurations.put(action, DEFAULT_FRAME_DURATION);
}
hitbox = new Rectangle(0, 0, 64, 128);
attackbox = new Rectangle(0, 0, 80, 80);
speed = 300f;
maxHealth = 100;
health = maxHealth;
attackPower = 10;
isFacingRight = true;
currentAction = Action.IDLE;
stateTime = 0;
isAnimationFinished = false;
loadAnimations();
}
/**
* 加载角色动画,由子类实现具体的动画帧加载
*/
protected abstract void loadAnimations();
/**
* 从精灵图表加载动画
*
* @param action 动作类型
* @param regionPrefix 该动作在图表中的区域前缀
* @param frameCount 帧数
* @param loop 是否循环
*/
protected void loadAnimationFromAtlas(Action action, String regionPrefix,
int frameCount, boolean loop) {
if (atlas == null) {
protected void loadAnimationFromAtlas(Action action, String regionPrefix, int frameCount, boolean loop) {
if (atlas == null)
throw new IllegalStateException("TextureAtlas 未初始化!");
}
if (frameCount <= 0) {
if (frameCount <= 0)
throw new IllegalArgumentException("帧数必须大于0: " + frameCount);
}
if (frameDurations == null || frameDurations.length <= action.ordinal()) {
throw new IllegalStateException("frameDurations 未初始化或大小不足");
}
Array<TextureRegion> frames = new Array<>();
for (int i = 0; i < frameCount; i++) {
String formattedIndex = SimpleFormatter.addLeadingZeros(i, 3);
String regionName = regionPrefix + formattedIndex;
String regionName = regionPrefix + SimpleFormatter.addLeadingZeros(i, 3);
TextureRegion region = atlas.findRegion(regionName);
if (region == null) {
throw new IllegalArgumentException("精灵图表中未找到区域: " + regionName +
" (前缀: " + regionPrefix + ", 索引: " + i + ")");
throw new IllegalArgumentException("未找到区域: " + regionName);
}
frames.add(region);
}
Animation<TextureRegion> animation = new Animation<>(
frameDurations[action.ordinal()],
frames);
Animation<TextureRegion> animation = new Animation<>(frameDurations.get(action), frames);
animation.setPlayMode(loop ? Animation.PlayMode.LOOP : Animation.PlayMode.NORMAL);
animations[action.ordinal()] = animation;
animations.put(action, animation);
}
/**
* 为特定动作设置帧间隔
*/
protected void setFrameDuration(Action action, float duration) {
frameDurations[action.ordinal()] = duration;
if (animations[action.ordinal()] != null) {
animations[action.ordinal()].setFrameDuration(duration);
}
frameDurations.put(action, duration);
Animation<TextureRegion> anim = animations.get(action);
if (anim != null)
anim.setFrameDuration(duration);
}
/**
* 更新角色状态
*/
public void update(float deltaTime) {
stateTime += deltaTime;
isAnimationFinished = animations[currentAction.ordinal()].isAnimationFinished(stateTime);
Animation<TextureRegion> anim = animations.get(currentAction);
if (anim != null) {
isAnimationFinished = anim.isAnimationFinished(stateTime);
}
handleAnimationTransitions();
updateHitboxes();
}
/**
* 处理动画完成后的状态转换,子类可以重写以实现特定逻辑
*/
protected void handleAnimationTransitions() {
Animation<TextureRegion> currentAnim = animations[currentAction.ordinal()];
if (!isAnimationFinished)
return;
if (currentAnim.getPlayMode() != Animation.PlayMode.LOOP && isAnimationFinished) {
switch (currentAction) {
case ATTACK1:
case ATTACK2:
case ATTACK3:
case SPECIAL1:
case SPECIAL2:
changeAction(Action.IDLE);
break;
case HIT:
changeAction(Action.IDLE);
break;
case JUMP:
changeAction(Action.FALL);
break;
}
switch (currentAction) {
case ATTACK1:
case ATTACK2:
case ATTACK3:
case SPECIAL1:
case SPECIAL2:
case HIT:
changeAction(Action.IDLE);
break;
case JUMP:
changeAction(Action.FALL);
break;
default:
break;
}
}
/**
* 绘制角色
*/
public void render(SpriteBatch batch) {
Animation<TextureRegion> currentAnimation = animations[currentAction.ordinal()];
if (currentAnimation == null) {
Animation<TextureRegion> anim = animations.get(currentAction);
if (anim == null) {
Gdx.app.error("Fighter", "动画未初始化: " + currentAction);
return;
}
boolean loop = currentAnimation.getPlayMode() == Animation.PlayMode.LOOP;
TextureRegion currentFrame = currentAnimation.getKeyFrame(stateTime, loop);
if (currentFrame == null) {
TextureRegion frame = anim.getKeyFrame(stateTime, anim.getPlayMode() == Animation.PlayMode.LOOP);
if (frame == null) {
Gdx.app.error("Fighter", "动画帧为空: " + currentAction);
return;
}
// 1. 计算缩放后的帧尺寸(保持原始比例)
float frameWidth = currentFrame.getRegionWidth() * scaleX;
float frameHeight = currentFrame.getRegionHeight() * scaleY;
float frameWidth = frame.getRegionWidth() * scaleX;
float frameHeight = frame.getRegionHeight() * scaleY;
float drawX = hitbox.x + (hitbox.width - frameWidth) / 2;
float drawY = hitbox.y;
// 2. 计算绘制位置始终以hitbox为基准水平居中、底部对齐关键
// 无论是否翻转x/y坐标都基于hitbox计算保证位置锚点一致
float drawX = hitbox.x + (hitbox.width - frameWidth) / 2; // 水平居中hitbox中心和帧中心对齐
float drawY = hitbox.y; // 底部对齐hitbox底部和帧底部对齐
boolean wasFlippedX = frame.isFlipX();
frame.flip(!isFacingRight && !wasFlippedX, false);
frame.flip(isFacingRight && wasFlippedX, false);
// 3. 处理翻转用TextureRegion的flip方法避免手动偏移x坐标
// 先记录原始flip状态防止影响其他地方复用该帧
boolean wasFlippedX = currentFrame.isFlipX();
// 根据朝向设置翻转只翻转X轴Y轴不变
currentFrame.flip(!isFacingRight && !wasFlippedX, false); // 正向→不翻,反向→翻
currentFrame.flip(isFacingRight && wasFlippedX, false); // 修复原始已翻转的情况
// 4. 绘制:缩放中心为帧的中心,确保翻转/旋转时围绕自身中心
batch.draw(
currentFrame,
drawX, // 绘制X基于hitbox的居中位置固定不变
drawY, // 绘制Y基于hitbox的底部固定不变
frameWidth / 2, // 缩放/旋转中心X帧的中心
frameHeight / 2, // 缩放/旋转中心Y帧的中心
frameWidth, // 缩放后的宽度已乘scaleX
frameHeight, // 缩放后的高度已乘scaleY
1f, // X轴额外缩放这里已提前计算设为1避免重复缩放
1f, // Y轴额外缩放同上
0f // 旋转角度
);
// 5. 恢复帧的原始flip状态关键避免影响后续绘制
currentFrame.flip(wasFlippedX != currentFrame.isFlipX(), false);
batch.draw(frame, drawX, drawY, frameWidth / 2, frameHeight / 2, frameWidth, frameHeight, 1f, 1f, 0f);
frame.flip(wasFlippedX != frame.isFlipX(), false);
}
/**
* 改变角色动作
*/
public boolean changeAction(Action newAction) {
if (isActionUninterruptible(currentAction)) {
if (isActionUninterruptible(currentAction))
return false;
}
if (currentAction != newAction) {
currentAction = newAction;
stateTime = 0;
stateTime = 0f;
isAnimationFinished = false;
return true;
}
return false;
}
/**
* 判断动作是否不可被打断
*/
protected boolean isActionUninterruptible(Action action) {
return action == Action.HIT || action == Action.DEATH;
}
/**
* 更新碰撞框位置
*/
protected void updateHitboxes() {
if (isFacingRight) {
attackbox.setPosition(hitbox.x + hitbox.width - 10, hitbox.y + 20);
} else {
attackbox.setPosition(hitbox.x - attackbox.width + 10, hitbox.y + 20);
}
float offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + 20);
}
/**
* 处理角色移动
*/
public void move(float x, float deltaTime) {
if (x != 0) {
isFacingRight = x > 0;
hitbox.x += x * speed * deltaTime;
handleMoveState();
} else if (currentAction == Action.WALK) {
changeAction(Action.IDLE);
}
}
/**
* 处理移动状态转换,子类可重写
*/
protected void handleMoveState() {
if (currentAction != Action.ATTACK1 && currentAction != Action.ATTACK2 &&
currentAction != Action.ATTACK3 && currentAction != Action.SPECIAL1 &&
currentAction != Action.SPECIAL2 && currentAction != Action.JUMP &&
currentAction != Action.FALL && currentAction != Action.DEFEND) {
if (!isActionUninterruptible(currentAction) &&
currentAction != Action.JUMP &&
currentAction != Action.FALL &&
currentAction != Action.DEFEND &&
!currentAction.name().startsWith("ATTACK") &&
!currentAction.name().startsWith("SPECIAL")) {
changeAction(Action.WALK);
}
}
/**
* 执行攻击
*/
public boolean attack(int attackType) {
if (!canAttack()) {
if (!canAttack())
return false;
}
Action attackAction;
switch (attackType) {
case 1:
attackAction = Action.ATTACK1;
break;
case 2:
attackAction = Action.ATTACK2;
break;
case 3:
attackAction = Action.ATTACK3;
break;
case 4:
attackAction = Action.SPECIAL1;
break;
case 5:
attackAction = Action.SPECIAL2;
break;
default:
return false;
}
Action attackAction = switch (attackType) {
case 1 -> Action.ATTACK1;
case 2 -> Action.ATTACK2;
case 3 -> Action.ATTACK3;
case 4 -> Action.SPECIAL1;
case 5 -> Action.SPECIAL2;
default -> null;
};
return changeAction(attackAction);
return attackAction != null && changeAction(attackAction);
}
/**
* 判断是否可以攻击,子类可重写以实现特定逻辑
*/
protected boolean canAttack() {
return currentAction == Action.IDLE || currentAction == Action.WALK;
}
/**
* 处理受击
*/
public void takeHit(int damage) {
if (currentAction != Action.DEATH) {
health -= damage;
if (health <= 0) {
health = 0;
changeAction(Action.DEATH);
} else {
changeAction(Action.HIT);
}
}
if (currentAction == Action.DEATH)
return;
health = Math.max(0, health - damage);
changeAction(health == 0 ? Action.DEATH : Action.HIT);
}
public Rectangle getHitbox() {
@@ -362,21 +235,29 @@ public abstract class Fighter implements Disposable {
return health;
}
public int getMaxHealth() {
return maxHealth;
@Override
public void dispose() {
if (atlas != null)
atlas.dispose();
}
public Action getCurrentAction() {
return currentAction;
}
public int getAttackPower() {
return attackPower;
public float getX() {
return hitbox.x;
}
public String getName(){ return ""; }
public float getY() {
return hitbox.y;
}
@Override
public void dispose() {
public float getCenterX() {
return hitbox.x + hitbox.width / 2;
}
public float getCenterY() {
return hitbox.y + hitbox.height / 2;
}
}

View File

@@ -0,0 +1,70 @@
package uno.mloluyu.characters;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
public class Reimu extends Fighter {
public Reimu() {
super(new TextureAtlas(Gdx.files.internal("src\\main\\resources\\character\\reimu\\reimu.atlas")));
// 设置角色属性
speed = 350f; // 更快的移动速度
maxHealth = 90; // 较低的生命值
health = maxHealth;
attackPower = 12; // 中等攻击力
}
@Override
protected void loadAnimations() {
// TODO Auto-generated method stub
// 加载基础动作动画
loadAnimationFromAtlas(Action.IDLE, "other/stand", 9, true);
loadAnimationFromAtlas(Action.WALK, "other/walkFront", 9, true);
loadAnimationFromAtlas(Action.JUMP, "other/jump", 8, false);
loadAnimationFromAtlas(Action.FALL, "other/hitSpin", 5, false);
// 加载攻击动作动画
loadAnimationFromAtlas(Action.ATTACK1, "attackAa/attackAa", 6, false);
loadAnimationFromAtlas(Action.ATTACK2, "attackAb/attackAb", 6, false);
loadAnimationFromAtlas(Action.ATTACK3, "attackAc/attackAc", 6, false);
loadAnimationFromAtlas(Action.ATTACK4, "attackAd/attackAd", 6, false);
// 加载受击动画
loadAnimationFromAtlas(Action.HIT, "hitSpin/hitSpin", 5, false);
// 设置帧间隔(动作速度)
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.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;
}
}