Starsector Nihilism Guidance
首页
GitHub 项目
首页
GitHub 项目
  • 首页
  • 基础及舰船相关
    • MOD 骨架和入口文件
    • 制作一艘基本舰船
    • 制作一艘航母与战机
    • 制作一艘相位舰
    • 制作火炮、光束与导弹
    • 为舰船加入内置武器、船插与战机
    • 补充描述文本
  • 无需入门的代码修改
    • 以船插为例初步熟悉代码
    • 制作完整船插
    • 制作最小战术系统
    • 为武器加入命中与光束特效
  • 势力与生涯星系
    • 注册一个势力
    • 配置势力舰队
    • 制作 MOD 入口类的最基础适配
    • 制作一个生涯星系
    • 补完描述文本
  • 战役与无代码对象
    • 制作测试战役装配
    • 为模拟战与标题画面加入装配
    • 为舰船加入外观风格
    • 制作一艘模块船
    • 制作一种星体类型
    • 制作一个自定义实体
  • 音频资源与播放
    • 准备 OGG 音频资源
    • 注册音频资源
    • 为游戏对象加入音频
  • Java 工程化
    • 确认 Java 与编译依赖
    • 迁移代码文件到 src
    • 配置 Gradle 工程
    • 构建 JAR 并声明产物
  • 全局服务
    • 制作 MOD 入口类
    • 使用 Global 访问游戏服务
    • 读取设置
    • 调用声音播放器
    • 记录和定位 MOD 日志
  • 战斗运行时
    • 读取战斗引擎状态
    • 读取战斗对象
    • 制作每帧战斗插件
    • 修改舰船数值
    • 配置武器脚本
    • 配置战斗监听器
    • 制作分层渲染插件
  • 战斗内容扩展
    • 详解舰船插件
    • 详解战术系统
    • 详解武器动画
  • 生涯世界与基础服务
    • 读取生涯世界状态
    • 构建生涯地点
    • 配置生涯势力
    • 放置生涯实体
  • 生涯舰队与舰队成员
    • 生成生涯舰队
    • 管理舰队中的成员
    • 舰队状态的重算过程
    • 维护舰队成员状态
    • 读取并修改舰船装配
  • 市场与商品
    • 创建生涯市场
    • 读写货舱内容
    • 注册市场商品
    • 实现专用子市场
    • 制作专用产业
    • 注册特殊物品
    • 实现市场条件
  • 人物与技能
    • 配置人物基础
    • 让人物承担角色
    • 制作角色技能
  • 生涯脚本与情报事件
    • 编写生涯逐帧脚本
    • 监听生涯事件
    • 选择生涯插件
    • 管理舰队活动情报
  • 酒吧事件与任务
    • 组织生涯交互对话
    • 实现对话插件
    • 接入酒吧事件
    • 构建阶段任务
    • 接入酒吧任务
  • Rules 系统、命令与内存
    • 组织 Rules 规则表
    • 编排 Rules 对话
    • 接入 Rules 生涯场景
    • 编写 Rules 命令
    • 使用 MemoryAPI 保存状态
  • 文件读取与写入
    • 读取 MOD 内文本
    • 读取 MOD 内 JSON 文件
    • 读取 MOD 内 CSV
    • 读写 common 数据
    • 实现本地化机制
  • 常用依赖库
    • 接入常用依赖库
    • 使用 LazyLib 几何工具
    • 使用 LazyLib 查询战场
    • 使用 LazyLib 保存公共 JSON
    • 注册 GraphicsLib 光照数据
    • 创建 GraphicsLib 动态光源
    • 制作 GraphicsLib 纹理映射
    • 配置 MagicLib 自动尾迹
    • 创建 MagicLib 运行时尾迹
    • 配置 MagicLib 数据赏金
    • 使用 MagicLib 辅助生涯开发
    • 使用 MagicLib 战斗小工具
  • 可选依赖与 Nexerelin
    • 接入可选依赖
    • 登记 Nexerelin 势力与外交配置
    • 配置 Nexerelin 的两种星区开局
    • 配置 Nexerelin 战略 AI 与扩张规模
    • 登记 Nexerelin 地面战单位与产业
    • 为 Nexerelin 特遣队添加命名与事件响应
    • 制作即时生效的 LunaLib 设置面板
  • 着色器与渲染
    • 使用 Java OpenGL
    • 使用 GLSL 进阶
    • 使用 VBO 和 VAO 保存顶点
    • 使用 SSBO 传递参数
    • 使用实例化进行绘制
    • 选择 BoxUtil 渲染方案

使用 MagicLib 战斗小工具

本节为锁定目标绘制近侧圆周标记,并在玩家舰船旁显示接近度。距离转换为线性比例后,再由 MagicAnim 映射为平滑曲线,两种结果交给同一个 HUD 状态条。几何位置与显示进度都来自当前玩家和目标,刷新频率则分别安排。

