# 声明

本blog对7.0.1的Arc-mobile进行了逆向,请勿用于非法用途

# 正文

# 初步

Hopper 字符串表包含:

  • N7cocos2d14ParticleSystemE
  • N7cocos2d18ParticleSystemQuadE
  • N10cocostudio14ParticleReaderE
  • N10cocostudio19ParticleDisplayDataE
  • Shader3DParticleColor
  • Shader3DParticleTexture
  • maxParticles
  • startParticleSize
  • finishParticleSize
  • particleLifespan

然后我们去资源文件的particle里找,可以看到:

particle/particle_arc.plist
particle/particle_arc_mirai_light.plist
particle/particle_arc_mirai_conflict.plist
particle/note_light.png
particle/note_conflict.png
particle/note_colorless.png
particle/note_sfx.png

# 初始化函数

# arc

这边可以看到701二进制的 0x100a5a460 这个sub读取了一大堆主题参数

不难注意到字符串里的plistScreenshot 2026-08-29 at 11.37.22 PM

这些东西通过cocos2dx的vfunc配置并启动粒子对象,然后进行循环初始化

最后它访问里 0x124写入0x2

Screenshot 2026-08-29 at 11.38.54 PM

# hold

hold这边 0x100099adc和0x100c1ea80引用,不难注意到这个firstHoldAction:Screenshot 2026-08-29 at 11.41.02 PM

这里hold里action的逻辑和particle的plist是拆分开的,用独立的particle system资源驱动

# 具体参考

# 首先

光说这些没用,我们不妨看看Arcade-Plus怎么写的(这里让ai画了个图):

ArcGameplayManager.Load(chart, audio)
 ├─ ArcTimingManager.Load(...)
 ├─ ArcHoldNoteManager.Load(chart.Holds)
 │   └─ ArcHold.Instantiate() -> HoldNotePrefab
 └─ ArcArcManager.Load(chart.Arcs)
     ├─ ArcArc.Instantiate()
     ├─ ArcArc.Rebuild()
     └─ CalculateArcRelationship()

每帧:
ArcGameplayManager.Update()
 ├─ 推进 AudioTiming / ChartTiming
 ├─ ArcHoldNoteManager.Update()
 │   ├─ JudgeHoldNotes()(Auto 模式)
 │   └─ RenderHoldNotes()
 ├─ ArcArcManager.Update()
 │   ├─ JudgeArcs()(Auto 模式)
 │   └─ RenderArcs()
 └─ ArcEffectManager.Update()
     └─ 更新 6 个 Hold VisualEffect 的位置和 Play/Stop 状态

(这里Hold/Arc的开始、结束、判定窗口以及粒子开关都由同一条毫秒时间轴驱动,注意不是粒子系统自身的时间)

# hold

# 初始化

ArcEffectManager.Start() 创建Tap粒子对象池,并保存6个Hold粒子实例的初始世界坐标:

for (int i = 0; i < 6; ++i) //HoldNoteEffects是长度为6的VisualEffect
  													//这里是对应6个track
    HoldNoteEffectPosition[i] = HoldNoteEffects[i].transform.position;

hold本体是ArcHoldNoteManager创建的

# 判定部分

然后ArcHoldNoteManager.JudgeHoldNotes每帧调用

ArcEffectManager.Instance.ResetHoldNoteEffect();

该函数把6条轨道的holdEffectStatus全部置为false然后遍历所有 Hold

注意两个特殊的跳过:NoInput、Judged

然后用下面的公式:

currentTiming >= Timing && currentTiming <= EndTiming

Judging = true;

然后首次进入播放一个Tap音效

用SetHoldNoteEffect(t.Track, true)打开对应track的hold粒子

超过EndTiming的部分:

Judging = false;
Judged = true;

没有到开始时间的:

ShouldPlayAudio = true

同一轨道上多个同时重叠的 Hold 只需要一个粒子实例,状态数组表达的是该轨道本帧是否至少有一个正在判定的 Hold,而不是 Hold 数量,这里注意下

# 判定面

ArcEffectManager.Update()对每个track执行

HoldNoteEffects[track].transform.position =
    EffectPlane.GetPositionOnPlane(HoldNoteEffectPosition[track]);

这一步把预置点转换到ArcEffectPlane所在的渲染平面,保证粒子不会因为相机/轨道空间变化而停留在错误的世界位置

然后比较期望状态holdEffectStatus[track]与实际状态holdEffectPlaying[track],

false -> true:Play(),随后Simulate(1f / 60f, 60)

true -> false:Stop(),随后同样Simulate(1f / 60f, 60)

Simulate(1/60, 60)的作用是把效果推进1秒,避免Play/Stop后粒子从空状态或旧状态突变

注意它不是游戏计时器,粒子是否显示仍由holdEffectStatus决定

# arc

# arc的segment

首先要明确一点:arc本体是网格分段

这里ArcArcRenderer.BuildSegments() 根据arc持续时间、EffectiveSmoothness、曲线函数(ArcAlgorithm.X/Y)、起止高度、时间到空间的换算将arc拆成segment(代码内叫ArcArcSegmentComponent)

每个segment生成网格,并同时生成阴影网格

ArcArcSegmentComponent.BuildSegment()保存FromTiming、ToTiming、FromPos、ToPos,暂时不生成粒子

Arc.shader 通过 _From、_To 裁切 segment 的 UV Y 范围,通过 uv2.x 在低色与高色之间插值:

if (i.uv.y < _From || i.uv.y > _To) return 0;
float4 inColor = lerp(_LowColor, _HighColor, clamp(abs(i.uv2.x), 0, 1));
//其实这个是hlsl但是似乎我的blog不支持hlsl的代码高亮所以写的c#

所以arc的移动、裁切、渐变主体是确定性网格渲染,而粒子只是命中/跟随反馈

# arc粒子的注入点

ArcEffectManager.SetParticleArcColor(start, end)对6个Hold VisualEffect设置StartColor和EndColor,然后转发到ArcArcManager.SetParticleArcColor(),后者取得 ArcNotePrefab上的 ArcArcRenderer,并对其JudgeEffect设置同名颜色属性

(SetParticleArcTexture(texture) 具有同样的双路转发,hold的6个VisualEffect设置 Texture,然后Arc prefab的JudgeEffect设置Texture)

主题这部分先不写了

# 为啥写这东西

闲的

好吧,主要是因为oa next调这玩意调半天才调好,期间多次求助ai,所以记录下