移除未使用的字符类及相关资源;更新二进制文件和项目结构。(完成联机和攻击框)

This commit is contained in:
2025-09-25 21:24:10 +08:00
parent e67428431f
commit 8e07b3204a
46 changed files with 3031 additions and 3553 deletions

View File

@@ -89,7 +89,7 @@
<configuration>
<!-- 确保这个类路径与你实际的Launcher类位置一致 -->
<!-- 例如如果你的类文件在src/main/java/uno/mloluyu/Launcher.java -->
<mainClass>uno.mloluyu.desktop.Launcher</mainClass>
<mainClass>uno.mloluyu.desktop.DesktopLauncher</mainClass>
</configuration>
</plugin>
@@ -102,7 +102,7 @@
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>uno.mloluyu.desktop.Launcher</mainClass>
<mainClass>uno.mloluyu.desktop.DesktopLauncher</mainClass>
</manifest>
</archive>
</configuration>

View File

@@ -8,23 +8,9 @@ public class AdvancedFighter extends SimpleFighter {
@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;
}
// 先使用父类的攻击逻辑来保证 isAttacking/attackTimer/attackbox 等状态被正确设置
super.attack(attackType);
// 在这里可以添加 AdvancedFighter 特有的扩展行为(攻击力、特效等)
// 例如:根据 attackType 调整伤害或触发粒子/声音,但不要忘记保留父类的状态设置
}
}

View File

