This commit is contained in:
2025-09-25 22:03:19 +08:00
parent 8e07b3204a
commit 7153ac9b97
6 changed files with 242 additions and 0 deletions

View File

@@ -29,12 +29,36 @@ public class SimpleFighter {
private boolean attackJustStarted = false; // 攻击是否刚开始
private float attackTimer = 0f; // 攻击计时器
private static final float ATTACK_DURATION = 0.15f; // 攻击持续时间
// 新增:连续攻击序号(本地,用于避免重复伤害)
private int attackSequence = 0;
private String lastAttackType = "light"; // 记录最后一次攻击类型,供伤害判定
private int lastDamageAppliedSeq = -1; // 已经对目标造成伤害的序号,避免重复
// 击退 & 无敌
private float knockbackX = 0f;
private float knockbackTimer = 0f;
private float invulnerableTimer = 0f; // 无敌帧时间(被击中后短暂无敌)
private static final float INVULNERABLE_DURATION = 0.3f;
private static final float KNOCKBACK_DURATION = 0.12f;
public SimpleFighter(String name) {
this.name = name;
}
public void update(float deltaTime) {
// 处理无敌帧计时
if (invulnerableTimer > 0) {
invulnerableTimer -= deltaTime;
if (invulnerableTimer < 0)
invulnerableTimer = 0;
}
// 处理击退
if (knockbackTimer > 0) {
hitbox.x += knockbackX * deltaTime;
knockbackTimer -= deltaTime;
if (knockbackTimer <= 0) {
knockbackX = 0;
}
}
if (isAttacking) {
if (attackJustStarted) {
attackJustStarted = false;
@@ -135,6 +159,19 @@ public class SimpleFighter {
}
}
// 供远程同步:根据位置变化量更新朝向(不触发移动动作,只用于攻击盒朝向正确)
public void updateFacingByDelta(float dx) {
if (dx > 0) {
isFacingRight = true;
} else if (dx < 0) {
isFacingRight = false;
}
}
public boolean isFacingRight() {
return isFacingRight;
}
private void updateAttackbox(String attackType) {
float offsetX, offsetY = 20, width = 80, height = 80;
switch (attackType) {
@@ -166,11 +203,21 @@ public class SimpleFighter {
attackJustStarted = true;
changeAction(Action.ATTACK);
updateAttackbox(attackType);
lastAttackType = attackType;
attackSequence++; // 本地每次攻击自增
lastDamageAppliedSeq = -1; // 新一次攻击重置
}
public void takeHit(int damage) {
if (invulnerableTimer > 0 || health <= 0)
return; // 无敌或已死亡
health = Math.max(0, health - damage);
changeAction(health > 0 ? Action.HIT : Action.DEAD);
// 设置无敌 & 击退
invulnerableTimer = INVULNERABLE_DURATION;
// 击退方向与幅度
knockbackX = isFacingRight ? -600f : 600f;
knockbackTimer = KNOCKBACK_DURATION;
}
public boolean isAlive() {
@@ -181,6 +228,10 @@ public class SimpleFighter {
return isAttacking;
}
public boolean isInvulnerable() {
return invulnerableTimer > 0;
}
public Rectangle getHitbox() {
return hitbox;
}
@@ -193,6 +244,43 @@ public class SimpleFighter {
return health;
}
public int getAttackSequence() {
return attackSequence;
}
public String getLastAttackType() {
return lastAttackType;
}
public int getLastDamageAppliedSeq() {
return lastDamageAppliedSeq;
}
public void setLastDamageAppliedSeq(int seq) {
this.lastDamageAppliedSeq = seq;
}
public boolean canDealDamage() {
return isAttacking && attackSequence != lastDamageAppliedSeq; // 未对当前序号造成过伤害
}
public void markDamageApplied() {
lastDamageAppliedSeq = attackSequence;
}
// 根据攻击类型返回伤害数值
public int getDamageForAttack(String type) {
switch (type) {
case "heavy":
return 20;
case "special":
return 30;
case "light":
default:
return 10;
}
}
public String getName() {
return name;
}

View File

@@ -24,6 +24,8 @@ public class GameScreen extends ScreenAdapter {
private final SimpleFighter player;
private final FighterController controller;
private final Map<String, SimpleFighter> otherPlayers = new HashMap<>();
private final Map<String, Float> deathTimers = new HashMap<>(); // 记录远程/本地死亡计时
private static final float RESPAWN_DELAY = 2f;
private SpriteBatch batch;
private ShapeRenderer shapeRenderer;
@@ -80,7 +82,11 @@ public class GameScreen extends ScreenAdapter {
}
return new SimpleFighter("Remote-" + k);
});
// 根据位置变化更新朝向
float oldX = remote.getHitbox().x;
remote.setPosition(pos[0], pos[1]);
float dx = remote.getHitbox().x - oldX;
remote.updateFacingByDelta(dx);
remote.update(delta);
}
// 处理远程攻击同步:触发远程角色的攻击动画
@@ -93,6 +99,92 @@ public class GameScreen extends ScreenAdapter {
}
attacks.clear();
}
// 主机执行碰撞检测并广播伤害
if (NetworkManager.getInstance().isHost()) {
// 只处理本地玩家攻击命中其他人,以及(可扩展)其他远程之间不处理
if (player.isAttacking()) {
for (Map.Entry<String, SimpleFighter> e : otherPlayers.entrySet()) {
SimpleFighter target = e.getValue();
if (target.isAlive() && player.isAttacking() && player.canDealDamage()
&& player.getAttackbox().overlaps(target.getHitbox())) {
// 使用攻击类型伤害
int dmg = player.getDamageForAttack(player.getLastAttackType());
NetworkManager.getInstance().sendDamage(e.getKey(), dmg);
player.markDamageApplied();
}
}
}
// 远程玩家攻击命中本地玩家
for (Map.Entry<String, SimpleFighter> e : otherPlayers.entrySet()) {
SimpleFighter attacker = e.getValue();
if (attacker.isAttacking() && attacker.canDealDamage() && player.isAlive()
&& attacker.getAttackbox().overlaps(player.getHitbox())) {
int dmg = attacker.getDamageForAttack(attacker.getLastAttackType());
NetworkManager.getInstance().sendDamage(NetworkManager.getInstance().getLocalPlayerId(), dmg);
attacker.markDamageApplied();
}
}
// 主机处理死亡计时与重生广播
// 本地玩家死亡
if (!player.isAlive()) {
deathTimers.merge(NetworkManager.getInstance().getLocalPlayerId(), Gdx.graphics.getDeltaTime(),
Float::sum);
}
// 远程玩家死亡
for (Map.Entry<String, SimpleFighter> e : otherPlayers.entrySet()) {
if (!e.getValue().isAlive()) {
deathTimers.merge(e.getKey(), Gdx.graphics.getDeltaTime(), Float::sum);
}
}
// 检查需要重生的对象
for (Map.Entry<String, Float> dt : new java.util.ArrayList<>(deathTimers.entrySet())) {
if (dt.getValue() >= RESPAWN_DELAY) {
String pid = dt.getKey();
// 重生位置简单放原点附近随机
float rx = (float) (Math.random() * 200 - 100);
float ry = 0;
NetworkManager.getInstance().sendRespawn(pid, rx, ry);
deathTimers.remove(pid);
}
}
}
// 应用收到的伤害事件
Map<String, Integer> damageEvents = NetworkManager.getInstance().getDamageEvents();
if (!damageEvents.isEmpty()) {
for (Map.Entry<String, Integer> dmg : damageEvents.entrySet()) {
String targetId = dmg.getKey();
int amount = dmg.getValue();
if (targetId.equals(NetworkManager.getInstance().getLocalPlayerId())) {
player.takeHit(amount);
} else {
SimpleFighter remote = otherPlayers.get(targetId);
if (remote != null) {
remote.takeHit(amount);
}
}
}
damageEvents.clear();
}
// 处理重生事件
Map<String, float[]> respawns = NetworkManager.getInstance().getRespawnEvents();
if (!respawns.isEmpty()) {
for (Map.Entry<String, float[]> r : respawns.entrySet()) {
String pid = r.getKey();
float[] p = r.getValue();
if (pid.equals(NetworkManager.getInstance().getLocalPlayerId())) {
player.setPosition(p[0], p[1]);
// 简单复活:重置血量字段(需访问)
// 由于 health 私有且无 setter可在 SimpleFighter 新增 resetForRespawn 方法(后续可优化)
// 临时通过反射或改造:这里暂时忽略直接血量恢复(若需要我可再补)
} else {
SimpleFighter remote = otherPlayers.get(pid);
if (remote != null) {
remote.setPosition(p[0], p[1]);
}
}
}
respawns.clear();
}
}
// 摄像机跟随

View File

@@ -16,6 +16,9 @@ public class NetworkManager {
private final Map<String, String> playerCharacters = new HashMap<>();
// 存储远程玩家的攻击类型attackType
private final Map<String, String> playerAttacks = new HashMap<>();
// 伤害事件targetId -> 累积伤害(本帧内可能多次)
private final Map<String, Integer> damageEvents = new HashMap<>();
private final Map<String, float[]> respawnEvents = new HashMap<>();
public static NetworkManager getInstance() {
if (instance == null) {
@@ -104,6 +107,39 @@ public class NetworkManager {
} else {
Gdx.app.error("Network", "攻击消息格式错误: " + message);
}
} else if (message.startsWith("DAMAGE:")) {
// DAMAGE:targetId,amount,(dir)
String[] parts = message.substring(7).split(",");
if (parts.length >= 2) {
String targetId = parts[0];
try {
int amount = Integer.parseInt(parts[1]);
damageEvents.merge(targetId, amount, Integer::sum);
// 第三个参数方向目前不存储,只为击退逻辑可扩展
Gdx.app.log("Network",
"收到伤害: " + targetId + " -" + amount + (parts.length >= 3 ? (" dir=" + parts[2]) : ""));
} catch (NumberFormatException e) {
Gdx.app.error("Network", "伤害数值解析失败: " + message);
}
} else {
Gdx.app.error("Network", "伤害消息格式错误: " + message);
}
} else if (message.startsWith("RESPAWN:")) {
// RESPAWN:playerId,x,y
String[] parts = message.substring(8).split(",");
if (parts.length == 3) {
try {
String pid = parts[0];
float x = Float.parseFloat(parts[1]);
float y = Float.parseFloat(parts[2]);
respawnEvents.put(pid, new float[] { x, y });
Gdx.app.log("Network", "收到重生: " + pid + " -> (" + x + "," + y + ")");
} catch (NumberFormatException e) {
Gdx.app.error("Network", "重生坐标解析失败: " + message);
}
} else {
Gdx.app.error("Network", "重生消息格式错误: " + message);
}
} else {
Gdx.app.log("Network", "未知消息类型: " + message);
}
@@ -140,6 +176,32 @@ public class NetworkManager {
return playerCharacters;
}
public Map<String, Integer> getDamageEvents() {
return damageEvents;
}
public void sendDamage(String targetId, int amount) {
String msg = "DAMAGE:" + targetId + "," + amount; // 不含方向
if (isHost && server != null) {
server.broadcastToOthers(null, msg);
receiveMessage(msg); // 本地也应用
} else if (client != null) {
client.sendMessage(msg);
}
}
public void sendRespawn(String playerId, float x, float y) {
String msg = "RESPAWN:" + playerId + "," + x + "," + y;
if (isHost && server != null) {
server.broadcastToOthers(null, msg);
receiveMessage(msg);
}
}
public Map<String, float[]> getRespawnEvents() {
return respawnEvents;
}
public boolean isHost() {
return isHost;
}