本节预期阅读时间:~20 分钟。

本节预期操作时间:~25 分钟。

学习目标

  • 使用 MagicFakeBeam.getCollisionPointOnCircumference(...) 计算线段与圆周的近侧交点;
  • 将战斗距离归一化为 0–1 接近度;
  • 使用 MagicAnim.smooth(...) 把线性进度映射为缓入缓出曲线;
  • 使用 MagicUI.drawHUDStatusBar(...) 比较平滑主条与线性副条;
  • 为每帧战斗插件分别安排 HUD 刷新与粒子节流。

前置知识

  • [C8-S3] 的战斗插件生命周期与暂停处理;
  • [C18-S1] 的 MagicLib 硬依赖和编译 classpath;
  • [C18-S2] 的位置、距离和 Vector2f;
  • [C18-S3] 的 BaseEveryFrameCombatPlugin 注册方式。

注册战斗目标探针

先保存本节完整的 GearInTargetProbe,再向既有 data/config/settings.json 的 plugins 对象追加唯一登记键:

{
    "plugins": {
        "gearInTargetProbe": "data.scripts.gearin.combat.GearInTargetProbe"
    }
}

游戏为每场战斗创建一个插件实例。实例字段只保存当前战斗的引擎、上一个目标与粒子计时器,不跨战斗或存档持久化。

让目标与 HUD 使用同一次查询

登记完成后,探针取得玩家与当前锁定的舰船。中心距离用于计算接近度,玩家至目标中心的线段用于取得圆周交点;同一帧使用这组对象,显示就能对应当前目标。HUD 每帧提交,粒子按间隔补充。

玩家舰船 + 当前舰船目标
    -> 中心距离 -> 线性接近度 -> MagicAnim.smooth -> HUD 主条
    -> 玩家位置到目标位置 -> MagicFakeBeam -> 圆周交点粒子

advance(...) 在战斗暂停、玩家不存在、玩家已被摧毁、目标不存在或目标已被摧毁时结束本帧。失去目标时还会清零粒子计时器,重新锁定后从零累计粒子计时,当前帧的 amount 也参与累积。

计算目标圆周交点

getCollisionPointOnCircumference(...) 接收线段起点、线段终点、圆心和半径。本例把玩家位置作为起点,把目标位置同时作为线段终点和圆心,再使用目标的碰撞半径:

Vector2f collisionPoint = MagicFakeBeam.getCollisionPointOnCircumference(
        player.getLocation(),
        target.getLocation(),
        target.getLocation(),
        target.getCollisionRadius());

正常交战距离下,返回值是玩家一侧的目标圆周交点。线段没有到达圆周时返回 null;调用方只有在结果存在时才生成粒子。线段起点已经位于圆内时,方法直接返回起点,因此贴近或穿入碰撞圆的玩家会在自身位置看到标记。

这个方法按圆心和半径处理几何边界。LazyLib CollisionUtils 面向舰船边界、线段和实际碰撞轮廓等查询;选择依据是当前问题需要抽象圆周,还是需要实体的具体碰撞形状。

把距离映射为接近度

玩家与目标的中心距离使用 MathUtils.getDistance(...) 取得。本例把 2000 su 设为显示量程,距离越近,接近度越高:

float distance = MathUtils.getDistance(player.getLocation(), target.getLocation());
float linearProximity = 1f - Math.min(distance / 2000f, 1f);

映射结果具有明确边界:距离为 0 su 时等于 1,距离为 1000 su 时等于 0.5,距离达到或超过 2000 su 时等于 0。两个位置的距离为非负数,Math.min(...) 将量程之外的比例截到 1。这里传入位置向量,HUD 显示的是中心距离;换成实体重载后,碰撞半径也会进入结果。

接近度用于表现玩家与目标的空间关系,不参与伤害、命中或 AI 判断。改变 DISPLAY_RANGE 只会改变 HUD 的显示比例。

使用 smooth 映射动画曲线

MagicAnim.smooth(float) 接收 0–1 进度,并返回同一范围内的缓入缓出结果:

float smoothProximity = MagicAnim.smooth(linearProximity);

输入 0、0.5 和 1 时,输出分别保持 0、0.5 和 1;输入 0.25 时约为 0.15,输入 0.75 时约为 0.85。曲线在两端变化较慢,在中段变化较快。

smooth 计算的是当前输入在曲线上的位置。目标瞬间跳到另一处,进度也会随当前距离立即变化;若需要状态在几帧内追赶目标值,还要由插件保存进度并按时间推进。这里采用即时映射,以便把显示比例与距离直接对应。

绘制双层 HUD 状态条

