移除未使用的字符类及相关资源;更新二进制文件和项目结构。(完成联机和攻击框)
This commit is contained in:
4
pom.xml
4
pom.xml
@@ -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>
|
||||
|
||||
@@ -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 调整伤害或触发粒子/声音,但不要忘记保留父类的状态设置
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() -> {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
@@ -45,7 +47,6 @@ public class NetworkManager {
|
||||
|
||||
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);
|
||||
@@ -57,7 +58,6 @@ public class NetworkManager {
|
||||
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);
|
||||
@@ -67,7 +67,6 @@ public class NetworkManager {
|
||||
}
|
||||
|
||||
public void receiveMessage(String message) {// 解析消息
|
||||
Gdx.app.log("Network", "收到消息: " + message);
|
||||
|
||||
if (message.startsWith("POS:")) {
|
||||
String[] parts = message.substring(4).split(",");
|
||||
@@ -77,7 +76,6 @@ public class NetworkManager {
|
||||
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);
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
1400
src/main/resources/character/alice/alice.atlas
Normal file
1400
src/main/resources/character/alice/alice.atlas
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/main/resources/character/alice/alice.png
Normal file
BIN
src/main/resources/character/alice/alice.png
Normal file
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 |
1400
target/classes/character/alice/alice.atlas
Normal file
1400
target/classes/character/alice/alice.atlas
Normal file
File diff suppressed because it is too large
Load Diff
BIN
target/classes/character/alice/alice.png
Normal file
BIN
target/classes/character/alice/alice.png
Normal file
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,2 @@
|
||||
uno\mloluyu\network\ConnectServer.class
|
||||
uno\mloluyu\characters\character\Fighter$1.class
|
||||
uno\mloluyu\util\SimpleFormatter.class
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user