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 运行时尾迹

运行时尾迹由 Java 决定节点何时出现、放在哪里,以及接入哪一段轨迹。本节为测试导弹保存主体与辉光的独立 ID,再以 SimpleEntity 产生沿初始方向前进的残影。弹体转向、跳变与命中时,采样、切段和结束各自对应到跟踪器中的状态。

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

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

学习目标

完成后,您将能够:

  • 为每枚发射物分配并跨帧复用稳定 trail ID;
  • 使用 addTrailMemberSimple(...) 配置一条基础尾迹;
  • 使用 addTrailMemberAdvanced(...) 配置渐变、滚动和尺寸脉冲;
  • 使用 SimpleEntity 为计算位置建立独立尾迹锚点;
  • 在位置跳变和实体结束时切断关联并停止跟踪。

前置知识

  • [C8-S2] 理解战斗实体有效性与对象结束边界;
  • [C8-S3] 理解战斗插件、暂停判断与每帧时间;
  • [C18-S8] 的 trail sprite、节点生命周期和自动尾迹字段;
  • [C18-S2] 的 GearIn_geometry_missile 弹体与德瓦林测试引用;
  • 能够在 .proj 中为弹体注册 OnFireEffectPlugin。

区分自动尾迹与运行时节点

[C18-S8] 的 MagicAutoTrails 先按 projectile ID 找到 CSV 数据,再替每条匹配记录持续提交节点。运行时尾迹直接调用 MagicTrailPlugin:Java 决定何时生成节点、节点放在哪里、同一段轨迹复用哪个 ID,以及何时结束关联。

MagicTrailPlugin 把具有相同 ID、sprite 和渲染层的节点连接为一条带状轨迹。单个节点没有相邻点可以连接,至少提交两个节点后才会出现连续尾迹。运行时代码因此需要同时管理采样频率和 ID 生命周期,少管任何一个,画面都会很诚实地暴露问题。

先保存本节完整类到 src/data/scripts/gearin/visuals/GearInMagicTrail.java。在 GearIn_geometry_missile.proj 中追加下面的局部字段,保留现有 ID、引擎和 AI 选择关系:

"onFireEffect": "data.scripts.gearin.visuals.GearInMagicTrail",

用稳定 ID 连接节点

OnFireEffectPlugin 的对象可能服务多次开火。**每枚弹体保存自己的尾迹状态。**把 trail ID 放在共用效果类字段中,会让后续弹体继续使用前一枚弹体的轨迹;在 onFire(...) 中为当前弹体创建独立跟踪器,状态才能与该弹体一起开始和结束:

public void onFire(DamagingProjectileAPI projectile, WeaponAPI weapon,
        CombatEngineAPI engine) {
    engine.addPlugin(new TrailTracker(projectile));
    Vector2f detachedVelocity = new Vector2f(projectile.getVelocity());
    detachedVelocity.scale(0.65f);
    spawnDetachedTrail(engine, projectile.getLocation(), detachedVelocity);
}

TrailTracker.init(...) 为主体和辉光各调用一次 getUniqueID()。这两个 float 在后续帧保持不变,同一层的新节点才会接在已有节点后方:

bodySprite = Global.getSettings().getSprite("fx", "base_trail_smooth");
glowSprite = Global.getSettings().getSprite("fx", "base_trail_contrail");
bodyTrailId = MagicTrailPlugin.getUniqueID();
glowTrailId = MagicTrailPlugin.getUniqueID();
lastLocation = new Vector2f(projectile.getLocation());
addSegments();

ID 只标识当前战斗中的一段尾迹。新的战斗会重新初始化 MagicTrail 插件;需要永久保存的是产生尾迹的规则,而不是这个运行时数值。

比较节点 ID 的使用方式

下面的局部故障片段在每次提交时生成新 ID。各节点分别进入新 tracker,无法与相邻节点连接:

