优化项目结构
This commit is contained in:
@@ -23,12 +23,27 @@ public class SimpleFighter extends FighterBase {
|
||||
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 = GameConstants.MOVE_SPEED; // 水平移动速度
|
||||
private int health = 200; // 生命值
|
||||
private boolean isAttacking = false; // 是否正在攻击
|
||||
private boolean attackJustStarted = false; // 攻击是否刚开始
|
||||
private float attackTimer = 0f; // 攻击计时器
|
||||
private static final float ATTACK_DURATION = 0.15f; // 攻击持续时间
|
||||
private float speed = GameConstants.MOVE_SPEED; // 目标最大水平移动速度
|
||||
private float velX = 0f; // 当前水平速度(加入加速度与减速)
|
||||
private static final int MAX_HEALTH = 200; // 最大生命值
|
||||
private int health = MAX_HEALTH; // 当前生命值
|
||||
private boolean isAttacking = false; // 是否正在攻击(包含 startup+active+recovery 整体)
|
||||
private boolean attackJustStarted = false; // 本帧是否刚开始(用于跳过首帧计时扣减)
|
||||
private float attackTimer = 0f; // 当前阶段剩余时间
|
||||
// 分阶段(帧数可按类型定制):startup -> active -> recovery
|
||||
|
||||
private enum AttackPhase {
|
||||
STARTUP, ACTIVE, RECOVERY
|
||||
}
|
||||
|
||||
private AttackPhase attackPhase = AttackPhase.STARTUP;
|
||||
// 基础时长(可后续移出到配置)
|
||||
// 降低攻击速度:整体各阶段放大(原值约 *1.6~2)
|
||||
private static final float LIGHT_STARTUP = 0.10f, LIGHT_ACTIVE = 0.10f, LIGHT_RECOVERY = 0.22f;
|
||||
private static final float HEAVY_STARTUP = 0.18f, HEAVY_ACTIVE = 0.16f, HEAVY_RECOVERY = 0.36f;
|
||||
private static final float SPECIAL_STARTUP = 0.20f, SPECIAL_ACTIVE = 0.22f, SPECIAL_RECOVERY = 0.42f;
|
||||
// 当前 attackType 的阶段时长(缓存便于递进)
|
||||
private float curStartup, curActive, curRecovery;
|
||||
// 新增:连续攻击序号(本地,用于避免重复伤害)
|
||||
private int attackSequence = 0;
|
||||
private String lastAttackType = "light"; // 记录最后一次攻击类型,供伤害判定
|
||||
@@ -39,6 +54,16 @@ public class SimpleFighter extends FighterBase {
|
||||
private float invulnerableTimer = 0f; // 无敌帧时间(被击中后短暂无敌)
|
||||
private static final float INVULNERABLE_DURATION = 0.3f;
|
||||
private static final float KNOCKBACK_DURATION = 0.12f;
|
||||
// 死亡淡出
|
||||
private float deathFadeTimer = 0f; // 剩余淡出时间(>0 表示正在淡出)
|
||||
private static final float DEATH_FADE_DURATION = 1.2f; // 完全消失所需时间
|
||||
// 防御新增字段
|
||||
private boolean defending = false; // 是否防御中
|
||||
private static final float DEFEND_DAMAGE_FACTOR = 0.25f; // 防御减伤比例
|
||||
private static final float DEFEND_KNOCKBACK_FACTOR = 0.3f; // 防御击退比例
|
||||
// 新增:攻击全局冷却(收招结束到允许下一次攻击的最短间隔)
|
||||
private static final float GLOBAL_ATTACK_COOLDOWN = 0.12f;
|
||||
private float globalAttackCDTimer = 0f;
|
||||
|
||||
public SimpleFighter(String name) {
|
||||
super(name);
|
||||
@@ -61,18 +86,39 @@ public class SimpleFighter extends FighterBase {
|
||||
}
|
||||
if (isAttacking) {
|
||||
if (attackJustStarted) {
|
||||
attackJustStarted = false;
|
||||
attackJustStarted = false; // 第一帧不扣时间,避免可见阶段提前缩短
|
||||
} else {
|
||||
attackTimer -= deltaTime;
|
||||
}
|
||||
if (attackTimer <= 0f) {
|
||||
isAttacking = false;
|
||||
attackTimer = 0f;
|
||||
if (currentAction == Action.ATTACK)
|
||||
changeAction(Action.IDLE);
|
||||
// 进入下一阶段
|
||||
switch (attackPhase) {
|
||||
case STARTUP:
|
||||
attackPhase = AttackPhase.ACTIVE;
|
||||
attackTimer = curActive;
|
||||
break;
|
||||
case ACTIVE:
|
||||
attackPhase = AttackPhase.RECOVERY;
|
||||
attackTimer = curRecovery;
|
||||
break;
|
||||
case RECOVERY:
|
||||
isAttacking = false;
|
||||
attackTimer = 0f;
|
||||
attackPhase = AttackPhase.STARTUP;
|
||||
globalAttackCDTimer = GLOBAL_ATTACK_COOLDOWN; // 开始冷却
|
||||
if (currentAction == Action.ATTACK)
|
||||
changeAction(Action.IDLE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateAttackbox("light");
|
||||
updateAttackbox("light"); // 非攻击中保持一个默认攻击盒(或可隐藏)
|
||||
}
|
||||
|
||||
// 冷却计时
|
||||
if (globalAttackCDTimer > 0f) {
|
||||
globalAttackCDTimer -= deltaTime;
|
||||
if (globalAttackCDTimer < 0f) globalAttackCDTimer = 0f;
|
||||
}
|
||||
|
||||
if (!isGrounded) {
|
||||
@@ -85,6 +131,12 @@ public class SimpleFighter extends FighterBase {
|
||||
changeAction(Action.IDLE);
|
||||
}
|
||||
}
|
||||
// 死亡淡出计时递减
|
||||
if (!isAlive() && deathFadeTimer > 0f) {
|
||||
deathFadeTimer -= deltaTime;
|
||||
if (deathFadeTimer < 0f)
|
||||
deathFadeTimer = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void renderSprite(SpriteBatch batch) {
|
||||
@@ -113,7 +165,7 @@ public class SimpleFighter extends FighterBase {
|
||||
if (keycode == Input.Keys.SPACE || keycode == Input.Keys.UP || keycode == Input.Keys.W) {
|
||||
jump();
|
||||
}
|
||||
if (!isAttacking) {
|
||||
if (!isAttacking && !defending && globalAttackCDTimer <= 0f) {
|
||||
if (keycode == Input.Keys.Z || keycode == Input.Keys.J) {
|
||||
attack("light");
|
||||
NetworkManager.getInstance().sendAttack("light", getFacingDir());
|
||||
@@ -125,11 +177,23 @@ public class SimpleFighter extends FighterBase {
|
||||
NetworkManager.getInstance().sendAttack("special", getFacingDir());
|
||||
}
|
||||
}
|
||||
// 防御键(C / 左CTRL)
|
||||
if (keycode == Input.Keys.C || keycode == Input.Keys.CONTROL_LEFT) {
|
||||
if (!isAttacking && isAlive()) {
|
||||
defending = true;
|
||||
changeAction(Action.DEFEND);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ((keycode == Input.Keys.LEFT || keycode == Input.Keys.RIGHT || keycode == Input.Keys.A
|
||||
|| keycode == Input.Keys.D) && getCurrentAction() == Action.MOVE) {
|
||||
changeAction(Action.IDLE);
|
||||
}
|
||||
if (keycode == Input.Keys.C || keycode == Input.Keys.CONTROL_LEFT) {
|
||||
defending = false;
|
||||
if (currentAction == Action.DEFEND)
|
||||
changeAction(Action.IDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,11 +218,37 @@ public class SimpleFighter extends FighterBase {
|
||||
}
|
||||
|
||||
public void move(float x, float deltaTime) {
|
||||
if (x != 0) {
|
||||
isFacingRight = x > 0;
|
||||
hitbox.x += x * speed * deltaTime;
|
||||
// x = -1,0,1 方向输入
|
||||
float targetSign = x;
|
||||
float accel = GameConstants.MOVE_ACCEL;
|
||||
if (!isGrounded) {
|
||||
accel *= GameConstants.AIR_ACCEL_FACTOR; // 空中降低加速度便于区分地面/空中控制
|
||||
}
|
||||
if (targetSign != 0) {
|
||||
isFacingRight = targetSign > 0;
|
||||
// 向目标速度加速
|
||||
float targetVel = targetSign * speed;
|
||||
if (velX < targetVel) {
|
||||
velX = Math.min(targetVel, velX + accel * deltaTime);
|
||||
} else if (velX > targetVel) {
|
||||
velX = Math.max(targetVel, velX - accel * deltaTime);
|
||||
}
|
||||
} else {
|
||||
// 无输入:减速(朝 0 逼近)
|
||||
float decel = GameConstants.MOVE_DECEL * deltaTime;
|
||||
if (velX > 0) {
|
||||
velX = Math.max(0, velX - decel);
|
||||
} else if (velX < 0) {
|
||||
velX = Math.min(0, velX + decel);
|
||||
}
|
||||
}
|
||||
// 应用位移
|
||||
hitbox.x += velX * deltaTime;
|
||||
// 状态切换
|
||||
if (Math.abs(velX) > 5f && isGrounded && !isAttacking) {
|
||||
changeAction(Action.MOVE);
|
||||
} else if (isGrounded && !isAttacking) {
|
||||
} else if (isGrounded && !isAttacking && targetSign == 0 && Math.abs(velX) <= 5f) {
|
||||
velX = 0f; // 近乎停止直接归零
|
||||
changeAction(Action.IDLE);
|
||||
}
|
||||
}
|
||||
@@ -215,13 +305,32 @@ public class SimpleFighter extends FighterBase {
|
||||
|
||||
public void attack(String attackType) {
|
||||
isAttacking = true;
|
||||
attackTimer = ATTACK_DURATION;
|
||||
attackPhase = AttackPhase.STARTUP;
|
||||
switch (attackType) {
|
||||
case "heavy":
|
||||
curStartup = HEAVY_STARTUP;
|
||||
curActive = HEAVY_ACTIVE;
|
||||
curRecovery = HEAVY_RECOVERY;
|
||||
break;
|
||||
case "special":
|
||||
curStartup = SPECIAL_STARTUP;
|
||||
curActive = SPECIAL_ACTIVE;
|
||||
curRecovery = SPECIAL_RECOVERY;
|
||||
break;
|
||||
case "light":
|
||||
default:
|
||||
curStartup = LIGHT_STARTUP;
|
||||
curActive = LIGHT_ACTIVE;
|
||||
curRecovery = LIGHT_RECOVERY;
|
||||
break;
|
||||
}
|
||||
attackTimer = curStartup;
|
||||
attackJustStarted = true;
|
||||
changeAction(Action.ATTACK);
|
||||
updateAttackbox(attackType);
|
||||
lastAttackType = attackType;
|
||||
attackSequence++; // 本地每次攻击自增
|
||||
lastDamageAppliedSeq = -1; // 新一次攻击重置
|
||||
attackSequence++; // 本地每次攻击序号自增
|
||||
lastDamageAppliedSeq = -1; // 重置伤害标记
|
||||
}
|
||||
|
||||
public void takeHit(int damage) {
|
||||
@@ -230,17 +339,28 @@ public class SimpleFighter extends FighterBase {
|
||||
|
||||
public void takeHit(int damage, int dirSign) {
|
||||
if (invulnerableTimer > 0 || health <= 0)
|
||||
return; // 无敌或已死亡
|
||||
health = Math.max(0, health - damage);
|
||||
changeAction(health > 0 ? Action.HIT : Action.DEAD);
|
||||
return;
|
||||
int finalDamage = damage;
|
||||
boolean wasDefending = defending && currentAction == Action.DEFEND;
|
||||
if (wasDefending) {
|
||||
finalDamage = Math.max(1, Math.round(damage * DEFEND_DAMAGE_FACTOR));
|
||||
}
|
||||
health = Math.max(0, health - finalDamage);
|
||||
if (!(wasDefending && health > 0)) {
|
||||
changeAction(health > 0 ? Action.HIT : Action.DEAD);
|
||||
}
|
||||
invulnerableTimer = INVULNERABLE_DURATION;
|
||||
// dirSign: -1 表示从右向左击中(目标向左被推), 1 表示从左向右击中(目标向右被推)
|
||||
if (dirSign == 0) { // 没有提供方向则沿用基于自身面向的旧逻辑
|
||||
knockbackX = isFacingRight ? -600f : 600f;
|
||||
float baseKb = 600f;
|
||||
if (wasDefending)
|
||||
baseKb *= DEFEND_KNOCKBACK_FACTOR;
|
||||
if (dirSign == 0) {
|
||||
knockbackX = isFacingRight ? -baseKb : baseKb;
|
||||
} else {
|
||||
knockbackX = dirSign * 600f;
|
||||
knockbackX = dirSign * baseKb;
|
||||
}
|
||||
knockbackTimer = KNOCKBACK_DURATION;
|
||||
if (health == 0)
|
||||
deathFadeTimer = DEATH_FADE_DURATION;
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
@@ -267,6 +387,10 @@ public class SimpleFighter extends FighterBase {
|
||||
return health;
|
||||
}
|
||||
|
||||
public int getMaxHealth() {
|
||||
return MAX_HEALTH;
|
||||
}
|
||||
|
||||
public int getAttackSequence() {
|
||||
return attackSequence;
|
||||
}
|
||||
@@ -284,7 +408,8 @@ public class SimpleFighter extends FighterBase {
|
||||
}
|
||||
|
||||
public boolean canDealDamage() {
|
||||
return isAttacking && attackSequence != lastDamageAppliedSeq; // 未对当前序号造成过伤害
|
||||
// 只有 ACTIVE 阶段且本序号未造成过伤害才允许
|
||||
return isAttacking && attackPhase == AttackPhase.ACTIVE && attackSequence != lastDamageAppliedSeq;
|
||||
}
|
||||
|
||||
public void markDamageApplied() {
|
||||
@@ -323,9 +448,21 @@ public class SimpleFighter extends FighterBase {
|
||||
return attackTimer;
|
||||
}
|
||||
|
||||
public boolean isInActivePhase() {
|
||||
return isAttacking && attackPhase == AttackPhase.ACTIVE;
|
||||
}
|
||||
|
||||
public boolean isInStartupPhase() {
|
||||
return isAttacking && attackPhase == AttackPhase.STARTUP;
|
||||
}
|
||||
|
||||
public boolean isInRecoveryPhase() {
|
||||
return isAttacking && attackPhase == AttackPhase.RECOVERY;
|
||||
}
|
||||
|
||||
// 重生时重置状态
|
||||
public void resetForRespawn() {
|
||||
health = 100;
|
||||
health = MAX_HEALTH;
|
||||
isAttacking = false;
|
||||
attackTimer = 0f;
|
||||
attackJustStarted = false;
|
||||
@@ -333,5 +470,20 @@ public class SimpleFighter extends FighterBase {
|
||||
invulnerableTimer = 0f;
|
||||
knockbackTimer = 0f;
|
||||
knockbackX = 0f;
|
||||
deathFadeTimer = 0f; // 重置淡出
|
||||
defending = false;
|
||||
velX = 0f;
|
||||
globalAttackCDTimer = 0f;
|
||||
}
|
||||
|
||||
/** 获取当前用于渲染的死亡淡出透明度(1=不透明,0=已完全淡出)。 */
|
||||
public float getRenderAlpha() {
|
||||
if (health > 0)
|
||||
return 1f;
|
||||
return deathFadeTimer / DEATH_FADE_DURATION; // 线性
|
||||
}
|
||||
|
||||
public boolean isDefending() {
|
||||
return defending && currentAction == Action.DEFEND;
|
||||
}
|
||||
}
|
||||
|
||||
158
src/main/java/uno/mloluyu/characters/ai/SimpleFighterAI.java
Normal file
158
src/main/java/uno/mloluyu/characters/ai/SimpleFighterAI.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package uno.mloluyu.characters.ai;
|
||||
|
||||
import com.badlogic.gdx.ai.fsm.DefaultStateMachine;
|
||||
import com.badlogic.gdx.ai.fsm.State;
|
||||
import com.badlogic.gdx.ai.fsm.StateMachine;
|
||||
import com.badlogic.gdx.ai.msg.Telegram;
|
||||
import com.badlogic.gdx.math.MathUtils;
|
||||
import uno.mloluyu.characters.SimpleFighter;
|
||||
|
||||
/**
|
||||
* 一个非常简化的 AI:
|
||||
* - 距离目标远: 向目标靠近
|
||||
* - 距离合适: 停下并有概率攻击
|
||||
* - 偶尔随机跳一下
|
||||
*/
|
||||
public class SimpleFighterAI {
|
||||
private final SimpleFighter self;
|
||||
private SimpleFighter target; // 追踪的目标(本地玩家)
|
||||
private final StateMachine<SimpleFighterAI, AIState> fsm;
|
||||
|
||||
// 参数(后续可提取到 GameConstants 或配置文件)
|
||||
private float attackCooldown = 0f;
|
||||
private float jumpCooldown = 0f;
|
||||
private static final float ATTACK_COOLDOWN_MIN = 0.6f;
|
||||
private static final float ATTACK_COOLDOWN_MAX = 1.2f;
|
||||
private static final float JUMP_COOLDOWN_MIN = 2.5f;
|
||||
private static final float JUMP_COOLDOWN_MAX = 4.5f;
|
||||
private static final float DESIRED_DISTANCE = 140f; // 停下的理想距离
|
||||
private static final float ATTACK_RANGE = 130f; // 触发攻击的距离
|
||||
|
||||
public SimpleFighterAI(SimpleFighter self, SimpleFighter target) {
|
||||
this.self = self;
|
||||
this.target = target;
|
||||
this.fsm = new DefaultStateMachine<>(this, AIState.IDLE);
|
||||
resetAttackCd();
|
||||
resetJumpCd();
|
||||
}
|
||||
|
||||
private void resetAttackCd() {
|
||||
attackCooldown = MathUtils.random(ATTACK_COOLDOWN_MIN, ATTACK_COOLDOWN_MAX);
|
||||
}
|
||||
|
||||
private void resetJumpCd() {
|
||||
jumpCooldown = MathUtils.random(JUMP_COOLDOWN_MIN, JUMP_COOLDOWN_MAX);
|
||||
}
|
||||
|
||||
public void update(float delta) {
|
||||
attackCooldown -= delta;
|
||||
jumpCooldown -= delta;
|
||||
fsm.update();
|
||||
}
|
||||
|
||||
public void setTarget(SimpleFighter target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
private float horizontalDistance() {
|
||||
if (target == null)
|
||||
return Float.MAX_VALUE;
|
||||
return target.getHitbox().x - self.getHitbox().x;
|
||||
}
|
||||
|
||||
private float absDistance() {
|
||||
return Math.abs(horizontalDistance());
|
||||
}
|
||||
|
||||
enum AIState implements State<SimpleFighterAI> {
|
||||
IDLE {
|
||||
@Override
|
||||
public void enter(SimpleFighterAI ai) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(SimpleFighterAI ai) {
|
||||
if (ai.target == null || !ai.target.isAlive())
|
||||
return;
|
||||
float dist = ai.absDistance();
|
||||
if (dist > DESIRED_DISTANCE * 1.3f) {
|
||||
ai.fsm.changeState(MOVE);
|
||||
return;
|
||||
}
|
||||
// 在理想距离范围内尝试攻击
|
||||
ai.tryAttack();
|
||||
ai.tryJump();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit(SimpleFighterAI ai) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMessage(SimpleFighterAI entity, Telegram telegram) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
MOVE {
|
||||
@Override
|
||||
public void enter(SimpleFighterAI ai) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(SimpleFighterAI ai) {
|
||||
if (ai.target == null || !ai.target.isAlive()) {
|
||||
ai.fsm.changeState(IDLE);
|
||||
return;
|
||||
}
|
||||
float distSigned = ai.horizontalDistance();
|
||||
float distAbs = Math.abs(distSigned);
|
||||
if (distAbs < DESIRED_DISTANCE) {
|
||||
ai.fsm.changeState(IDLE);
|
||||
return;
|
||||
}
|
||||
float dir = Math.signum(distSigned);
|
||||
ai.self.move(dir, com.badlogic.gdx.Gdx.graphics.getDeltaTime());
|
||||
// 攻击机会(接近过程中如果已经够近)
|
||||
if (distAbs < ATTACK_RANGE * 0.9f) {
|
||||
ai.tryAttack();
|
||||
}
|
||||
ai.tryJump();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit(SimpleFighterAI ai) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMessage(SimpleFighterAI entity, Telegram telegram) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void tryAttack() {
|
||||
if (attackCooldown <= 0f && self.isAlive() && !self.isAttacking()) {
|
||||
float dist = absDistance();
|
||||
if (dist < ATTACK_RANGE) {
|
||||
// 轻重攻击随机
|
||||
String atkType;
|
||||
float r = MathUtils.random();
|
||||
if (r < 0.7f)
|
||||
atkType = "light";
|
||||
else if (r < 0.9f)
|
||||
atkType = "heavy";
|
||||
else
|
||||
atkType = "special";
|
||||
self.attack(atkType);
|
||||
resetAttackCd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tryJump() {
|
||||
if (jumpCooldown <= 0f && self.isAlive() && MathUtils.random() < 0.15f) {
|
||||
self.jump();
|
||||
resetJumpCd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package uno.mloluyu.characters.effects;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
import com.badlogic.gdx.math.Rectangle;
|
||||
|
||||
/** 简单攻击特效:在攻击盒区域闪现并淡出。 */
|
||||
public class AttackEffect {
|
||||
private final Rectangle area = new Rectangle();
|
||||
private float life; // 剩余寿命
|
||||
private final float totalLife;
|
||||
private final Color color = new Color();
|
||||
|
||||
public AttackEffect(Rectangle src, float duration, Color base) {
|
||||
this.area.set(src);
|
||||
this.life = duration;
|
||||
this.totalLife = duration;
|
||||
this.color.set(base);
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return life > 0f;
|
||||
}
|
||||
|
||||
public void update(float delta) {
|
||||
life -= delta;
|
||||
}
|
||||
|
||||
public void render(ShapeRenderer sr) {
|
||||
if (!isAlive())
|
||||
return;
|
||||
float alpha = life / totalLife; // 线性淡出
|
||||
sr.setColor(color.r, color.g, color.b, alpha * color.a);
|
||||
sr.rect(area.x, area.y, area.width, area.height);
|
||||
}
|
||||
|
||||
public void renderOutline(ShapeRenderer sr) {
|
||||
if (!isAlive())
|
||||
return;
|
||||
float alpha = life / totalLife;
|
||||
sr.setColor(color.r, color.g, color.b, alpha * 0.6f);
|
||||
sr.rect(area.x, area.y, area.width, area.height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package uno.mloluyu.characters.effects;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
import com.badlogic.gdx.math.MathUtils;
|
||||
|
||||
/** 单个命中粒子:简单方块/短线喷射,带速度、重力与淡出。 */
|
||||
public class HitParticle {
|
||||
private float x, y;
|
||||
private float vx, vy;
|
||||
private float life; // 剩余寿命
|
||||
private final float totalLife;
|
||||
private final float size;
|
||||
private final Color color = new Color();
|
||||
private static final float GRAVITY = 900f; // 轻微下坠
|
||||
|
||||
public HitParticle(float x, float y, Color base, float speedMin, float speedMax, float lifeMin, float lifeMax,
|
||||
float sizeMin, float sizeMax) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
float ang = MathUtils.random(15f, 165f) * MathUtils.degreesToRadians; // 向上扇形
|
||||
float spd = MathUtils.random(speedMin, speedMax);
|
||||
this.vx = MathUtils.cos(ang) * spd;
|
||||
this.vy = MathUtils.sin(ang) * spd;
|
||||
this.life = MathUtils.random(lifeMin, lifeMax);
|
||||
this.totalLife = life;
|
||||
this.size = MathUtils.random(sizeMin, sizeMax);
|
||||
// 基础色随机稍许扰动
|
||||
float tint = MathUtils.random(0.85f, 1.05f);
|
||||
this.color.set(base.r * tint, base.g * tint, base.b * tint, 1f);
|
||||
}
|
||||
|
||||
public boolean isAlive() {
|
||||
return life > 0f;
|
||||
}
|
||||
|
||||
public void update(float dt) {
|
||||
if (!isAlive())
|
||||
return;
|
||||
life -= dt;
|
||||
// 运动积分
|
||||
x += vx * dt;
|
||||
y += vy * dt;
|
||||
vy -= GRAVITY * dt * 0.35f; // 轻微重力
|
||||
}
|
||||
|
||||
public void render(ShapeRenderer sr) {
|
||||
if (!isAlive())
|
||||
return;
|
||||
float a = (life / totalLife); // 线性淡出
|
||||
sr.setColor(color.r, color.g, color.b, a);
|
||||
sr.rect(x - size / 2f, y - size / 2f, size, size);
|
||||
}
|
||||
}
|
||||
48
src/main/java/uno/mloluyu/characters/effects/RadialRing.java
Normal file
48
src/main/java/uno/mloluyu/characters/effects/RadialRing.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package uno.mloluyu.characters.effects;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
|
||||
/**
|
||||
* 攻击进入 ACTIVE 瞬间生成的环形扩散效果:半径扩大+透明衰减。
|
||||
*/
|
||||
public class RadialRing {
|
||||
private float x, y;
|
||||
private float radius;
|
||||
private float maxRadius;
|
||||
private float life;
|
||||
private float maxLife;
|
||||
private Color color;
|
||||
|
||||
public RadialRing(float x, float y, float startRadius, float maxRadius, float life, Color color) {
|
||||
this.x = x; this.y = y; this.radius = startRadius; this.maxRadius = maxRadius; this.life = life; this.maxLife = life; this.color = new Color(color);
|
||||
}
|
||||
|
||||
public void update(float dt) {
|
||||
life -= dt;
|
||||
float t = 1f - life / maxLife; // 0 -> 1
|
||||
radius = (startRadius() + (maxRadius - startRadius()) * t);
|
||||
}
|
||||
|
||||
private float startRadius() { return 6f; }
|
||||
|
||||
public boolean isAlive() { return life > 0f; }
|
||||
|
||||
public void render(ShapeRenderer sr) {
|
||||
float alpha = life / maxLife;
|
||||
sr.setColor(color.r, color.g, color.b, alpha * color.a);
|
||||
// 使用多段线模拟圆环(填充模式下画薄圆)
|
||||
int segments = 26;
|
||||
float lineWidth = 2f;
|
||||
float step = (float)(Math.PI * 2 / segments);
|
||||
for (int i = 0; i < segments; i++) {
|
||||
float a1 = i * step;
|
||||
float a2 = (i + 1) * step;
|
||||
float x1 = x + (float)Math.cos(a1) * radius;
|
||||
float y1 = y + (float)Math.sin(a1) * radius;
|
||||
float x2 = x + (float)Math.cos(a2) * radius;
|
||||
float y2 = y + (float)Math.sin(a2) * radius;
|
||||
sr.rectLine(x1, y1, x2, y2, lineWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package uno.mloluyu.characters.effects;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
|
||||
|
||||
public class SparkParticle {
|
||||
private float x, y;
|
||||
private float vx, vy;
|
||||
private float life;
|
||||
private float maxLife;
|
||||
private float length;
|
||||
private Color color;
|
||||
|
||||
public SparkParticle(float x, float y, float speed, float angle, float life, float length, Color base) {
|
||||
this.x = x; this.y = y;
|
||||
this.vx = (float)Math.cos(angle) * speed;
|
||||
this.vy = (float)Math.sin(angle) * speed;
|
||||
this.life = life; this.maxLife = life; this.length = length; this.color = new Color(base);
|
||||
}
|
||||
|
||||
public void update(float dt) {
|
||||
life -= dt;
|
||||
x += vx * dt;
|
||||
y += vy * dt;
|
||||
vy -= 900f * dt; // 轻微重力
|
||||
}
|
||||
|
||||
public boolean isAlive() { return life > 0f; }
|
||||
|
||||
public void render(ShapeRenderer sr) {
|
||||
float t = life / maxLife;
|
||||
float a = t; // 线性淡出
|
||||
sr.setColor(color.r, color.g, color.b, a * color.a);
|
||||
float nx = x - vx * 0.015f; // 逆向一点形成拖尾
|
||||
float ny = y - vy * 0.015f;
|
||||
sr.rectLine(x, y, nx, ny, Math.max(1.2f, length * t));
|
||||
}
|
||||
}
|
||||
19
src/main/java/uno/mloluyu/characters/effects/Untitled-1.html
Normal file
19
src/main/java/uno/mloluyu/characters/effects/Untitled-1.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<form>
|
||||
<label for="username">用户名:</label>
|
||||
<input type="text" id="username" name="username"><br><br>
|
||||
<input type="checkbox" id="remember">
|
||||
<label for="remember">记住用户名</label><br><br>
|
||||
<input type="button" value="提交" onclick="submitForm()">
|
||||
</form>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user