本例把平滑接近度交给主填充 fill,把线性接近度交给较宽的副填充 secondfill。两层使用相同绿色,距离文本提供当前位置的数值:

MagicUI.drawHUDStatusBar(
        player,
        smoothProximity,
        MagicUI.GREENCOLOR,
        MagicUI.GREENCOLOR,
        linearProximity,
        Math.round(distance) + " su",
        "目标接近度",
        false);

副填充的显示长度会被限制在主填充以内。 接近度大于 0.5 时,平滑值高于线性值,两层可以显示不同长度;接近度小于 0.5 时,线性副条被截到主条长度。因此,约 500 su 处适合比较两层,约 1500 su 处则能观察这项限制。

HUD 参数速查表

参数本例值作用
shipplayer指定状态条所属舰船
fillsmoothProximity绘制平滑主填充
innerColorMagicUI.GREENCOLOR指定主条颜色
borderColorMagicUI.GREENCOLOR指定边框颜色
secondfilllinearProximity绘制线性副填充
bottext整数距离加 su显示当前中心距离
toptext目标接近度标识状态条含义
offsetfalse使用默认 HUD 位置

drawHUDStatusBar(...) 内部只为当前玩家舰船绘制,而且要求战斗引擎存在 viewport、HUD 当前可见。传入其他舰船、隐藏 HUD 或离开正常战斗画面时,方法会自行结束。

节流圆周标记粒子

HUD 每个有效帧都要提交,才能跟随界面刷新;粒子只需定期补充。本例累计 amount,每 0.05 秒 生成一个持续 0.08 秒 的平滑粒子:

particleElapsed += amount;
if (particleElapsed < 0.05f) {
    return;
}
particleElapsed %= 0.05f;

粒子寿命比间隔长 0.03 秒,相邻标记会轻微重叠。零速度让粒子停留在本次计算的交点;目标移动后,新粒子会出现在新的圆周位置,旧粒子在 0.08 秒 内自然消失。

切换目标时,插件把计时器清零。新目标从零累计到阈值再生成粒子,旧目标的计时余量不带入。此前粒子继续按 0.08 秒寿命消失。

常用方法速查表

API返回值或结果本节用途
ShipAPI.getShipTarget()ShipAPI 或 null取得玩家当前锁定目标
MathUtils.getDistance(...)float取得玩家与目标的中心距离
MagicFakeBeam.getCollisionPointOnCircumference(...)Vector2f 或 null取得线段遇到的近侧圆周交点
MagicAnim.smooth(float)0–1 的 float把线性接近度映射为缓入缓出进度
MagicUI.drawHUDStatusBar(...)无返回值绘制主副填充、距离和标题
CombatEngineAPI.addSmoothParticle(...)无返回值在交点生成短寿命标记

完整最小例

GearInTargetProbe 读取当前玩家、舰船目标与中心距离,再生成 HUD 和标记粒子。源码保存到 src/data/scripts/gearin/combat/GearInTargetProbe.java:

完整成品:GearInTargetProbe
package data.scripts.gearin.combat;

import java.awt.Color;
import java.util.List;

import com.fs.starfarer.api.combat.BaseEveryFrameCombatPlugin;
import com.fs.starfarer.api.combat.CombatEngineAPI;
import com.fs.starfarer.api.combat.ShipAPI;
import com.fs.starfarer.api.input.InputEventAPI;
import org.lazywizard.lazylib.MathUtils;
import org.lwjgl.util.vector.Vector2f;
import org.magiclib.util.MagicAnim;
import org.magiclib.util.MagicFakeBeam;
import org.magiclib.util.MagicUI;

public final class GearInTargetProbe extends BaseEveryFrameCombatPlugin {

    private static final float DISPLAY_RANGE = 2000f;
    private static final float PARTICLE_INTERVAL = 0.05f;
    private static final float PARTICLE_DURATION = 0.08f;
    private static final float PARTICLE_SIZE = 18f;
    private static final Color PARTICLE_COLOR = new Color(90, 255, 140, 220);
    private static final Vector2f ZERO = new Vector2f();

    private CombatEngineAPI engine;
    private ShipAPI previousTarget;
    private float particleElapsed;

    @Override
    public void init(CombatEngineAPI engine) {
        this.engine = engine;
    }