MagicTrailPlugin.addTrailMemberSimple(
        projectile, MagicTrailPlugin.getUniqueID(), bodySprite, location,
        0f, facing, 14f, 3f, new Color(120, 220, 255),
        0.8f, 0.05f, 0.2f, 0.8f, true);

修正片段在初始化时保存 ID:

bodyTrailId = MagicTrailPlugin.getUniqueID();

后续提交反复使用这个字段,同一连续段的节点才能相互连接:

MagicTrailPlugin.addTrailMemberSimple(
        projectile, bodyTrailId, bodySprite, location,
        0f, facing, 14f, 3f, new Color(120, 220, 255),
        0.8f, 0.05f, 0.2f, 0.8f, true);

使用 Simple 提交主体节点

主体层直接使用 addTrailMemberSimple(...):

MagicTrailPlugin.addTrailMemberSimple(
        projectile, bodyTrailId, bodySprite, location,
        0f, facing, 14f, 3f, new Color(120, 220, 255),
        0.8f, 0.05f, 0.2f, 0.8f, true);

前三个参数确定关联实体、稳定 ID 和 sprite。projectile 让 cutTrailsOnEntity(projectile) 能找到这条尾迹;它不会让节点自动追随弹体。节点仍从第四个参数 location 取得本次世界坐标,因此每次采样都要重新读取当前位置。

speed = 0f 让节点生成后留在原地,facing 同时决定节点的运动方向和宽度方向。startSize = 14f、endSize = 3f 让单个节点随生命周期收窄;颜色的 alpha 分量不会进入尾迹计算,透明度由后面的 0.8f 单独控制。

最后四个时间与混合参数依次表示淡入 0.05 秒、完整显示 0.2 秒、淡出 0.8 秒,以及使用加色混合。节点的总寿命是三段时间之和。sprite 的宽和高都使用偶数尺寸,纹理才能稳定落在尾迹四边形上。

使用 Advanced 提交辉光节点

辉光层需要首尾颜色、纹理滚动和尺寸脉冲,因此改用参数完整的布尔混合重载:

MagicTrailPlugin.addTrailMemberAdvanced(
        projectile, glowTrailId, glowSprite, location,
        0f, 0f, facing, 0f, 0f, 22f, 6f,
        new Color(90, 170, 255), new Color(30, 70, 180),
        0.55f, 0.05f, 0.15f, 0.6f, true,
        192f, 600f, -1f, new Vector2f(), glowOptions,
        CombatEngineLayers.CONTRAILS_LAYER, 1f);

startSpeed 与 endSpeed 控制节点从生成到消失的移动速度,随后的 startAngularVelocity 与 endAngularVelocity 控制旋转速度。本例把四项都设为 0,让节点停在采样位置;22→6 的尺寸与亮蓝→深蓝的颜色负责可见变化。

textureLoopLength = 192f 表示纹理每 192 su 重复一次;textureScrollSpeed = 600f 表示每秒滚动 0.6 个完整纹理周期。textureOffset = -1f 为同一个 tracker 选择固定随机起点,后续节点共享这份相位,零向量 offsetVelocity 则不增加额外世界速度。

CONTRAILS_LAYER 明确渲染层,传入 null 也会采用这一层。frameOffsetMult = 1f 保留弹体节点的位置修正:Advanced 用发射舰速度与本帧时间计算位移,再从传入位置扣除它;Simple 没有这项修正。倍率传入 0 仍按 1 处理。自定义 OpenGL 混合重载则用源与目标混合常量替换 boolean additive,节点寿命的含义相同。

高级选项使用准确的包装类型:

private TrailTracker(DamagingProjectileAPI projectile) {
    this.projectile = projectile;
    glowOptions.put("SIZE_PULSE_WIDTH", 3f);
    glowOptions.put("SIZE_PULSE_COUNT", 2);
    glowOptions.put("FORWARD_PROPAGATION", false);
}

