-
Notifications
You must be signed in to change notification settings - Fork 50
贡献指南 Develop Guide
This page does not have an English version yet.
本页为简要概述ZSim基础架构
ZSim分为两个主要入口模块,即webui.py和main.py,分别为临时UI和CLI入口,UI部分除常规前端代码外,还包含了所有的数据处理逻辑,未来重构时考虑单独拎出去,或保持在前端处理。
- 现阶段UI使用
Streamlit为Web框架,Plotly作为绘图工具 - UI部分是配置与启动并行模式的唯一入口
- WebUI启动的模拟使用
concurrent.futures.ProcessPoolExecutor直接运行模拟器对象,因此并不依赖CLI传递参数
UI部分的逻辑较为清晰,也并非ZSim的核心,所以不多介绍
ZSim CLI支持两种模拟模式:普通模式、并行模式。
普通模式直接使用配置文件中的信息启动,并直接将结果输出
并行模式下,CLI进程读取命令行参数,将自身作为单个子进程运行,将结果输出到主进程创建的目录中,并记录自身的命令行参数
CLI无法启动完整的并行模式,目前只能作为子进程,如有重构想法,请与开发组联系 WebUI启动的模拟并非使用CLI接口
ZSim模拟器由位于zsim\simulator\simulator_class.py的模拟器类启动,main_loop()函数为核心逻辑。每次主循环代表1 tick,即1/60秒,这可能与游戏本体的最小时间单位不同,但考虑到测帧是基于60帧的视频素材,故取此数值,以求最小的系统误差。
流程图:
flowchart TD
A[程序开始] --> B[初始化]
NoteB(模拟器框架\n角色对象\n随机数\n监听器 管理器\n......) -.-> B
B --> C[MainLoop开始]
C --> U[Update]
NoteU(更新Buff 检查持续时间\n更新Dot 判断是否有新的一跳\n更新所有和时间有关的事件\n......) -.-> U
U --> D
subgraph Preload[Preload阶段]
D[敌人进攻模块] --> E[APL模块\n负责产生想法]
NoteD(根据预设策略生成敌人进攻动作) -.-> D
E --> F{SwapCancelEngine}
NoteF(合轴检查器\n负责检查\n当前想法是否能实现) -.-> F
F -->|通过| G[ConfirmEngine]
NoteG(确认动作\n更新内部数据) -.-> G
G --> NoteActionReplace[打出新技能]
NoteActionReplace(ActionReplace\n处理快速支援和招架支援\n以及部分角色的特殊动作替换逻辑) --> G_1
end
subgraph Load[Load阶段]
F -->|不通过| I[驳回]
G_1[打出新技能] --> H[SkillEventSplit]
NoteH(将新技能\n分解并打包成事件集) -.-> H
H --> I{DamageEventJudge}
NoteI(当前tick是否有\n开始\n命中\n结束 事件) -.-> I
I -->|是| L[ScheduleEvent构造]
L --> M_1[Schedule.event_list.append\n向event_list添加新事件]
I --> J[BuffLoadLoop]
NoteJ(判断本tick是否有Buff要触发\n即Buff的 预触发) -.-> J
J --> K[buff_add]
NoteK(正式激活\n所有预触发Buff) -.-> K
end
subgraph Schedule[Schedule阶段]
K --> L_1{event_list中\n是否有事件需要处理}
L_1 -->|是| M[ScheduleEvent.event_start]
M --> S[Enemy.receive_hit]
S --> T[更新动态信息]
NoteT(异常条\n异常快照\n血量\n角色特殊状态\n后置Buff触发\n......) -.-> T
T --> U_1[更新异常条]
NoteU1(在Schedule之后更新异常条\n确保触发时机正确) -.-> U_1
U_1 --> Z[Report\n记录到CSV]
Z --> L_1
end
subgraph UpdateLoop[更新循环]
L_1 -->|否| O[tick+=1]
O --> P{是否达到时间限制}
P -->|是| Q[循环结束]
P -->|否| U
NoteU
end
Q --> R[生成战斗日志]
%% 样式定义
classDef preload fill:#e6f7ff,stroke:#3399ff,stroke-width:2px;
classDef load fill:#fff0e6,stroke:#ff9933,stroke-width:2px;
classDef schedule fill:#e6ffe6,stroke:#33cc33,stroke-width:2px;
classDef update fill:#f0f0f0,stroke:#666666,stroke-width:2px;
classDef note fill:#f9f9f9,stroke-dasharray: 5 5,stroke:#666,color:#333;
class Preload preload;
class Load load;
class Schedule schedule;
class UpdateLoop update;
class NoteB,NoteU,NoteD,NoteF,NoteG,NoteH,NoteJ,NoteK,NoteT,NoteActionReplace note;
class NoteU1 note;
每个tick(每次主循环)会按照固定顺序执行一系列事件:
- Tick Update(刻更新):tick +=1 ,同时处理随时间变化的buff、状态,如buff剩余时间递减;
- Preload(预加载):决定这个刻和未来某些刻可能要做什么,是APL代码运行的部分,包含了合轴、怪物攻击、QTE等判定,负责抛出将要释放的技能节点对象(
SkillNode); - Load(加载):大部分模块间交互发生的位置,负责对技能按命中段数与帧数进行拆解,决定被APL抛出的技能何时命中,决定当前tick是否有Buff触发、异常触发;
- ScheduledEvent(计划事件):负责计算伤害、异常、失衡,判定怪物失衡状态,储存属性异常的虚拟代理人。计划事件模块高度可扩展,可以允许向计划事件列表抛入任何对象并以特定逻辑执行。
- Report(结果记录):但单独线程运行的异步IO模块,可以接受任何模块要求的结果记录请求,主循环函数没有直接调用
叠甲:本架构参考了 Minecraft 的 tick 逻辑,但是核心代码定调过早,两位最初的开发者当时仅有初学者水平,您可能会看到很多明显有优化空间的代码和开发者的一路成长(shishan),不用担心什么,想改就改(=w=)
APL的核心代码位于zsim\sim_progress\Preload下的apl_unit以及APLModule下。数据结构以及总调用接口存放于apl_unit下,而APLModule负责详细业务逻辑。
-
初始化阶段:由位于
APLModule\APLParser.py负责解析APL脚本代码,其核心是字符串处理,最终输出成APL单元初始化需要的列表:list[dict],最终传入APL的构造函数(位于APLModule\APLClass.py)。每一行APL脚本代码最后都会被构造成一个APL子单元(apl_unit),而APLClass.apl_operator则存放了所有子单元,遍历自检的方法也在APLClass中。 -
子单元结构:每个APL子单元都含有若干个“条件子单元”, 具体业务逻辑位于
APLModule\SubConditionUnit下,这里也是APL业务逻辑的末端。 -
核心逻辑:
APLClass.apl_operator负责遍历所有的APL子单元,每个APL子单元都会进行自检,检查自身所有的 子条件单元 是否满足,如果有不满足的,则直接continue到下一个APL子单元。 -
后置处理:最终,APL会返回 首个自检通过 的APL子单元的结果(
APLUnit.result)。但是实际游玩过程中,玩家做出的操作也时常会被替换、覆盖(比如快支亮起时切人总是会打出快支二不能打出其他技能,弹刀同理),所以APL的输出结果并不能直接上交,而是需要经过ActionReplaceManager再处理,Manager位于APLModule\ActionReplaceManager.py -
输出:整个APL模块输出的结果是一个
SkillNode,并且交由Preload的后续步骤处理。注意:该SkillNode与整个模拟器后续运行过程中使用的SkillNode是不一致的(UUID不同),由于APL的输出结果未必会被Preload采纳(举例说明:脑子和手很想在第二帧就打出后续技能但实际上游戏程序并不会受理这个操作,因为上个动作还未结束或是其他原因),所以最后由Preload抛出、流通于整个ZSim流程中的SkillNode是重新构造过的。
另外需要强调的是APL和Preload的协作方式: APL作为模拟“玩家大脑”的存在,是每个tick都会运行的——这意味着它每个Tick都会抛出当前tick的最优操作(这是合轴功能实现的基础),而整个Preload结构则负责审查当前tick APL模块的想法是否能够实现。