@@ -3,14 +3,13 @@ package uno.mloluyu.characters;
// 注意:本类使用的是包 uno.mloluyu.characters 下的 Action (IDLE, JUMP, MOVE, ATTACK, DEFEND, HIT, DEAD)
// 避免与 uno.mloluyu.characters.character.Action (ATTACK1/2/3...) 混淆
// 简化:去除内部按键时长跟踪,统一由 FighterController 负责
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;
import uno.mloluyu.network.NetworkManager;
/**
* 简化版角色类,仅包含移动、攻击、受击等基础功能。
@@ -18,51 +17,27 @@ import com.badlogic.gdx.math.Rectangle;
public class SimpleFighter {
private String name; // 角色名称
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 boolean isAttacking = false; // 是否正在攻击(攻击状态标志)
private boolean attackJustStarted = false; // 攻击刚开始的标记,避免第一帧被减掉
private int attackInvokeCount = 0; // 调试attack()调用次数
// 攻击持续时间(秒)
private float attackTimer = 0f;
private static final float ATTACK_DURATION = 0.15f; // 攻击判定显示时间
private static boolean debugEnabled = true; // F3 开关
public static void toggleDebug() {
debugEnabled = !debugEnabled;
}
public static boolean isDebugEnabled() {
return debugEnabled;
}
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 boolean isAttacking = false; // 是否正在攻击
private boolean attackJustStarted = false; // 攻击是否刚开始
private float attackTimer = 0f; // 攻击计时器
private static final float ATTACK_DURATION = 0.15f; // 攻击持续时间
public SimpleFighter(String name) {
this.name = name; // 构造函数,初始化角色名称
this.name = name;
}
public void update(float deltaTime) {
// 自愈:动作是 ATTACK 但标记丢失
if (currentAction == Action.ATTACK && attackTimer > 0f && !isAttacking) {
isAttacking = true;
attackJustStarted = false;
}
// 攻击计时
if (isAttacking) {
if (attackJustStarted) {
attackJustStarted = false; // 第一帧不扣时间
attackJustStarted = false;
} else {
attackTimer -= deltaTime;
}
@@ -71,15 +46,11 @@ public class SimpleFighter {
attackTimer = 0f;
if (currentAction == Action.ATTACK)
changeAction(Action.IDLE);
if (debugEnabled)
System.out.println("[ATTACK-END]");
}
} else {
// 空闲/移动状态下保持一个默认攻击盒(便于调试观察)
updateAttackbox("light");
}
// 垂直运动 & 重力
if (!isGrounded) {
verticalSpeed -= 2500 * deltaTime;
hitbox.y += verticalSpeed * deltaTime;
@@ -92,36 +63,23 @@ public class SimpleFighter {
}
}
@Deprecated
public void render(SpriteBatch batch, ShapeRenderer shapeRenderer) {
batch.end();
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
renderDebug(shapeRenderer);
shapeRenderer.end();
batch.begin();
public void renderSprite(SpriteBatch batch) {
}
public void renderSprite(SpriteBatch batch) {
/* 预留贴图渲染入口 */ }
public void renderDebug(ShapeRenderer sr) {
if (!debugEnabled)
return;
sr.setColor(Color.BLUE);
sr.rect(hitbox.x, hitbox.y, hitbox.width, hitbox.height);
if (isAttacking) {
// 只画轮廓;填充在 GameScreen 专门的 Filled pass 中画
sr.setColor(Color.RED);
sr.rect(attackbox.x, attackbox.y, attackbox.width, attackbox.height);
}
// 朝向箭头
float arrowX = isFacingRight ? hitbox.x + hitbox.width + 5 : hitbox.x - 15;
sr.setColor(Color.YELLOW);
sr.line(arrowX, hitbox.y + hitbox.height * 0.7f, arrowX + (isFacingRight ? 10 : -10),
hitbox.y + hitbox.height * 0.7f);
}
public void handleInput(int keycode, boolean isPressed, float duration) {
public void handleInput(int keycode, boolean isPressed) {
if (isPressed) {
if (keycode == Input.Keys.LEFT || keycode == Input.Keys.A) {
move(-1, Gdx.graphics.getDeltaTime());
@@ -131,42 +89,38 @@ public class SimpleFighter {
if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
jump();
}
// 攻击按键
if (!isAttacking) {
if (keycode == Input.Keys.Z || keycode == Input.Keys.J) {
attack("light");
NetworkManager.getInstance().sendAttack("light");
} else if (keycode == Input.Keys.X || keycode == Input.Keys.K) {
attack("heavy");
NetworkManager.getInstance().sendAttack("heavy");
} else if (keycode == Input.Keys.SHIFT_LEFT || keycode == Input.Keys.SHIFT_RIGHT) {
attack("special");
NetworkManager.getInstance().sendAttack("special");
}
}
} else {
if ((keycode == Input.Keys.LEFT || keycode == Input.Keys.RIGHT || keycode == Input.Keys.A
|| keycode == Input.Keys.D) &&
getCurrentAction() == Action.MOVE) {
|| keycode == Input.Keys.D) && getCurrentAction() == Action.MOVE) {
changeAction(Action.IDLE);
}
}
}
public void handleInput(int keycode, boolean isPressed) {
handleInput(keycode, isPressed, 0f); // 调用已有方法,补充默认持续时间
}
public Action getCurrentAction() {
return currentAction; // 获取当前动作状态
return currentAction;
}
public void changeAction(Action newAction) {
this.currentAction = newAction; // 切换角色动作状态
this.currentAction = newAction;
}
public void jump() {
if (isGrounded) {
verticalSpeed = 1000f;
isGrounded = false;
System.out.println("跳跃高度: " + verticalSpeed);
changeAction(Action.JUMP);
}
}
@@ -175,18 +129,14 @@ public class SimpleFighter {
if (x != 0) {
isFacingRight = x > 0;
hitbox.x += x * speed * deltaTime;
changeAction(Action.MOVE); // 移动时切换为 MOVE 状态
changeAction(Action.MOVE);
} else if (isGrounded && !isAttacking) {
changeAction(Action.IDLE); // 停止移动时恢复待机
changeAction(Action.IDLE);
}
}
private void updateAttackbox(String attackType) {
float offsetX;
float offsetY = 20; // 默认偏移量
float width = 80;
float height = 80;
float offsetX, offsetY = 20, width = 80, height = 80;
switch (attackType) {
case "heavy":
offsetX = isFacingRight ? hitbox.width : -100;
@@ -206,7 +156,6 @@ public class SimpleFighter {
default:
offsetX = isFacingRight ? hitbox.width - 10 : -attackbox.width + 10;
}
attackbox.setPosition(hitbox.x + offsetX, hitbox.y + offsetY);
attackbox.setSize(width, height);
}
@@ -217,59 +166,42 @@ public class SimpleFighter {
attackJustStarted = true;
changeAction(Action.ATTACK);
updateAttackbox(attackType);
attackInvokeCount++;
if (debugEnabled)
System.out.println("[ATTACK] type=" + attackType + " count=" + attackInvokeCount);
}
public void takeHit(int damage) {
health = Math.max(0, health - damage); // 扣除生命值,最小为 0
changeAction(health > 0 ? Action.HIT : Action.DEAD); // 根据生命值切换为受击或死亡状态
health = Math.max(0, health - damage);
changeAction(health > 0 ? Action.HIT : Action.DEAD);
}
public boolean isAlive() {
return health > 0; // 判断角色是否存活
return health > 0;
}
public boolean isAttacking() {
return isAttacking; // 判断是否处于攻击状态
return isAttacking;
}
// 常用访问器
public Rectangle getHitbox() {
return hitbox; // 获取碰撞盒
return hitbox;
}
public Rectangle getAttackbox() {
return attackbox; // 获取攻击盒
return attackbox;
}
public int getHealth() {
return health; // 获取当前生命值
return health;
}
public String getName() {
return name; // 获取角色名称
return name;
}
public void setPosition(float x, float y) {
hitbox.setPosition(x, y); // 设置角色位置
}
public void debugPrintState() {
if (debugEnabled)
System.out.println("[STATE] action=" + currentAction + ", atk=" + isAttacking + ", t=" + attackTimer);
hitbox.setPosition(x, y);
}
public float getAttackTimer() {
return attackTimer;
}
public float getAttackTimerPercent() {
return isAttacking ? attackTimer / ATTACK_DURATION : 0f;
}
public int getAttackInvokeCount() {
return attackInvokeCount;
}
}

View File

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

View File

@@ -1,70 +0,0 @@
package uno.mloluyu.characters.character;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
/**
* Alice角色类继承自Fighter父类定义其专属属性和动画
*/
public class Alice extends Fighter {
private static final String ATLAS_PATH = "src/main/resources/character/alice/精灵1.2.atlas";
public Alice() {
super("Alice", new TextureAtlas(Gdx.files.internal(ATLAS_PATH)));
speed = 350f;
maxHealth = 90;
health = maxHealth;
attackPower = 12;
}
@Override
protected void loadAnimations() {
animationManager.loadLooping(Action.IDLE, "stand/stand", 15);
animationManager.loadLooping(Action.WALK, "walkFront/walkFront", 9);
animationManager.loadOneShot(Action.JUMP, "jump/jump", 8);
animationManager.loadOneShot(Action.FALL, "hitSpin/hitSpin", 5);
animationManager.loadOneShot(Action.ATTACK1, "attackAa/attackAa", 6);
animationManager.loadOneShot(Action.ATTACK2, "attackAb/attackAb", 6);
animationManager.loadOneShot(Action.ATTACK3, "attackAc/attackAc", 6);
animationManager.loadOneShot(Action.ATTACK4, "attackAd/attackAd", 6);
animationManager.loadOneShot(Action.HIT, "hitSpin/hitSpin", 5);
// animationManager.loadOneShot(Action.DEATH, "death/death", 8);
// 可选特殊动作(如资源存在可启用)
// animationManager.loadOneShot(Action.SPECIAL1, "special/special1", 6);
// animationManager.loadOneShot(Action.SPECIAL2, "special/special2", 6);
animationManager.setFrameDuration(Action.IDLE, 0.04f);
animationManager.setFrameDuration(Action.WALK, 0.08f);
animationManager.setFrameDuration(Action.ATTACK1, 0.07f);
animationManager.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,287 +0,0 @@
package uno.mloluyu.characters.character;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
/**
* 抽象类 Fighter定义所有角色的基础属性与行为。
* 包括动画控制、移动、攻击、受击、渲染等核心逻辑。
*/
public abstract class Fighter implements Disposable {
// 默认帧持续时间(秒)
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 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 FighterAnimationManager animationManager;
/**
* 构造函数,初始化角色名称与动画资源。
*
* @param name 角色名称
* @param atlas 动画图集
*/
public Fighter(String name, TextureAtlas atlas) {
this.name = name;
this.animationManager = new FighterAnimationManager(atlas);
loadAnimations();
}
/**
* 加载角色的所有动画资源,由子类实现。
*/
protected abstract void loadAnimations();
/**
* 每帧更新角色状态,包括动画播放与碰撞盒更新。
*
* @param deltaTime 帧间隔时间
*/
public void update(float deltaTime) {
stateTime += deltaTime;
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;
switch (currentAction) {
case ATTACK1, ATTACK2, ATTACK3, ATTACK4, SPECIAL1, SPECIAL2, HIT -> changeAction(Action.IDLE);
case JUMP -> changeAction(Action.FALL);
default -> {
}
}
}
/**
* 切换角色动作状态。
*
* @param newAction 新动作
* @return 是否成功切换
*/
public boolean changeAction(Action newAction) {
if (isActionUninterruptible(currentAction))
return false;
if (currentAction != newAction) {
currentAction = newAction;
stateTime = 0f;
isAnimationFinished = false;
return true;
}
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;
hitbox.x += x * speed * deltaTime;
handleMoveState();
} else if (currentAction == Action.WALK) {
changeAction(Action.IDLE);
}
}
/**
* 移动状态下的动作切换逻辑。
*/
protected void handleMoveState() {
if (!isActionUninterruptible(currentAction) &&
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;
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 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;
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;
}
public Rectangle getHitbox() {
return hitbox;
}
public Rectangle getAttackbox() {
return attackbox;
}
public boolean isFacingRight() {
return isFacingRight;
}
public int getHealth() {
return health;
}
public Action getCurrentAction() {
return currentAction;
}
public float getX() {
return hitbox.x;
}
public float getY() {
return hitbox.y;
}
public float getCenterX() {
return hitbox.x + hitbox.width / 2;
}
public float getCenterY() {
return hitbox.y + hitbox.height / 2;
}
/**
* 帧事件监听器接口,用于在动画播放到某一帧时触发逻辑。
*/
public interface FrameEventListener {
void onFrameEvent(Action action, int frameIndex);
}
/**
* 释放资源(如动画图集)。
*/
@Override
public void dispose() {
animationManager.dispose();
}
}

View File

@@ -1,83 +0,0 @@
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,10 +0,0 @@
package uno.mloluyu.characters.character;
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

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

View File

@@ -1,7 +1,6 @@
package uno.mloluyu.desktop;
import java.util.UUID;
import uno.mloluyu.network.NetworkManager;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
@@ -11,10 +10,9 @@ 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.character.Alice;
import uno.mloluyu.characters.AdvancedFighter;
import uno.mloluyu.characters.SimpleFighter;
import uno.mloluyu.characters.character.Reimu;
import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.util.ClearScreen;
import java.util.Arrays;
import java.util.List;
@@ -154,24 +152,30 @@ public class CharacterSelectScreen extends ScreenAdapter {
SimpleFighter fighter = null;
switch (selectedCharacter) {
case "Alice":
fighter = new AdvancedFighter("Alice");
break;
case "Reimu":
fighter = new AdvancedFighter("Reimu");
fighter = new AdvancedFighter(selectedCharacter);
break;
default:
fighter = new SimpleFighter(selectedCharacter);
}
if (fighter != null) {
if (multiplayerMode) {
// 设置唯一玩家 ID 并发送角色选择
if (NetworkManager.getInstance().getLocalPlayerId() == null) {
NetworkManager nm = NetworkManager.getInstance();
// 主机或客户端的 localPlayerId 应该已经在 NetworkSettingsScreen 设置,这里兜底
if (nm.getLocalPlayerId() == null) {
String playerId = UUID.randomUUID().toString();
NetworkManager.getInstance().setLocalPlayerId(playerId);
Gdx.app.log("Network", "设置玩家ID: " + playerId);
nm.setLocalPlayerId(playerId);
Gdx.app.log("Network", "兜底设置玩家ID: " + playerId);
}
if (nm.isConnected()) {
nm.sendCharacterSelection(selectedCharacter);
// 发送初始位置(角色生成初始 hitbox 坐标)
nm.sendPosition(fighter.getHitbox().x, fighter.getHitbox().y);
} else {
Gdx.app.log("Network", "未连接网络,无法发送角色选择");
}
NetworkManager.getInstance().sendCharacterSelection(selectedCharacter);
}
game.setScreen(new GameScreen(game, fighter));
}
}

View File

@@ -15,6 +15,7 @@ import java.util.HashMap;
import java.util.Map;
import uno.mloluyu.characters.SimpleFighter;
import uno.mloluyu.characters.AdvancedFighter;
import uno.mloluyu.network.NetworkManager;
import uno.mloluyu.util.ClearScreen;
import uno.mloluyu.versatile.FighterController;
@@ -26,8 +27,7 @@ public class GameScreen extends ScreenAdapter {
private SpriteBatch batch;
private ShapeRenderer shapeRenderer;
private OrthographicCamera camera; // 添加摄像机
private com.badlogic.gdx.graphics.g2d.BitmapFont debugFont; // 添加 debugFont 字段
private OrthographicCamera camera;
public GameScreen(MainGame game, SimpleFighter player) {
this.player = player;
@@ -42,7 +42,6 @@ public class GameScreen extends ScreenAdapter {
batch = new SpriteBatch();
shapeRenderer = new ShapeRenderer();
debugFont = new com.badlogic.gdx.graphics.g2d.BitmapFont(); // 初始化 debugFont
Gdx.input.setInputProcessor(controller);
}
@@ -58,24 +57,57 @@ public class GameScreen extends ScreenAdapter {
Map<String, float[]> positions = NetworkManager.getInstance().getPlayerPositions();
if (positions != null) {
for (Map.Entry<String, float[]> entry : positions.entrySet()) {
float[] pos = entry.getValue();
if (pos == null)
String playerId = entry.getKey();
// 忽略本地玩家自己,仅渲染其他玩家
if (playerId.equals(NetworkManager.getInstance().getLocalPlayerId())) {
continue;
SimpleFighter remote = otherPlayers.computeIfAbsent(entry.getKey(),
k -> new SimpleFighter("Remote-" + k));
}
float[] pos = entry.getValue();
if (pos == null) {
continue;
}
// 根据网络上的角色选择信息来创建对应类型的远程角色实例,便于未来同步更多行为状态
final String charName = NetworkManager.getInstance().getPlayerCharacters().get(entry.getKey());
SimpleFighter remote = otherPlayers.computeIfAbsent(entry.getKey(), k -> {
if (charName != null) {
switch (charName) {
case "Alice":
case "Reimu":
return new AdvancedFighter(charName);
default:
return new SimpleFighter("Remote-" + k);
}
}
return new SimpleFighter("Remote-" + k);
});
remote.setPosition(pos[0], pos[1]);
remote.update(delta);
}
// 处理远程攻击同步:触发远程角色的攻击动画
Map<String, String> attacks = NetworkManager.getInstance().getPlayerAttacks();
for (Map.Entry<String, String> atk : attacks.entrySet()) {
SimpleFighter remoteAtk = otherPlayers.get(atk.getKey());
if (remoteAtk != null) {
remoteAtk.attack(atk.getValue());
}
}
// F3 调试切换
if (Gdx.input.isKeyJustPressed(com.badlogic.gdx.Input.Keys.F3)) {
SimpleFighter.toggleDebug();
attacks.clear();
}
}
// 摄像机跟随
camera.position.lerp(new Vector3(player.getHitbox().x, player.getHitbox().y, 0), 0.1f);
// 摄像头跟随:若有一个远程玩家,则居中于本地和远程玩家中点
Vector3 targetPos;
if (otherPlayers.size() == 1) {
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);
} else {
// 默认为跟随本地玩家
targetPos = new Vector3(player.getHitbox().x, player.getHitbox().y, 0);
}
camera.position.lerp(targetPos, 0.1f);
camera.update();
batch.setProjectionMatrix(camera.combined);
shapeRenderer.setProjectionMatrix(camera.combined);
@@ -91,6 +123,7 @@ public class GameScreen extends ScreenAdapter {
|| (player.getCurrentAction() == Action.ATTACK && player.getAttackTimer() > 0);
if (showPlayerAttack)
drawAttackBox(player, 1f, 0f, 0f, 0.35f);
for (SimpleFighter remote : otherPlayers.values()) {
drawHitbox(remote, Color.GREEN);
if (remote.isAttacking())
@@ -98,22 +131,12 @@ public class GameScreen extends ScreenAdapter {
}
shapeRenderer.end();
// -------- Sprite / HUD pass --------
// -------- Sprite pass --------
batch.begin();
player.renderSprite(batch);
if (SimpleFighter.isDebugEnabled()) {
debugFont.setColor(Color.WHITE);
debugFont.draw(batch,
"ACTION:" + player.getCurrentAction() +
" atk=" + player.isAttacking() +
" timer=" + String.format("%.2f", player.getAttackTimer()) +
" atkInvoke=" + player.getAttackInvokeCount(),
10, Gdx.graphics.getHeight() - 10);
}
batch.end();
// -------- Debug line pass --------
if (SimpleFighter.isDebugEnabled()) {
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
player.renderDebug(shapeRenderer);
for (SimpleFighter remote : otherPlayers.values())
@@ -121,15 +144,34 @@ public class GameScreen extends ScreenAdapter {
shapeRenderer.setColor(Color.WHITE);
shapeRenderer.rect(0, 0, 1000, 1000);
shapeRenderer.end();
// -------- UI health bar pass --------
// 使用屏幕坐标绘制血条
OrthographicCamera uiCam = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
uiCam.setToOrtho(false);
uiCam.update();
shapeRenderer.setProjectionMatrix(uiCam.combined);
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
// 绘制本地玩家血条(左侧)和所有远程玩家血条(右侧依次排列)
float barWidth = 200f, barHeight = 10f, padding = 10f;
float screenW = Gdx.graphics.getWidth(), screenH = Gdx.graphics.getHeight();
// 本地玩家血条
shapeRenderer.setColor(Color.DARK_GRAY);
shapeRenderer.rect(padding, screenH - padding - barHeight, barWidth, barHeight);
shapeRenderer.setColor(Color.RED);
shapeRenderer.rect(padding, screenH - padding - barHeight,
barWidth * (player.getHealth() / 100f), barHeight);
// 远程玩家血条
int idx = 0;
for (SimpleFighter remote : otherPlayers.values()) {
float x = screenW - padding - barWidth - idx * (barWidth + padding);
shapeRenderer.setColor(Color.DARK_GRAY);
shapeRenderer.rect(x, screenH - padding - barHeight, barWidth, barHeight);
shapeRenderer.setColor(Color.GREEN);
shapeRenderer.rect(x, screenH - padding - barHeight,
barWidth * (remote.getHealth() / 100f), barHeight);
idx++;
}
// 控制台状态输出(保持以便继续诊断)
player.debugPrintState();
if (SimpleFighter.isDebugEnabled() && player.isAttacking()) {
Rectangle ab = player.getAttackbox();
System.out.println("[DEBUG] AttackBox: x=" + ab.x + ", y=" + ab.y + ", w=" + ab.width + ", h=" + ab.height
+ ", timer=" + player.getAttackTimer());
}
shapeRenderer.end();
}
private void drawHitbox(SimpleFighter fighter, Color color) {

View File

@@ -8,7 +8,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.characters.character.Alice;
import uno.mloluyu.versatile.FighterController;
import static uno.mloluyu.util.Font.loadChineseFont;

View File

@@ -72,11 +72,13 @@ public class NetworkSettingsScreen extends ScreenAdapter {
// 创建房间
if (isHovered(mouseX, mouseY, BUTTON_X, CREATE_ROOM_Y)) {
Gdx.app.log("Network", "创建房间按钮被点击!");
NetworkManager.getInstance().createRoom();
NetworkManager.getInstance().joinRoom("127.0.0.1");
Gdx.app.log("Network", "已连接到本地服务器,等待其他玩家加入...");
NetworkManager nm = NetworkManager.getInstance();
nm.createRoom(); // 只创建服务器,不自连,等待其他客户端加入
if (nm.getLocalPlayerId() == null) {
nm.setLocalPlayerId(java.util.UUID.randomUUID().toString());
Gdx.app.log("Network", "房主玩家ID: " + nm.getLocalPlayerId());
}
Gdx.app.log("Network", "房间创建成功,等待客户端加入...");
CharacterSelectScreen characterSelectScreen = new CharacterSelectScreen(game);
characterSelectScreen.setMultiplayerMode(true);
game.setScreen(characterSelectScreen);
@@ -90,7 +92,12 @@ public class NetworkSettingsScreen extends ScreenAdapter {
@Override
public void input(String ip) {
if (ip != null && !ip.trim().isEmpty()) {
NetworkManager.getInstance().joinRoom(ip.trim());
NetworkManager nm = NetworkManager.getInstance();
if (nm.getLocalPlayerId() == null) {
nm.setLocalPlayerId(java.util.UUID.randomUUID().toString());
Gdx.app.log("Network", "客户端玩家ID: " + nm.getLocalPlayerId());
}
nm.joinRoom(ip.trim());
Gdx.app.log("Network", "正在连接到服务器 " + ip.trim() + "...");
Gdx.app.postRunnable(() -> {

View File

@@ -30,6 +30,8 @@ public class ConnectServer implements Runnable {
Socket socket = serverSocket.accept(null);
connectedSockets.add(socket);
Gdx.app.log("Server", "玩家连接成功: " + socket.getRemoteAddress());
// 向新加入的客户端发送当前已有玩家的状态快照(角色选择 + 当前位置)
sendSnapshotTo(socket);
new Thread(() -> handlePlayer(socket)).start();
}
@@ -39,6 +41,29 @@ public class ConnectServer implements Runnable {
}
}
private void sendSnapshotTo(Socket socket) {
try {
NetworkManager nm = NetworkManager.getInstance();
// 发送角色选择快照
for (java.util.Map.Entry<String, String> e : nm.getPlayerCharacters().entrySet()) {
String line = "SELECT:" + e.getKey() + "," + e.getValue();
socket.getOutputStream().write(line.getBytes(StandardCharsets.UTF_8));
}
// 发送位置快照
for (java.util.Map.Entry<String, float[]> e : nm.getPlayerPositions().entrySet()) {
float[] p = e.getValue();
if (p != null && p.length == 2) {
String line = "POS:" + e.getKey() + "," + p[0] + "," + p[1];
socket.getOutputStream().write(line.getBytes(StandardCharsets.UTF_8));
}
}
socket.getOutputStream().flush();
Gdx.app.log("Server", "已发送状态快照给新客户端");
} catch (Exception ex) {
Gdx.app.error("Server", "发送快照失败: " + ex.getMessage(), ex);
}
}
private void handlePlayer(Socket socket) {
try {
byte[] buffer = new byte[1024];

View File

@@ -14,6 +14,8 @@ public class NetworkManager {
private String localCharacter;
private final Map<String, float[]> playerPositions = new HashMap<>();
private final Map<String, String> playerCharacters = new HashMap<>();
// 存储远程玩家的攻击类型attackType
private final Map<String, String> playerAttacks = new HashMap<>();
public static NetworkManager getInstance() {
if (instance == null) {
@@ -30,22 +32,21 @@ 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);
@@ -54,10 +55,9 @@ public class NetworkManager {
}
}
public void sendCharacterSelection(String character) {//发送角色选择消息
public void sendCharacterSelection(String character) {// 发送角色选择消息
this.localCharacter = character;
String msg = "SELECT:" + localPlayerId + "," + character;
Gdx.app.log("Network", "发送角色选择消息: " + msg);
if (isHost && server != null) {
server.broadcastToOthers(null, msg);
receiveMessage(msg);
@@ -66,8 +66,7 @@ public class NetworkManager {
}
}
public void receiveMessage(String message) {//解析消息
Gdx.app.log("Network", "收到消息: " + message);
public void receiveMessage(String message) {// 解析消息
if (message.startsWith("POS:")) {
String[] parts = message.substring(4).split(",");
@@ -76,8 +75,7 @@ public class NetworkManager {
try {
float x = Float.parseFloat(parts[1]);
float y = Float.parseFloat(parts[2]);
playerPositions.put(playerId, new float[]{x, y});
Gdx.app.log("Network", "位置更新: " + playerId + " -> " + x + "," + y);
playerPositions.put(playerId, new float[] { x, y });
} catch (NumberFormatException e) {
Gdx.app.error("Network", "位置解析失败: " + message);
}
@@ -96,6 +94,16 @@ public class NetworkManager {
}
} else if (message.equals("READY")) {
Gdx.app.log("Network", "收到准备信号");
} else if (message.startsWith("ATTACK:")) {
String[] parts = message.substring(7).split(",");
if (parts.length == 2) {
String playerId = parts[0];
String attackType = parts[1];
playerAttacks.put(playerId, attackType);
Gdx.app.log("Network", "攻击同步: " + playerId + " -> " + attackType);
} else {
Gdx.app.error("Network", "攻击消息格式错误: " + message);
}
} else {
Gdx.app.log("Network", "未知消息类型: " + message);
}
@@ -105,6 +113,29 @@ public class NetworkManager {
return playerPositions;
}
/**
* 获取远程玩家的未处理攻击类型映射
*/
public Map<String, String> getPlayerAttacks() {
return playerAttacks;
}
/**
* 发送攻击消息给其他玩家
*/
public void sendAttack(String attackType) {
if (localPlayerId == null)
return;
String msg = "ATTACK:" + localPlayerId + "," + attackType;
Gdx.app.log("Network", "发送攻击消息: " + msg);
if (isHost && server != null) {
server.broadcastToOthers(null, msg);
receiveMessage(msg);
} else if (client != null) {
client.sendMessage(msg);
}
}
public Map<String, String> getPlayerCharacters() {
return playerCharacters;
}

View File

@@ -1,20 +1,12 @@
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.character.Action;
import uno.mloluyu.characters.character.Fighter;
import uno.mloluyu.characters.SimpleFighter;
public class FighterController extends InputAdapter {
private final SimpleFighter fighter;
private final Array<Integer> pressedKeys = new Array<>();
private final Map<Integer, Float> keyPressDuration = new HashMap<>();
public FighterController(SimpleFighter fighter) {
this.fighter = fighter;
@@ -27,44 +19,34 @@ public class FighterController extends InputAdapter {
public void update(float deltaTime) {
if (fighter == null)
return;
for (int keycode : pressedKeys) {
float currentDuration = keyPressDuration.getOrDefault(keycode, 0f);
currentDuration += deltaTime;
keyPressDuration.put(keycode, currentDuration);
fighter.handleInput(keycode, true, currentDuration); // 持续按下的键,传递持续时间
fighter.handleInput(keycode, true); // 持续按下的键,每帧触发
}
}
@Override
public boolean keyDown(int keycode) {
// System.out.println("按键按下: " + keycode);
if (fighter == null)
return false;
if (!pressedKeys.contains(keycode, false)) {
pressedKeys.add(keycode);
keyPressDuration.put(keycode, 0f); // 初始化按键持续时间
}
fighter.handleInput(keycode, true, 0f); // 按下事件,初始持续时间为 0
fighter.handleInput(keycode, true); // 按下事件
return true;
}
@Override
public boolean keyUp(int keycode) {
// System.out.println("按键松开: " + keycode);
if (fighter == null)
return false;
float duration = keyPressDuration.getOrDefault(keycode, 0f);
pressedKeys.removeValue(keycode, false);
keyPressDuration.remove(keycode);
fighter.handleInput(keycode, false, duration); // 按键松开事件,传递持续时间
fighter.handleInput(keycode, false); // 按键松开事件
return true;
}// 松开事件
}
public SimpleFighter getFighter() {
return fighter;

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

View File

@@ -1,3 +1,2 @@
uno\mloluyu\network\ConnectServer.class
uno\mloluyu\characters\character\Fighter$1.class
uno\mloluyu\util\SimpleFormatter.class

View File

@@ -1,11 +1,5 @@
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\AdvancedFighter.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\character\Action.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\character\Alice.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\character\Fighter.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\character\FighterAnimationManager.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\character\FighterList.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\characters\character\Reimu.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\CharacterSelectScreen.java
C:\Users\www\Documents\Game\Game\src\main\java\uno\mloluyu\desktop\DesktopLauncher.java