SIZE_PULSE_WIDTH 是 Float,让宽度每次最多增加 3 su;SIZE_PULSE_COUNT 是 Integer,让节点寿命内完成两次脉冲。FORWARD_PROPAGATION 是 Boolean,本例保持当前传播方式。键名正确而值类型错误时,对应选项不会进入 tracker。

addTrailMemberAnimated(...) 是兼容既有动态换图代码的弃用入口。当前静态 sprite 使用 addTrailMemberAdvanced(...),避免让整条尾迹随最后一次提交反复切换纹理。

纹理与渲染层也决定连续段

同一 ID 更换 sprite 或渲染层,会进入另一份 tracker,原节点继续留在原段。需要独立表现时分配新 ID,并保持该段的混合与传播方式稳定。

区分关联实体与节点位置

linkedEntity 进入 MagicTrail 的切断索引,position 进入当前节点。把导弹作为关联实体,只意味着可以按导弹一次切断主体和辉光;MagicTrail 不会从导弹自动推导节点坐标,也不会在导弹移动后修改已经生成的节点。

这种分离使一条尾迹可以关联舰船,却从引擎喷口坐标生成;也可以关联一个专用锚点,再让锚点沿完全由代码计算的路线移动。关联决定如何结束,位置决定节点画在哪里。

使用 SimpleEntity 建立独立锚点

SimpleEntity 把一个位置、武器或舰船引擎包装成 CombatEntityAPI,适合承担只有坐标和切断关系的视觉锚点。

SimpleEntity 构造方式速查表

构造参数位置来源适用效果
Vector2f调用方逐帧更新的坐标独立飞线、计算轨迹和脱离实体的残影
WeaponAPI当前武器位置炮口或炮塔关联尾迹
ShipEngineAPI当前引擎位置每个引擎独立保存 ID 的喷焰尾迹

完整成果公开 spawnDetachedTrail(...),复制位置和速度后创建独立跟踪器:

public static void spawnDetachedTrail(CombatEngineAPI engine,
        Vector2f location, Vector2f velocity) {
    engine.addPlugin(new DetachedTrailTracker(location, velocity));
}

SimpleEntity 的坐标构造器保存位置副本,跟踪器也单独保存速度副本。局部构造如下:

private DetachedTrailTracker(Vector2f location, Vector2f velocity) {
    anchor = new SimpleEntity(new Vector2f(location));
    this.velocity = new Vector2f(velocity);
}

每帧先按初始速度的 0.65 倍移动锚点,再从新位置生成洋红色节点。导弹后续转向不会改变这份速度,所以残影继续沿开火瞬间的方向前进。0.75 秒后,跟踪器按 anchor 切断关联并退出;它与导弹尾迹拥有不同 ID 和结束条件。

控制采样、切段和收尾

两个跟踪器都在非暂停时间内累积 segmentTimer,每 0.05 秒提交一次节点。导弹跟踪器使用归零方式,让节点密度保持简洁;独立锚点减去一个间隔,能够保留超出阈值的少量累计时间:

segmentTimer += amount;
if (segmentTimer >= SEGMENT_INTERVAL) {
    segmentTimer -= SEGMENT_INTERVAL;
    addSegment();
}

导弹被直接移动到另一处以后,旧坐标和新坐标已经失去空间连续性。当前坐标与上一帧相差超过 250 su 时,旧 ID 已经代表跳变前的连续段,因此先切断,再为两层取得新 ID:

if (MathUtils.getDistance(lastLocation, projectile.getLocation())
        > TELEPORT_DISTANCE) {
    MagicTrailPlugin.cutTrailsOnEntity(projectile);
    bodyTrailId = MagicTrailPlugin.getUniqueID();
    glowTrailId = MagicTrailPlugin.getUniqueID();
}

命中、淡出和离场都结束当前弹体的采样。cutTrailsOnEntity(...) 只阻止新节点接回旧 tracker,已经生成的节点仍按自己的淡入、保持和淡出时间消失;随后移除每帧插件,避免没有弹体的跟踪器继续运行。