    @Override
    public void advance(float amount, List<InputEventAPI> events) {
        if (engine.isPaused()) {
            return;
        }

        ShipAPI player = engine.getPlayerShip();
        if (player == null || !player.isAlive() || !engine.isEntityInPlay(player)) {
            resetTarget();
            return;
        }

        ShipAPI target = player.getShipTarget();
        if (target == null || !target.isAlive() || !engine.isEntityInPlay(target)) {
            resetTarget();
            return;
        }

        if (target != previousTarget) {
            previousTarget = target;
            particleElapsed = 0f;
        }

        float distance = MathUtils.getDistance(player.getLocation(), target.getLocation());
        float linearProximity = 1f - Math.min(distance / DISPLAY_RANGE, 1f);
        float smoothProximity = MagicAnim.smooth(linearProximity);

        MagicUI.drawHUDStatusBar(
                player,
                smoothProximity,
                MagicUI.GREENCOLOR,
                MagicUI.GREENCOLOR,
                linearProximity,
                Math.round(distance) + " su",
                "目标接近度",
                false);

        particleElapsed += amount;
        if (particleElapsed < PARTICLE_INTERVAL) {
            return;
        }
        particleElapsed %= PARTICLE_INTERVAL;

        Vector2f collisionPoint = MagicFakeBeam.getCollisionPointOnCircumference(
                player.getLocation(),
                target.getLocation(),
                target.getLocation(),
                target.getCollisionRadius());
        if (collisionPoint != null) {
            engine.addSmoothParticle(
                    collisionPoint, ZERO, PARTICLE_SIZE, 1f,
                    PARTICLE_DURATION, PARTICLE_COLOR);
        }
    }

    private void resetTarget() {
        previousTarget = null;
        particleElapsed = 0f;
    }
}

验证方案

前置数据:MagicLib 与 LazyLib 已安装并启用,GearInTargetProbe 已加入 settings 的 plugins 对象,测试战斗包含至少两艘可以锁定的敌舰。

  1. 保存源码与插件登记,构建并重启。进入战斗,保持未锁定目标,观察玩家 HUD 与战场粒子。
  2. 锁定一艘存活敌舰,观察目标近侧碰撞圆和玩家舰船左上方 HUD。
  3. 在 2000 su 内接近目标,比较绿色主填充与较宽副填充。
  4. 拉开到 2000 su 以外,继续观察距离文本和两层填充。
  5. 切换到第二艘敌舰,观察圆周标记的位置和首次粒子时机。
  6. 取消锁定或摧毁当前目标,观察 HUD 状态条与新粒子生成。

预期结果:

  • 无有效目标时不显示目标接近度状态条,也不生成交点粒子;
  • 锁定有效目标后,绿色粒子按 0.05 秒间隔、每帧最多一个标记玩家一侧圆周;常规帧率下约每秒 20 个;
  • 接近目标时,主填充与副填充都增加,距离文本以整数 su 更新;
  • 平滑主条在量程两端变化较慢,线性副条按距离等比例变化;
  • 距离达到或超过 2000 su 时,两层填充均为 0;
  • 切换目标后,交点跟随新目标,计时从零累计达到 0.05 秒后生成新粒子,当前帧时间计入其中。

故障分析

  • 锁定目标后 HUD 和粒子都没有出现:插件没有从 settings 注册,或玩家取得的不是舰船目标。

    • 解决方案:核对 plugins.gearInTargetProbe 的包名与类名,并确认目标来自 player.getShipTarget()。
  • HUD 出现但圆周没有粒子:交点结果为 null,或粒子节流尚未达到 0.05 秒。

    • 解决方案:核对四个交点参数的顺序,并确认线段终点使用目标中心。
  • 状态条随距离增加而填满:距离比例没有反转为接近度。

    • 解决方案:使用 1f - Math.min(distance / DISPLAY_RANGE, 1f)。
  • 两层长度相同,难以比较曲线:接近度处于端点、中点,或线性副条高于主条后被截到相同长度。

    • 解决方案:保留主条的 smoothProximity 与副条的 linearProximity,在约 500 su 处比较。
  • 传入其他舰船后状态条不显示:这个入口只为当前玩家舰船绘制。

    • 解决方案:始终把 engine.getPlayerShip() 传给 drawHUDStatusBar(...)。
  • 切换目标后粒子沿用上次采样的计时余量:目标变化时没有清零计时器。

    • 解决方案:比较 target 与 previousTarget,变化后把 particleElapsed 设为 0f。

总结

探针从当前目标取得圆周交点与中心距离,曲线映射改变显示进度,HUD 再按主副条的实际限制绘制。曲线只处理当前值,粒子计时则保存在插件字段中;目标切换时清除计时余量,既有粒子继续完成自己的寿命,两种刷新职责由此分开。

建议参考项

  • org.magiclib.util.MagicFakeBeam:假光束和圆形边界求交;
  • org.magiclib.util.MagicAnim:归一化进度的动画曲线;
  • org.magiclib.util.MagicUI:战斗 HUD 状态条、文字与颜色常量;
  • org.lazywizard.lazylib.MathUtils:两个位置向量之间的中心距离;
  • com.fs.starfarer.api.combat.BaseEveryFrameCombatPlugin:每帧战斗入口。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
上一页
使用 MagicLib 辅助生涯开发