在战斗中触发位置跳变

准备 Console Commands,在场上只有玩家这枚测试导弹、弹体仍在飞行时执行:

runcode com.fs.starfarer.api.Global.getCombatEngine().getMissiles().get(0).getLocation().translate(600f, 0f);

这次直接改变导弹的位置,下一帧与此前位置相差 600 su,超过切段阈值。恢复战斗后观察旧段淡出、新段从新坐标开始。

常用方法速查表

方法保存或输入直接结果
getUniqueID()每条连续尾迹保存一个返回值后续同 ID 节点连接为一段轨迹
addTrailMemberSimple(...)单一速度、颜色和加色选择在 CONTRAILS_LAYER 提交基础节点
addTrailMemberAdvanced(...)起止状态、纹理、额外速度、选项与渲染层提交完整可调节点
cutTrailsOnEntity(...)建立节点时使用的关联实体当前关联 tracker 改用内部切断 ID,后续节点形成新段
spawnDetachedTrail(...)世界坐标与速度副本创建由 SimpleEntity 驱动的短寿命残影

完整最小例

测试弹体的 .proj 使用以下注册字段:

"onFireEffect": "data.scripts.gearin.visuals.GearInMagicTrail",

完整类为每枚弹体建立关联尾迹和独立残影。主体与辉光按弹体切断,洋红色残影按自己的 SimpleEntity 锚点结束。

完整成品:GearInMagicTrail
package data.scripts.gearin.visuals;

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

import org.lazywizard.lazylib.MathUtils;
import org.lazywizard.lazylib.VectorUtils;
import org.lazywizard.lazylib.combat.entities.SimpleEntity;
import org.lwjgl.util.vector.Vector2f;
import org.magiclib.plugins.MagicTrailPlugin;

import com.fs.starfarer.api.Global;
import com.fs.starfarer.api.combat.BaseEveryFrameCombatPlugin;
import com.fs.starfarer.api.combat.CombatEngineAPI;
import com.fs.starfarer.api.combat.CombatEngineLayers;
import com.fs.starfarer.api.combat.DamagingProjectileAPI;
import com.fs.starfarer.api.combat.OnFireEffectPlugin;
import com.fs.starfarer.api.combat.WeaponAPI;
import com.fs.starfarer.api.graphics.SpriteAPI;
import com.fs.starfarer.api.input.InputEventAPI;

public class GearInMagicTrail implements OnFireEffectPlugin {

    @Override
    public void onFire(DamagingProjectileAPI projectile, WeaponAPI weapon,
            CombatEngineAPI engine) {
        engine.addPlugin(new TrailTracker(projectile));
        Vector2f detachedVelocity = new Vector2f(projectile.getVelocity());
        detachedVelocity.scale(0.65f);
        spawnDetachedTrail(engine, projectile.getLocation(), detachedVelocity);
    }

    public static void spawnDetachedTrail(CombatEngineAPI engine,
            Vector2f location, Vector2f velocity) {
        engine.addPlugin(new DetachedTrailTracker(location, velocity));
    }

    private static final class TrailTracker extends BaseEveryFrameCombatPlugin {
        private static final float SEGMENT_INTERVAL = 0.05f;
        private static final float TELEPORT_DISTANCE = 250f;

        private final DamagingProjectileAPI projectile;
        private CombatEngineAPI engine;
        private SpriteAPI bodySprite;
        private SpriteAPI glowSprite;
        private float bodyTrailId;
        private float glowTrailId;
        private float segmentTimer;
        private Vector2f lastLocation;
        private final Map<String, Object> glowOptions = new HashMap<>();

        private TrailTracker(DamagingProjectileAPI projectile) {
            this.projectile = projectile;
            glowOptions.put("SIZE_PULSE_WIDTH", 3f);
            glowOptions.put("SIZE_PULSE_COUNT", 2);
            glowOptions.put("FORWARD_PROPAGATION", false);
        }

        @Override
        public void init(CombatEngineAPI engine) {
            this.engine = engine;
            bodySprite = Global.getSettings().getSprite("fx", "base_trail_smooth");
            glowSprite = Global.getSettings().getSprite("fx", "base_trail_contrail");
            bodyTrailId = MagicTrailPlugin.getUniqueID();
            glowTrailId = MagicTrailPlugin.getUniqueID();
            lastLocation = new Vector2f(projectile.getLocation());
            addSegments();
        }

        @Override
        public void advance(float amount, List<InputEventAPI> events) {
            if (engine.isPaused()) {
                return;
            }
            if (!engine.isEntityInPlay(projectile)
                    || projectile.isFading() || projectile.didDamage()) {
                cutAndRemove();
                return;
            }
            if (MathUtils.getDistance(lastLocation, projectile.getLocation())
                    > TELEPORT_DISTANCE) {
                MagicTrailPlugin.cutTrailsOnEntity(projectile);
                bodyTrailId = MagicTrailPlugin.getUniqueID();
                glowTrailId = MagicTrailPlugin.getUniqueID();
            }
            segmentTimer += amount;
            if (segmentTimer >= SEGMENT_INTERVAL) {
                segmentTimer = 0f;
                addSegments();
            }
            lastLocation.set(projectile.getLocation());
        }

        private void addSegments() {
            Vector2f location = projectile.getLocation();
            float facing = projectile.getFacing();
            MagicTrailPlugin.addTrailMemberSimple(
                    projectile, bodyTrailId, bodySprite, location,
                    0f, facing, 14f, 3f, new Color(120, 220, 255),
                    0.8f, 0.05f, 0.2f, 0.8f, true);
            MagicTrailPlugin.addTrailMemberAdvanced(
                    projectile, glowTrailId, glowSprite, location,
                    0f, 0f, facing, 0f, 0f, 22f, 6f,
                    new Color(90, 170, 255), new Color(30, 70, 180),
                    0.55f, 0.05f, 0.15f, 0.6f, true,
                    192f, 600f, -1f, new Vector2f(), glowOptions,
                    CombatEngineLayers.CONTRAILS_LAYER, 1f);
        }

        private void cutAndRemove() {
            MagicTrailPlugin.cutTrailsOnEntity(projectile);
            engine.removePlugin(this);
        }
    }

    private static final class DetachedTrailTracker
            extends BaseEveryFrameCombatPlugin {
        private static final float SEGMENT_INTERVAL = 0.05f;
        private static final float LIFETIME = 0.75f;

        private final SimpleEntity anchor;
        private final Vector2f velocity;
        private CombatEngineAPI engine;
        private SpriteAPI sprite;
        private float trailId;
        private float segmentTimer;
        private float elapsed;

        private DetachedTrailTracker(Vector2f location, Vector2f velocity) {
            anchor = new SimpleEntity(new Vector2f(location));
            this.velocity = new Vector2f(velocity);
        }

        @Override
        public void init(CombatEngineAPI engine) {
            this.engine = engine;
            sprite = Global.getSettings().getSprite("fx", "base_trail_smooth");
            trailId = MagicTrailPlugin.getUniqueID();
            addSegment();
        }

        @Override
        public void advance(float amount, List<InputEventAPI> events) {
            if (engine.isPaused()) {
                return;
            }
            elapsed += amount;
            if (elapsed >= LIFETIME) {
                MagicTrailPlugin.cutTrailsOnEntity(anchor);
                engine.removePlugin(this);
                return;
            }
            Vector2f movement = new Vector2f(velocity);
            movement.scale(amount);
            Vector2f.add(anchor.getLocation(), movement, anchor.getLocation());

            segmentTimer += amount;
            if (segmentTimer >= SEGMENT_INTERVAL) {
                segmentTimer -= SEGMENT_INTERVAL;
                addSegment();
            }
        }

        private void addSegment() {
            MagicTrailPlugin.addTrailMemberSimple(
                    anchor, trailId, sprite, anchor.getLocation(),
                    0f, VectorUtils.getFacing(velocity), 10f, 2f,
                    new Color(255, 90, 220), 0.65f,
                    0.02f, 0.08f, 0.35f, true);
        }
    }
}

验证方案

前置数据:Console Commands 已启用,测试敌舰使用不发射导弹的装配,便于按正文命令选择玩家弹体。

  1. 构建 MOD,启动游戏并进入能够连续发射测试导弹的战斗。
  2. 连续发射两枚导弹,观察每枚弹体后方的青色主体、蓝色辉光和洋红色残影。
  3. 让第一枚导弹转向,比较弹体关联尾迹与保持初始方向的独立残影。
  4. 观察蓝色辉光的首尾颜色、纹理滚动和两次尺寸脉冲。
  5. 暂停战斗 2 秒,确认节点提交和独立锚点移动同时停止,再恢复战斗。
  6. 场上只保留一枚飞行中的测试导弹,执行正文位置跳变命令,观察旧段留在原处淡出,新段从新位置开始。
  7. 让导弹命中目标或进入淡出,观察关联尾迹停止增加,已有节点继续完成自身生命周期。
  8. 等待发射后的 0.75 秒,确认洋红色残影停止增加;再次连续发射,比较各枚弹体的尾迹关联。

预期结果:

  • 每枚弹体拥有互不串联的主体、辉光与发射残影;
  • 主体层按单一颜色收窄,辉光层呈现颜色渐变、纹理滚动和尺寸脉冲;
  • 导弹转向后,SimpleEntity 残影仍按发射瞬间速度的 0.65 倍直线移动;
  • 非暂停状态下,跟踪器每 0.05 秒最多追加一组节点;
  • 位置跳变超过 250 su 时,两条弹体尾迹同时切段,新节点不会横跨 600 su;
  • 命中、淡出或离场后,导弹跟踪器停止提交节点;
  • 独立残影在 0.75 秒后停止提交节点;
  • starsector.log 没有未处理异常。

故障分析

  • 节点持续提交却没有连续尾迹:每次调用 getUniqueID(),各 tracker 只有一个节点,达不到连接所需的两个节点。
    • 解决方案:在跟踪器初始化时保存 ID,提交节点时反复使用该字段,只在明确切段后更新。

总结

尾迹跟踪器随弹体开始和结束,固定 ID 把相邻节点接成一段轨迹。节点坐标每次由代码提交,关联实体则用于切断;独立锚点沿自己的速度前进,两种关系因此可以分别控制。Simple 提供基础节点,Advanced 增加首尾状态、纹理与渲染属性。

导弹跟踪器让尾迹服从实体生命周期,SimpleEntity 则让纯计算位置也能拥有相同的切断关系。只要为每段轨迹保存自己的 ID,并在空间或生命周期不连续时主动切段,运行时生成的效果就能保持连续而不跨越不该连接的位置。

建议参考项

  • org.magiclib.plugins.MagicTrailPlugin:创建 ID、提交 Simple 或 Advanced 节点并切断实体关联;
  • org.lazywizard.lazylib.combat.entities.SimpleEntity:把位置、武器或舰船引擎包装为轻量战斗实体;
  • com.fs.starfarer.api.combat.OnFireEffectPlugin:在 projectile 生成后取得弹体和当前战斗引擎;
  • com.fs.starfarer.api.combat.CombatEngineAPI:添加和移除每枚弹体或独立锚点的跟踪插件。
在 GitHub 上编辑此页
上次更新: 2026/10/14 12:25
贡献者: 1847905557
上一页
配置 MagicLib 自动尾迹
下一页
配置 MagicLib 数据赏金