语言模块基础迭代

This commit is contained in:
2025-11-12 19:46:16 +08:00
parent a634d92b9c
commit 8cf629b1c6
41 changed files with 6105 additions and 1319 deletions
+9 -13
View File
@@ -463,19 +463,15 @@ MonoBehaviour:
m_PostInfinity: 2
m_RotationOrder: 4
_filter:
- 0.0048309183
- 0.016401261
- 0.04532712
- 0.08293076
- 0.12053438
- 0.14946026
- 0.1610306
- 0.14946026
- 0.12053438
- 0.08293076
- 0.04532712
- 0.016401261
- 0.0048309183
- 0.007228916
- 0.043749984
- 0.12409639
- 0.20444278
- 0.24096388
- 0.20444278
- 0.12409639
- 0.043749984
- 0.007228916
--- !u!114 &-6086986437317696584
MonoBehaviour:
m_ObjectHideFlags: 3
+496
View File
@@ -0,0 +1,496 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &125585505
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 125585506}
- component: {fileID: 125585507}
m_Layer: 0
m_Name: LanguageParticleSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &125585506
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 125585505}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -2.8477545, y: -5.088608, z: -8.612933}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &125585507
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 125585505}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 79af040068968934c9a7d092267034f6, type: 3}
m_Name:
m_EditorClassIdentifier:
worldCanvas: {fileID: 1665911935}
canvasSize: 10
chineseFontAsset: {fileID: 11400000, guid: f242fb3dde1933640859f1c54591c9c8, type: 2}
floatingParticlePrefab: {fileID: 0}
candidateParticlePrefab: {fileID: 0}
floatingCount: 100
candidateCount: 50
redParticleCount: 8
connectionDistance: 2
textMargin: 1
targetSentence: "\u522B\u8FC7\u6765\u6211\u611F\u89C9\u5BB3\u6015"
waveformAmplitude: 0.3
interactionRadius: 0.5
--- !u!1 &786157861
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 786157866}
- component: {fileID: 786157865}
- component: {fileID: 786157864}
- component: {fileID: 786157862}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &786157862
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 786157861}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_VolumeFrameworkUpdateModeOption: 2
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_AllowHDROutput: 1
m_UseScreenCoordOverride: 0
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_Version: 2
m_TaaSettings:
quality: 3
frameInfluence: 0.1
jitterScale: 1
mipBias: 0
varianceClampScale: 0.9
contrastAdaptiveSharpening: 0
--- !u!81 &786157864
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 786157861}
m_Enabled: 1
--- !u!20 &786157865
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 786157861}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 1
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &786157866
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 786157861}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10.72}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1665911932
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1665911936}
- component: {fileID: 1665911935}
- component: {fileID: 1665911934}
- component: {fileID: 1665911933}
m_Layer: 5
m_Name: Canvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1665911933
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1665911932}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &1665911934
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1665911932}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UiScaleMode: 0
m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1
m_ReferenceResolution: {x: 800, y: 600}
m_ScreenMatchMode: 0
m_MatchWidthOrHeight: 0
m_PhysicalUnit: 3
m_FallbackScreenDPI: 96
m_DefaultSpriteDPI: 96
m_DynamicPixelsPerUnit: 1
m_PresetInfoIsWorld: 1
--- !u!223 &1665911935
Canvas:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1665911932}
m_Enabled: 1
serializedVersion: 3
m_RenderMode: 2
m_Camera: {fileID: 0}
m_PlaneDistance: 100
m_PixelPerfect: 0
m_ReceivesEvents: 1
m_OverrideSorting: 0
m_OverridePixelPerfect: 0
m_SortingBucketNormalizedSize: 0
m_VertexColorAlwaysGammaSpace: 0
m_AdditionalShaderChannelsFlag: 25
m_UpdateRectTransformForStandalone: 0
m_SortingLayerID: 0
m_SortingOrder: 0
m_TargetDisplay: 0
--- !u!224 &1665911936
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1665911932}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 10}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &1764870496
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1764870499}
- component: {fileID: 1764870498}
- component: {fileID: 1764870497}
m_Layer: 0
m_Name: EventSystem
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1764870497
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1764870496}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4f231c4fb786f3946a6b90b886c48677, type: 3}
m_Name:
m_EditorClassIdentifier:
m_SendPointerHoverToParent: 1
m_HorizontalAxis: Horizontal
m_VerticalAxis: Vertical
m_SubmitButton: Submit
m_CancelButton: Cancel
m_InputActionsPerSecond: 10
m_RepeatDelay: 0.5
m_ForceModuleActive: 0
--- !u!114 &1764870498
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1764870496}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_FirstSelected: {fileID: 0}
m_sendNavigationEvents: 1
m_DragThreshold: 10
--- !u!4 &1764870499
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1764870496}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 1665911936}
- {fileID: 786157866}
- {fileID: 1764870499}
- {fileID: 125585506}
+7
View File
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a10c0994c14362743ab6f65c6382410d
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -419,6 +419,8 @@ namespace AibisDream.Framework
BreakCamera,
DreamCamera,
Subway,
SubwayTV
SubwayTV,
Language,
LanguageDeep
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c7cbe6739626ae5458cf9c27e963b310
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 632d2ea6fbd2c684e90cbd7f7e77036a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a1958c693a891564ca52d09de765d438
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,203 @@
using UnityEngine;
using System.Collections.Generic;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 候选粒子(可交互的主要粒子)
/// </summary>
public class CandidateParticle : TextParticle
{
[Header("粒子状态")]
public bool isRed = false; // 是否是红色目标粒子
public bool isOrange = false; // 是否受到效果影响(橙色)
public bool originalIsRed = false; // 原始是否为红色
public bool hasEffect = false; // 是否有效果
public bool isTemporaryRed = false; // 临时红色状态
[Header("连接")]
public List<CandidateParticle> connections = new List<CandidateParticle>();
[Header("效果")]
public float changeSpeedMultiplier = 1f;
public Vector2 vibrationOffset;
public float vibrationPhase;
public float effectTimer;
public float effectAlpha;
public float effectScale = 1f;
public float effectGlow;
[Header("焦虑效果")]
public float anxietyShakePhase;
public float anxietyShakeIntensity;
public float anxietyFloatPhase;
public float anxietyFloatSpeed;
public Vector2 anxietyShakeOffset;
public float anxietyFloatY;
[Header("波形关联")]
public float calmProgress = 0f; // 平静进度(0-1
public float dissolveProgress = 0f; // 消散进度(0-1
public WaveformPoint waveformPoint; // 关联的波形点
protected override void Awake()
{
base.Awake();
baseAlpha = 0.6f; // 0-1 范围的透明度
probability = Random.Range(0.1f, 1f);
alpha = baseAlpha + probability * 0.4f; // 最大 1.0
size = 3f;
velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f));
changeInterval = Random.Range(1f, 2.5f);
if (textMesh != null)
{
textMesh.fontSize = size;
}
}
protected override void Update()
{
base.Update();
// 更新效果动画
if (hasEffect)
{
UpdateEffectAnimation();
}
// 更新焦虑效果
if (isRed && (anxietyShakeIntensity > 0))
{
UpdateAnxietyEffect();
}
}
private void UpdateEffectAnimation()
{
if (effectTimer > 0)
{
effectTimer -= Time.deltaTime;
// 振动效果
vibrationPhase += Time.deltaTime * 15f;
float vibrationIntensity = 0.02f;
vibrationOffset = new Vector2(
Mathf.Cos(vibrationPhase) * vibrationIntensity,
Mathf.Sin(vibrationPhase * 1.3f) * vibrationIntensity
);
// 透明度动画(0-1 范围)
effectAlpha = Mathf.Lerp(0.6f, 1f, (Mathf.Sin(Time.time * 3f) + 1f) / 2f);
// 缩放动画
effectScale = Mathf.Lerp(0.95f, 1.05f, (Mathf.Sin(Time.time * 4.5f) + 1f) / 2f);
// 发光强度
effectGlow = Mathf.Lerp(0.06f, 0.1f, (effectAlpha - 0.6f) / 0.4f);
}
}
private void UpdateAnxietyEffect()
{
// 根据平静进度减少抖动强度
float shakeIntensity = anxietyShakeIntensity * (1 - calmProgress) * 0.01f;
float floatAmplitude = 0.03f * (1 - calmProgress);
if (calmProgress < 0.9f)
{
anxietyShakePhase += Time.deltaTime * 10f;
anxietyFloatPhase += anxietyFloatSpeed;
}
// 计算抖动偏移
anxietyShakeOffset = new Vector2(
Mathf.Cos(anxietyShakePhase) * shakeIntensity + Mathf.Sin(anxietyShakePhase * 1.7f) * shakeIntensity * 0.5f,
Mathf.Sin(anxietyShakePhase * 1.3f) * shakeIntensity + Mathf.Cos(anxietyShakePhase * 0.9f) * shakeIntensity * 0.5f
);
// 计算漂浮偏移
anxietyFloatY = Mathf.Sin(anxietyFloatPhase) * floatAmplitude;
}
protected override void UpdateVisuals()
{
if (textMesh == null) return;
// 计算显示位置
Vector3 displayOffset = (Vector3)(vibrationOffset + anxietyShakeOffset) + Vector3.up * anxietyFloatY;
textMesh.transform.localPosition = displayOffset;
// 计算颜色和透明度
Color color = Color.white;
float displayAlpha = alpha;
if (hasEffect && effectTimer > 0)
{
displayAlpha = effectAlpha;
textMesh.transform.localScale = Vector3.one * effectScale;
}
// 根据消散进度减少透明度
displayAlpha *= (1 - dissolveProgress);
if (isRed)
{
// 红色粒子:根据平静进度调整颜色
float r = Mathf.Lerp(1f, 0.78f, calmProgress);
float g = Mathf.Lerp(0.39f, 0.59f, calmProgress);
float b = Mathf.Lerp(0.39f, 0.59f, calmProgress);
color = new Color(r, g, b, displayAlpha); // displayAlpha 已经是 0-1 范围
}
else if (isOrange)
{
color = new Color(1f, 0.59f, 0.2f, displayAlpha);
}
else
{
color = new Color(0.2f, 0.78f, 1f, displayAlpha);
}
textMesh.color = color;
// 更新光晕
if (glowSprite != null)
{
glowSprite.color = new Color(color.r, color.g, color.b, color.a * 0.5f);
float glowScale = probability * 0.5f;
if (hasEffect)
glowScale += effectGlow;
glowSprite.transform.localScale = Vector3.one * glowScale;
}
}
public void InitializeAnxietyEffect()
{
anxietyShakePhase = Random.Range(0f, Mathf.PI * 2f);
anxietyShakeIntensity = Random.Range(2f, 4f);
anxietyFloatPhase = Random.Range(0f, Mathf.PI * 2f);
anxietyFloatSpeed = Random.Range(0.02f, 0.04f);
}
public void ApplyEffect(float duration = 1f)
{
hasEffect = true;
effectTimer = duration;
isOrange = true;
isRed = false;
changeSpeedMultiplier = 3f;
vibrationPhase = Random.Range(0f, Mathf.PI * 2f);
}
public void RemoveEffect()
{
hasEffect = false;
isOrange = false;
isRed = false;
changeSpeedMultiplier = 1f;
vibrationOffset = Vector2.zero;
effectTimer = 0;
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: bc6c154e0dac2e6469211e12b1f05fce
guid: fe9b66f677d7a024c8f232d03839b2ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,210 @@
using UnityEngine;
using System.Collections.Generic;
using Shapes;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 连接线渲染器:使用Shapes绘制粒子间的连接线
/// </summary>
[ExecuteAlways]
public class ConnectionRenderer : ImmediateModeShapeDrawer
{
[Header("引用")]
public LanguageParticleManager manager;
[Header("连接线设置")]
public float connectionDistance = 2f;
public Color blueConnectionColor = new Color(0.39f, 0.78f, 1f, 0.7f);
public Color redConnectionColor = new Color(1f, 0.39f, 0.39f, 1f);
public float blueLineThickness = 0.02f;
public float redLineThickness = 0.04f;
[Header("效果传播设置")]
public Color propagationColor = new Color(1f, 0.59f, 0.2f, 1f);
public float propagationLineThickness = 0.03f;
private List<CandidateParticle> candidateParticles;
private List<EffectPropagation> effectPropagations;
private CompletionPhase completionPhase;
private float fadeOutAlpha;
public void SetData(
List<CandidateParticle> candidates,
List<EffectPropagation> propagations,
CompletionPhase phase,
float fadeAlpha)
{
candidateParticles = candidates;
effectPropagations = propagations;
completionPhase = phase;
fadeOutAlpha = fadeAlpha;
}
public override void DrawShapes(Camera cam)
{
if (candidateParticles == null || candidateParticles.Count == 0)
return;
using (Draw.Command(cam))
{
Draw.BlendMode = ShapesBlendMode.Transparent;
Draw.ZTest = UnityEngine.Rendering.CompareFunction.Always;
// 绘制蓝色粒子间的连线
if (completionPhase == CompletionPhase.None || completionPhase == CompletionPhase.Arranging)
{
DrawBlueConnections();
}
// 绘制红色粒子间的连线
if (completionPhase == CompletionPhase.None || completionPhase == CompletionPhase.Arranging)
{
DrawRedConnections();
}
// 绘制效果传播线
if (effectPropagations != null && effectPropagations.Count > 0)
{
DrawPropagationEffects();
}
}
}
private void DrawBlueConnections()
{
Draw.LineGeometry = LineGeometry.Flat2D;
Draw.Thickness = blueLineThickness;
for (int i = 0; i < candidateParticles.Count; i++)
{
for (int j = i + 1; j < candidateParticles.Count; j++)
{
var p1 = candidateParticles[i];
var p2 = candidateParticles[j];
// 跳过红色粒子
if (p1.isRed && p2.isRed) continue;
float distance = Vector3.Distance(p1.transform.position, p2.transform.position);
if (distance < connectionDistance)
{
float alpha = Mathf.Lerp(0.7f, 0f, distance / connectionDistance) * fadeOutAlpha;
Color lineColor = blueConnectionColor;
lineColor.a = alpha;
Draw.Line(p1.transform.position, p2.transform.position, lineColor);
}
}
}
}
private void DrawRedConnections()
{
Draw.LineGeometry = LineGeometry.Flat2D;
Draw.Thickness = redLineThickness;
var redParticles = new List<CandidateParticle>();
foreach (var p in candidateParticles)
{
if (p.isRed)
redParticles.Add(p);
}
for (int i = 0; i < redParticles.Count; i++)
{
for (int j = i + 1; j < redParticles.Count; j++)
{
float distance = Vector3.Distance(
redParticles[i].transform.position,
redParticles[j].transform.position
);
if (distance < connectionDistance)
{
float alpha = Mathf.Lerp(1f, 0.4f, distance / connectionDistance);
Color lineColor = redConnectionColor;
lineColor.a = alpha;
Draw.Line(redParticles[i].transform.position, redParticles[j].transform.position, lineColor);
}
}
}
}
private void DrawPropagationEffects()
{
Draw.LineGeometry = LineGeometry.Flat2D;
Draw.Thickness = propagationLineThickness;
foreach (var propagation in effectPropagations)
{
if (propagation.PropagatedParticles == null) continue;
foreach (var info in propagation.PropagatedParticles)
{
if (info.source == null || info.particle == null) continue;
// 绘制连接线
Color lineColor = propagationColor;
lineColor.a = 0.3f;
Draw.Line(info.source.transform.position, info.particle.transform.position, lineColor);
// 绘制脉冲光点
if (info.pulseProgress < 1f)
{
Vector3 pulsePos = Vector3.Lerp(
info.source.transform.position,
info.particle.transform.position,
info.pulseProgress
);
float pulseSize = Mathf.Lerp(0.03f, 0.08f, info.pulseProgress);
float pulseAlpha = Mathf.Lerp(1f, 0f, info.pulseProgress);
Color pulseColor = new Color(1f, 0.78f, 0.39f, pulseAlpha);
Draw.Ring(pulsePos, pulseSize, pulseSize * 0.5f, pulseColor);
// 光晕
pulseColor.a = pulseAlpha * 0.5f;
Draw.Disc(pulsePos, pulseSize * 1.5f, pulseColor);
}
// 绘制箭头
if (info.pulseProgress > 0.5f && info.pulseProgress < 1f)
{
Vector3 arrowPos = Vector3.Lerp(
info.source.transform.position,
info.particle.transform.position,
0.5f
);
Vector3 direction = (info.particle.transform.position - info.source.transform.position).normalized;
float angle = Mathf.Atan2(direction.y, direction.x);
Color arrowColor = new Color(1f, 0.78f, 0.39f, 0.8f);
DrawArrow(arrowPos, angle, 0.1f, arrowColor);
}
}
}
}
private void DrawArrow(Vector3 position, float angle, float size, Color color)
{
Vector3 tip = position + new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * size;
Vector3 left = position + new Vector3(
Mathf.Cos(angle + 2.5f),
Mathf.Sin(angle + 2.5f),
0
) * size * 0.5f;
Vector3 right = position + new Vector3(
Mathf.Cos(angle - 2.5f),
Mathf.Sin(angle - 2.5f),
0
) * size * 0.5f;
Draw.Triangle(tip, left, right, color);
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: c5da4de45a1d4c741b06d9c9c0519ff4
guid: d7feb80748ec75e4eb986d0d676dfa04
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,154 @@
using UnityEngine;
using System.Collections.Generic;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 效果传播信息
/// </summary>
public class PropagationInfo
{
public CandidateParticle particle;
public CandidateParticle source;
public int step;
public float pulseProgress;
public PropagationInfo(CandidateParticle particle, CandidateParticle source, int step)
{
this.particle = particle;
this.source = source;
this.step = step;
this.pulseProgress = 0f;
}
}
/// <summary>
/// 效果传播系统:管理从红色粒子到蓝色粒子的效果传递
/// </summary>
public class EffectPropagation
{
private CandidateParticle redParticle;
private List<PropagationInfo> propagatedParticles = new List<PropagationInfo>();
private List<CandidateParticle> propagationQueue = new List<CandidateParticle>();
private float lifeTimer;
private float propagationDelay = 0.05f;
private float propagationCounter = 0f;
private int maxPropagationSteps = 8;
private int currentPropagationStep = 0;
public bool IsActive => lifeTimer > 0;
public List<PropagationInfo> PropagatedParticles => propagatedParticles;
public EffectPropagation(CandidateParticle redParticle, float duration = 1f)
{
this.redParticle = redParticle;
this.lifeTimer = duration;
// 从红色粒子开始传播
var connectedBlues = FindConnectedBlues(redParticle);
if (connectedBlues.Count > 0 && currentPropagationStep < maxPropagationSteps)
{
// 随机选择一个连接的蓝色粒子
var targetParticle = connectedBlues[Random.Range(0, connectedBlues.Count)];
ApplyEffectToParticle(targetParticle);
// 记录传播信息
propagatedParticles.Add(new PropagationInfo(targetParticle, redParticle, 1));
// 加入传播队列
propagationQueue.Add(targetParticle);
currentPropagationStep = 1;
}
}
public void Update(float deltaTime)
{
lifeTimer -= deltaTime;
propagationCounter += deltaTime;
// 定期传播
if (propagationCounter >= propagationDelay && currentPropagationStep < maxPropagationSteps)
{
propagationCounter = 0f;
List<CandidateParticle> nextQueue = new List<CandidateParticle>();
foreach (var sourceParticle in propagationQueue)
{
if (currentPropagationStep >= maxPropagationSteps) break;
var connectedBlues = FindConnectedBlues(sourceParticle);
if (connectedBlues.Count > 0)
{
var targetParticle = connectedBlues[Random.Range(0, connectedBlues.Count)];
ApplyEffectToParticle(targetParticle);
currentPropagationStep++;
propagatedParticles.Add(new PropagationInfo(targetParticle, sourceParticle, currentPropagationStep));
if (currentPropagationStep < maxPropagationSteps)
{
nextQueue.Add(targetParticle);
}
}
}
propagationQueue = nextQueue;
}
// 更新所有传播粒子的脉冲动画
foreach (var info in propagatedParticles)
{
info.pulseProgress += deltaTime * 5f;
if (info.pulseProgress > 1f)
info.pulseProgress = 1f;
}
}
private List<CandidateParticle> FindConnectedBlues(CandidateParticle particle)
{
List<CandidateParticle> connected = new List<CandidateParticle>();
HashSet<CandidateParticle> usedParticles = new HashSet<CandidateParticle>();
// 记录已使用的粒子
usedParticles.Add(redParticle);
foreach (var prop in propagatedParticles)
{
usedParticles.Add(prop.particle);
}
// 只检查真正连接的粒子
foreach (var connectedParticle in particle.connections)
{
if (!connectedParticle.hasEffect &&
!usedParticles.Contains(connectedParticle) &&
!connectedParticle.isRed &&
!connectedParticle.isOrange)
{
connected.Add(connectedParticle);
}
}
return connected;
}
private void ApplyEffectToParticle(CandidateParticle particle)
{
if (particle.hasEffect) return;
if (particle.isRed && particle.originalIsRed) return;
particle.ApplyEffect(lifeTimer);
}
public void Cleanup()
{
foreach (var info in propagatedParticles)
{
info.particle.RemoveEffect();
}
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ce8085db413e67d4c9ddd8a3415ae3cf
guid: 13d2bba97b998f049bb52f097b0f597c
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,42 @@
using UnityEngine;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 背景浮动文字粒子
/// </summary>
public class FloatingTextParticle : TextParticle
{
protected override void Awake()
{
base.Awake();
// 背景文字特性
baseAlpha = Random.Range(0.04f, 0.1f); // 0-1 范围的透明度
alpha = baseAlpha;
size = Random.Range(1.5f, 2.5f);
velocity = new Vector2(Random.Range(-0.3f, 0.3f), Random.Range(-0.3f, 0.3f));
changeInterval = Random.Range(0.5f, 0.7f);
if (textMesh != null)
{
textMesh.fontSize = size;
textMesh.color = new Color(0.7f, 0.78f, 1f, alpha);
}
}
protected override void UpdateVisuals()
{
if (textMesh != null)
{
Color color = new Color(0.7f, 0.78f, 1f, alpha);
textMesh.color = color;
}
if (glowSprite != null)
{
glowSprite.color = new Color(0.7f, 0.78f, 1f, alpha * 0.3f);
}
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 68f10137fe541b44485fa63022e29524
guid: f7f33e95ffe9c50418758691861b6ebd
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,795 @@
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using Shapes;
using TMPro;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 游戏完成阶段
/// </summary>
public enum CompletionPhase
{
None, // 未完成
Arranging, // 排列中
Revealing, // 揭示中
Completed // 已完成
}
/// <summary>
/// 语言粒子系统主管理器
/// </summary>
public class LanguageParticleManager : MonoBehaviour
{
[Header("Canvas设置")]
[SerializeField] private Canvas worldCanvas;
[SerializeField] private float canvasSize = 10f;
[Header("字体设置")]
[SerializeField] private TMP_FontAsset chineseFontAsset;
[Header("粒子预制体")]
[SerializeField] private GameObject floatingParticlePrefab;
[SerializeField] private GameObject candidateParticlePrefab;
[Header("粒子数量")]
[SerializeField] private int floatingCount = 100;
[SerializeField] private int candidateCount = 50;
[SerializeField] private int redParticleCount = 8;
[Header("游戏参数")]
[SerializeField] private float connectionDistance = 2f; // Unity单位
[SerializeField] private float textMargin = 1f;
[Header("目标句子")]
[SerializeField] private string targetSentence = "别过来我感觉害怕";
[Header("波形参数")]
[SerializeField] private float waveformAmplitude = 0.3f;
[SerializeField] private float interactionRadius = 0.5f;
[Header("鼠标交互参数")]
[SerializeField] private float mouseInteractionRadius = 1.5f;
[SerializeField] private float repulsionForce = 2f;
[SerializeField] private float attractionForce = 1.5f;
// 粒子列表
private List<FloatingTextParticle> floatingParticles = new List<FloatingTextParticle>();
private List<CandidateParticle> candidateParticles = new List<CandidateParticle>();
private List<CandidateParticle> redParticles = new List<CandidateParticle>();
// 效果系统
private List<EffectPropagation> effectPropagations = new List<EffectPropagation>();
private Dictionary<string, bool> previousConnections = new Dictionary<string, bool>();
private bool initializationComplete = false;
private int initializationFrames = 0;
// 游戏状态
private bool isCompleted = false;
private CompletionPhase completionPhase = CompletionPhase.None;
private float completionTimer = 0f;
private float fadeOutAlpha = 1f;
private List<CandidateParticle> orderedRedParticles = new List<CandidateParticle>();
private List<Vector3> targetPositions = new List<Vector3>();
// 波形系统
private List<WaveformPoint> waveformPoints = new List<WaveformPoint>();
private float calmProgress = 0f;
private Vector2 lastMousePos;
private float mouseDragSpeed;
// 边界
private Bounds movementBounds;
// 连接渲染(使用Shapes
private ConnectionRenderer connectionRenderer;
private WaveformRenderer waveformRenderer;
private void Start()
{
InitializeCanvas();
InitializeParticles();
InitializeRenderers();
}
private void InitializeCanvas()
{
// 如果没有指定canvas,创建一个
if (worldCanvas == null)
{
GameObject canvasObj = new GameObject("Language Canvas");
canvasObj.transform.SetParent(transform);
canvasObj.transform.localPosition = Vector3.zero;
worldCanvas = canvasObj.AddComponent<Canvas>();
worldCanvas.renderMode = RenderMode.WorldSpace;
RectTransform rectTransform = canvasObj.GetComponent<RectTransform>();
rectTransform.sizeDelta = new Vector2(canvasSize, canvasSize);
}
// 确保 Canvas 配置正确
if (worldCanvas != null)
{
// 设置 WorldSpace Canvas 的相机
if (worldCanvas.worldCamera == null)
{
worldCanvas.worldCamera = Camera.main;
}
}
// 设置运动边界
float halfSize = canvasSize / 2f - textMargin;
movementBounds = new Bounds(
worldCanvas.transform.position,
new Vector3(halfSize * 2f, halfSize * 2f, 0f)
);
}
private void InitializeParticles()
{
// 创建背景浮动文字
for (int i = 0; i < floatingCount; i++)
{
Vector3 pos = GetRandomPositionInBounds();
GameObject obj = CreateParticleObject(pos, $"FloatingParticle_{i}");
FloatingTextParticle particle = obj.AddComponent<FloatingTextParticle>();
particle.SetFont(chineseFontAsset);
particle.SetMovementBounds(movementBounds);
floatingParticles.Add(particle);
}
// 创建候选粒子
for (int i = 0; i < candidateCount; i++)
{
Vector3 pos = GetRandomPositionInBounds();
GameObject obj = CreateParticleObject(pos, $"CandidateParticle_{i}");
CandidateParticle particle = obj.AddComponent<CandidateParticle>();
particle.SetFont(chineseFontAsset);
particle.SetMovementBounds(movementBounds);
candidateParticles.Add(particle);
}
// 选择红色粒子
SelectRedParticles();
}
private GameObject CreateParticleObject(Vector3 position, string name)
{
GameObject obj = new GameObject(name);
// 添加 RectTransform(在 Canvas 下必需)
RectTransform rectTransform = obj.AddComponent<RectTransform>();
rectTransform.SetParent(worldCanvas.transform, false);
rectTransform.position = position;
rectTransform.localScale = Vector3.one;
rectTransform.sizeDelta = Vector2.zero; // 父对象不需要大小
return obj;
}
private Vector3 GetRandomPositionInBounds()
{
float x = Random.Range(movementBounds.min.x, movementBounds.max.x);
float y = Random.Range(movementBounds.min.y, movementBounds.max.y);
return new Vector3(x, y, 0f);
}
private void InitializeRenderers()
{
// 连接线渲染器
GameObject connObj = new GameObject("ConnectionRenderer");
connObj.transform.SetParent(transform);
connObj.transform.localPosition = Vector3.zero;
connectionRenderer = connObj.AddComponent<ConnectionRenderer>();
connectionRenderer.manager = this;
connectionRenderer.connectionDistance = connectionDistance;
// 波形渲染器
GameObject waveObj = new GameObject("WaveformRenderer");
waveObj.transform.SetParent(transform);
waveObj.transform.localPosition = Vector3.zero;
waveformRenderer = waveObj.AddComponent<WaveformRenderer>();
waveformRenderer.interactionRadius = interactionRadius;
}
private void SelectRedParticles()
{
redParticles.Clear();
List<CandidateParticle> shuffled = candidateParticles.OrderBy(x => Random.value).ToList();
float minDistance = Mathf.Min(movementBounds.size.x, movementBounds.size.y) / (redParticleCount * 0.8f);
foreach (var particle in shuffled)
{
if (redParticles.Count >= redParticleCount) break;
bool tooClose = false;
foreach (var selected in redParticles)
{
if (Vector3.Distance(particle.transform.position, selected.transform.position) < minDistance)
{
tooClose = true;
break;
}
}
if (!tooClose || redParticles.Count == 0)
{
particle.isRed = true;
particle.originalIsRed = true;
redParticles.Add(particle);
}
}
// 如果不够,强制添加
int remaining = redParticleCount - redParticles.Count;
for (int i = 0; i < remaining && i < shuffled.Count; i++)
{
if (!shuffled[i].isRed)
{
shuffled[i].isRed = true;
shuffled[i].originalIsRed = true;
redParticles.Add(shuffled[i]);
}
}
}
private void Update()
{
// 初始化检查
if (!initializationComplete)
{
initializationFrames++;
if (initializationFrames >= 30)
{
initializationComplete = true;
}
else if (initializationFrames >= 20)
{
// 建立初始连接状态
BuildConnectionMap();
}
}
// 更新连接
UpdateConnections();
// 检测连接变化(触发效果传播)
if (initializationComplete && completionPhase == CompletionPhase.None)
{
DetectConnectionChanges();
}
// 更新效果传播
UpdateEffectPropagations();
// 检测完成状态
if (!isCompleted && completionPhase == CompletionPhase.None)
{
if (CheckRedParticlesConnected())
{
isCompleted = true;
StartCompletionSequence();
}
}
// 更新完成动画
if (isCompleted)
{
UpdateCompletionAnimation();
}
// 更新波形
if (completionPhase == CompletionPhase.Revealing || completionPhase == CompletionPhase.Completed)
{
UpdateWaveform();
}
// 更新输入
UpdateInput();
// 更新渲染器数据
UpdateRenderers();
}
private void UpdateRenderers()
{
// 更新连接线渲染器
if (connectionRenderer != null)
{
connectionRenderer.SetData(
candidateParticles,
effectPropagations,
completionPhase,
fadeOutAlpha
);
}
// 更新波形渲染器
if (waveformRenderer != null)
{
Vector2 mousePos = GetMouseWorldPosition();
bool mouseDown = Input.GetMouseButton(0);
waveformRenderer.SetData(
waveformPoints,
completionPhase,
calmProgress,
mousePos,
mouseDown,
mouseDragSpeed
);
}
}
private void UpdateConnections()
{
// 重置所有连接
foreach (var particle in candidateParticles)
{
particle.connections.Clear();
}
// 检测连接
for (int i = 0; i < candidateParticles.Count; i++)
{
for (int j = i + 1; j < candidateParticles.Count; j++)
{
float distance = Vector3.Distance(
candidateParticles[i].transform.position,
candidateParticles[j].transform.position
);
if (distance < connectionDistance)
{
candidateParticles[i].connections.Add(candidateParticles[j]);
candidateParticles[j].connections.Add(candidateParticles[i]);
}
}
}
}
private void BuildConnectionMap()
{
previousConnections.Clear();
for (int i = 0; i < candidateParticles.Count; i++)
{
for (int j = i + 1; j < candidateParticles.Count; j++)
{
float distance = Vector3.Distance(
candidateParticles[i].transform.position,
candidateParticles[j].transform.position
);
if (distance < connectionDistance)
{
string key = $"{Mathf.Min(i, j)}-{Mathf.Max(i, j)}";
previousConnections[key] = true;
}
}
}
}
private void DetectConnectionChanges()
{
Dictionary<string, bool> currentConnections = new Dictionary<string, bool>();
for (int i = 0; i < candidateParticles.Count; i++)
{
for (int j = i + 1; j < candidateParticles.Count; j++)
{
float distance = Vector3.Distance(
candidateParticles[i].transform.position,
candidateParticles[j].transform.position
);
if (distance < connectionDistance)
{
string key = $"{Mathf.Min(i, j)}-{Mathf.Max(i, j)}";
currentConnections[key] = true;
// 检测新连接
if (!previousConnections.ContainsKey(key))
{
var p1 = candidateParticles[i];
var p2 = candidateParticles[j];
// 检查是否是红色粒子与蓝色粒子的新连接
bool isRedToBlue = (p1.isRed && !p2.isRed && !p2.isOrange && p1.originalIsRed) ||
(!p1.isRed && !p1.isOrange && p2.isRed && p2.originalIsRed);
if (isRedToBlue)
{
var redParticle = p1.isRed ? p1 : p2;
var blueParticle = p1.isRed ? p2 : p1;
if (!blueParticle.isStatic && !blueParticle.hasEffect)
{
TriggerEffectPropagation(redParticle, blueParticle);
}
}
}
}
}
}
previousConnections = currentConnections;
}
private void TriggerEffectPropagation(CandidateParticle redParticle, CandidateParticle blueParticle)
{
effectPropagations.Add(new EffectPropagation(redParticle, 1f));
}
private void UpdateEffectPropagations()
{
for (int i = effectPropagations.Count - 1; i >= 0; i--)
{
effectPropagations[i].Update(Time.deltaTime);
if (!effectPropagations[i].IsActive)
{
effectPropagations[i].Cleanup();
effectPropagations.RemoveAt(i);
}
}
}
private bool CheckRedParticlesConnected()
{
if (redParticles.Count == 0) return false;
if (redParticles.Count == 1) return true;
// 检查所有红色粒子是否连通
HashSet<CandidateParticle> visited = new HashSet<CandidateParticle>();
Queue<CandidateParticle> queue = new Queue<CandidateParticle>();
queue.Enqueue(redParticles[0]);
visited.Add(redParticles[0]);
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var connected in current.connections)
{
if (connected.isRed && !visited.Contains(connected))
{
visited.Add(connected);
queue.Enqueue(connected);
}
}
}
if (visited.Count != redParticles.Count)
return false;
// 检查红色粒子是否与蓝色粒子分离
foreach (var red in redParticles)
{
foreach (var other in candidateParticles)
{
if (other.isRed) continue;
float dist = Vector3.Distance(red.transform.position, other.transform.position);
if (dist < connectionDistance)
return false;
}
}
return true;
}
private void StartCompletionSequence()
{
completionPhase = CompletionPhase.Arranging;
completionTimer = 0f;
fadeOutAlpha = 1f;
// 重置波形
calmProgress = 0f;
waveformPoints.Clear();
// 获取排序的红色粒子
orderedRedParticles = GetOrderedRedParticles();
int displayLength = Mathf.Min(orderedRedParticles.Count, targetSentence.Length);
// 计算目标位置
float spacing = 0.8f;
float totalWidth = displayLength * spacing;
float startX = -totalWidth / 2f + spacing / 2f;
float centerY = 0f;
targetPositions.Clear();
for (int i = 0; i < displayLength; i++)
{
targetPositions.Add(new Vector3(startX + i * spacing, centerY, 0f));
}
if (orderedRedParticles.Count > displayLength)
{
orderedRedParticles = orderedRedParticles.Take(displayLength).ToList();
}
// 清理效果
effectPropagations.Clear();
// 停止非红色粒子
foreach (var p in candidateParticles)
{
if (!p.isRed)
{
p.velocity = Vector2.zero;
if (p.isOrange)
{
p.RemoveEffect();
}
}
}
}
private List<CandidateParticle> GetOrderedRedParticles()
{
if (redParticles.Count == 0) return new List<CandidateParticle>();
if (redParticles.Count == 1) return new List<CandidateParticle>(redParticles);
// 从最左边的粒子开始
var ordered = new List<CandidateParticle>();
var startParticle = redParticles.OrderBy(p => p.transform.position.x).First();
HashSet<CandidateParticle> visited = new HashSet<CandidateParticle>();
Stack<CandidateParticle> stack = new Stack<CandidateParticle>();
stack.Push(startParticle);
visited.Add(startParticle);
while (stack.Count > 0)
{
var current = stack.Pop();
ordered.Add(current);
foreach (var connected in current.connections)
{
if (connected.isRed && !visited.Contains(connected))
{
visited.Add(connected);
stack.Push(connected);
}
}
}
return ordered;
}
private void UpdateCompletionAnimation()
{
completionTimer += Time.deltaTime;
if (completionPhase == CompletionPhase.Arranging)
{
bool allArrived = true;
for (int i = 0; i < orderedRedParticles.Count; i++)
{
var particle = orderedRedParticles[i];
var target = targetPositions[i];
particle.transform.position = Vector3.Lerp(
particle.transform.position,
target,
Time.deltaTime * 5f
);
if (Vector3.Distance(particle.transform.position, target) > 0.01f)
{
allArrived = false;
}
}
fadeOutAlpha = Mathf.Max(0f, fadeOutAlpha - Time.deltaTime * 2f);
if (allArrived && completionTimer > 0.5f)
{
completionPhase = CompletionPhase.Revealing;
completionTimer = 0f;
InitializeWaveform();
// 初始化抖动效果
foreach (var p in orderedRedParticles)
{
p.InitializeAnxietyEffect();
}
}
}
else if (completionPhase == CompletionPhase.Revealing)
{
float revealDelay = 0.25f;
int currentIndex = Mathf.FloorToInt(completionTimer / revealDelay);
for (int i = 0; i < orderedRedParticles.Count; i++)
{
var p = orderedRedParticles[i];
if (i < currentIndex)
{
// 已显示
if (i < targetSentence.Length)
{
p.currentChar = targetSentence[i].ToString();
p.isStatic = false;
}
}
else if (i == currentIndex)
{
// 正在显示(闪烁)
p.changeSpeedMultiplier = 20f;
if (completionTimer % revealDelay >= revealDelay - 0.1f)
{
if (i < targetSentence.Length)
{
p.currentChar = targetSentence[i].ToString();
}
}
}
}
if (currentIndex >= orderedRedParticles.Count)
{
completionPhase = CompletionPhase.Completed;
for (int i = 0; i < orderedRedParticles.Count && i < targetSentence.Length; i++)
{
orderedRedParticles[i].currentChar = targetSentence[i].ToString();
orderedRedParticles[i].isStatic = false;
orderedRedParticles[i].changeSpeedMultiplier = 1f;
}
}
}
else if (completionPhase == CompletionPhase.Completed)
{
// 检查是否所有文字都已消散
bool allDissolved = orderedRedParticles.All(p => p.dissolveProgress >= 1f);
if (allDissolved && completionTimer > 1f)
{
ResetGame();
}
}
}
private void InitializeWaveform()
{
waveformPoints.Clear();
if (orderedRedParticles.Count == 0) return;
float baseY = 0f;
for (int i = 0; i < orderedRedParticles.Count; i++)
{
var particle = orderedRedParticles[i];
var point = new WaveformPoint(
particle.transform.position.x,
baseY,
waveformAmplitude,
i,
particle
);
waveformPoints.Add(point);
particle.waveformPoint = point;
}
lastMousePos = GetMouseWorldPosition();
}
private void UpdateWaveform()
{
if (waveformPoints.Count == 0)
{
InitializeWaveform();
return;
}
Vector2 mousePos = GetMouseWorldPosition();
mouseDragSpeed = Vector2.Distance(mousePos, lastMousePos) / Time.deltaTime;
lastMousePos = mousePos;
bool mousePressed = Input.GetMouseButton(0);
foreach (var point in waveformPoints)
{
point.Update(0f, waveformAmplitude, mousePos, mousePressed, mouseDragSpeed, interactionRadius);
}
// 更新整体平静进度
if (waveformPoints.Count > 0)
{
calmProgress = waveformPoints.Average(p => p.calmAmount);
}
}
private void UpdateInput()
{
// 只在第一阶段(未完成)时处理鼠标输入
if (completionPhase != CompletionPhase.None) return;
Vector2 mouseWorldPos = GetMouseWorldPosition();
// 左键点击:排斥附近粒子
if (Input.GetMouseButton(0))
{
ApplyForceToNearbyParticles(mouseWorldPos, mouseInteractionRadius, repulsionForce);
}
// 右键点击:吸引附近粒子
if (Input.GetMouseButton(1))
{
ApplyForceToNearbyParticles(mouseWorldPos, mouseInteractionRadius, -attractionForce);
}
}
private void ApplyForceToNearbyParticles(Vector2 center, float repulsionRadius, float repulsionForce)
{
foreach (var particle in candidateParticles)
{
Vector2 particlePos = particle.transform.position;
float distance = Vector2.Distance(center, particlePos);
if (distance < repulsionRadius && distance > 0.01f)
{
// 计算方向和力度
Vector2 direction = (particlePos - center).normalized;
float forceMagnitude = repulsionForce * (1f - distance / repulsionRadius);
// 应用力
particle.ApplyForce(direction * forceMagnitude);
}
}
}
private Vector2 GetMouseWorldPosition()
{
Vector3 mousePos = Input.mousePosition;
Camera cam = Camera.main;
if (cam != null)
{
// 对于正交相机,需要使用正确的Z距离
float zDistance = cam.transform.position.z - worldCanvas.transform.position.z;
Vector3 worldPos = cam.ScreenToWorldPoint(new Vector3(mousePos.x, mousePos.y, Mathf.Abs(zDistance)));
return new Vector2(worldPos.x, worldPos.y);
}
return Vector2.zero;
}
private void ResetGame()
{
isCompleted = false;
completionPhase = CompletionPhase.None;
completionTimer = 0f;
fadeOutAlpha = 1f;
orderedRedParticles.Clear();
targetPositions.Clear();
effectPropagations.Clear();
previousConnections.Clear();
initializationComplete = false;
initializationFrames = 0;
calmProgress = 0f;
waveformPoints.Clear();
// 重置所有粒子
foreach (var p in candidateParticles)
{
p.isRed = false;
p.isOrange = false;
p.hasEffect = false;
p.isStatic = false;
p.originalIsRed = false;
p.changeSpeedMultiplier = 1f;
p.vibrationOffset = Vector2.zero;
p.velocity = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(-0.5f, 0.5f));
p.transform.position = GetRandomPositionInBounds();
p.calmProgress = 0f;
p.dissolveProgress = 0f;
p.isCalmed = false;
p.waveformPoint = null;
}
SelectRedParticles();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 79af040068968934c9a7d092267034f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,132 @@
# 语言粒子系统效果总结
## 核心功能
### 1. 粒子系统
- **背景浮动文字**(100个):半透明、缓慢移动、定期随机变换字符
- **可交互候选粒子**50个):
- 蓝色粒子:普通粒子
- 红色粒子:目标粒子(8个,需要连接)
- 橙色粒子:被红色粒子"感染"的蓝色粒子
### 2. 粒子行为
- 边界反弹
- 相互排斥(避免重叠)
- 字符定期随机变化(中文字符集)
- 可选词组模式(相邻粒子组成词组)
### 3. 连接系统
- 距离小于200像素的粒子之间绘制连线
- 蓝色粒子间:细蓝线
- 红色粒子间:粗红线
### 4. 交互效果
- **涟漪效果**(右键):吸引附近粒子
- **排斥爆发**(左键):推开附近粒子
- 粒子受力后产生速度变化
### 5. 效果传播系统
- 当红色粒子与蓝色粒子新建立连接时:
- 蓝色粒子变橙色
- 橙色粒子字符变化加速(3倍速)
- 橙色粒子产生振动效果
- 橙色粒子有光晕和缩放动画
- 效果可以链式传播(橙色粒子连接新的蓝色粒子)
- 最多传播8步
- 传播时绘制连接线、脉冲光点、箭头
### 6. 游戏目标与完成序列
**目标**:让所有红色粒子连接成一个整体,且与蓝色粒子分离
**完成序列**
1. **arranging阶段**
- 红色粒子排列成水平一行(屏幕中央)
- 蓝色/橙色粒子停止移动
- 非红色粒子淡出
2. **revealing阶段**
- 红色粒子逐个显示目标文字:"别过来我感觉害怕"
- 显示前快速闪烁随机字符
- 文字开始剧烈抖动(焦虑效果)
3. **completed阶段**
- 进入波形交互阶段
### 7. 波形交互系统(核心创新)
**初始状态**
- 每个文字下方有一个对应的波形点
- 波形使用Perlin噪声生成(多层噪声叠加)
- 波形呈现异常、混乱的抖动
- 文字随波形剧烈抖动+上下漂浮(表现焦虑情绪)
**交互机制**
- 玩家按住鼠标左键并拖动,抚平附近的波形
- 波形有"抵抗力"机制:
- 异常程度越高,抵抗力越强
- 需要持续按住30-90帧才能完全抚平
- 拖动速度越快,抚平效果越强
- 抚平区域有交互半径(50像素)
- 显示进度环反馈
**抚平效果**
- 波形从异常噪波逐渐变为规律的正弦波
- 波形颜色从红色渐变为蓝色
- 对应文字的抖动和漂浮逐渐减弱
- 波形完全抚平后:
- 文字停止变化和抖动
- 文字开始消散(透明度降低)
- 波形点也随之消散
**视觉反馈**
- 抚平时产生蓝色粒子效果
- 鼠标周围显示交互范围圈
- 波形点显示抚平进度环
- 底部显示整体平静进度条
**情感隐喻**
- 异常波形 = 焦虑、不安的情绪
- 文字抖动 = 情绪的外在表现
- 抚平波形 = 安抚、平复情绪的过程
- 文字消散 = 负面情绪的消解
### 8. 重置机制
- 所有文字消散后,游戏自动重置
- 粒子重新随机分布
- 重新选择8个红色目标粒子
## Unity实现要点
### 技术选型
1. **文字渲染**TextMeshPro(支持中文)
2. **线条绘制**Shapes插件
3. **动画**DOTween
4. **画布**10x10 World Space Canvas
5. **输入**Unity Input System或传统Input
### 核心挑战
1. Perlin噪声波形生成与平滑
2. 粒子间连接检测与效果传播
3. 波形交互的抵抗力机制
4. 文字抖动与消散效果
5. 性能优化(大量粒子+连线)
### 建议架构
```
LanguageParticleManager(主管理器)
├── FloatingTextParticle(背景文字)
├── CandidateParticle(候选粒子)
├── ConnectionRenderer(连线渲染)
├── EffectPropagationSystem(效果传播)
├── WaveformSystem(波形系统)
│ ├── WaveformPoint(波形点)
│ └── WaveformRenderer(波形渲染)
└── InteractionHandler(交互处理)
```
### 性能优化建议
1. 使用对象池管理粒子和效果
2. 连接检测使用空间分区(如Grid
3. 波形点数量与文字数量一致(8个)
4. 粒子效果使用Instancing
5. 线条使用批量绘制
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 82c45afa1fdda6d45af591a380e1f4ec
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,150 @@
# 快速开始指南
## 5分钟快速设置
### 步骤1:创建场景对象
1. 在Hierarchy中右键 → Create Empty
2. 命名为 "LanguageParticleSystem"
3. 添加组件:`LanguageParticleManager`
### 步骤2:检查依赖
确保项目已安装:
- ✅ TextMeshProUnity内置)
- ✅ DOTweenAsset Store
- ✅ ShapesAsset Store
如果缺少DOTween,在Manager中会有命名空间错误。
### 步骤3:配置参数(可选)
在Inspector中调整默认值:
```
Canvas Size: 10
Floating Count: 100
Candidate Count: 50
Red Particle Count: 8
Connection Distance: 2
Target Sentence: "别过来我感觉害怕"
Waveform Amplitude: 0.3
Interaction Radius: 0.5
```
### 步骤4:设置相机
1. 选择Main Camera
2. 设置Position: (0, 0, -10)
3. 设置Size (Orthographic): 5-6
### 步骤5:运行
点击Play,系统会自动:
- 创建10x10的World Space Canvas
- 生成100个背景浮动文字
- 生成50个候选粒子(其中8个红色)
## 游戏操作
### 第一阶段:连接粒子
- **左键点击**:排斥粒子(推开)
- **右键点击**:吸引粒子(涟漪)
- **目标**:让所有红色粒子连成一体,且远离蓝色粒子
### 第二阶段:观看揭示
- 红色粒子自动排列
- 逐个显示文字
- 文字开始剧烈抖动
### 第三阶段:抚平波形
- **按住左键并拖动**:抚平异常波形
- **目标**:让所有波形变平,文字停止抖动并消散
## 常见问题快速解决
### 文字显示为方块?
**原因**:字体不支持中文
**解决**
1. Window → TextMeshPro → Font Asset Creator
2. 选择支持中文的字体(如:思源黑体、微软雅黑)
3. Character Set: Custom Characters
4. 粘贴`TextParticle.cs`中的`chineseChars`字符串
5. Generate Font Atlas
### 看不到粒子?
**原因**:相机位置或Canvas设置问题
**解决**
- 相机Z坐标应为负值(如-10
- Canvas Render Mode应为World Space
- 检查粒子的Z坐标是否为0
### Shapes相关错误?
**原因**:未安装Shapes插件
**解决**
1. 从Asset Store安装Shapes
2. 或者临时注释掉渲染器相关代码:
- ConnectionRenderer.cs
- WaveformRenderer.cs
- LanguageParticleManager.cs中的渲染器初始化
### DOTween错误?
**原因**:未安装DOTween
**解决**
1. 从Asset Store安装DOTween (免费)
2. 或者移除相关using和功能(本系统中DOTween可选)
## 性能调优
### 低配置设备
```
Floating Count: 50
Candidate Count: 30
Red Particle Count: 6
```
### 中配置设备(推荐)
```
Floating Count: 100
Candidate Count: 50
Red Particle Count: 8
```
### 高配置设备
```
Floating Count: 200
Candidate Count: 80
Red Particle Count: 10
```
## 视觉调优
### 增强视觉效果
- 增加`Waveform Amplitude`0.5-0.8
- 增加`Interaction Radius`0.8-1.0
- 调整渲染器颜色(在Renderer组件中)
### 减少视觉混乱
- 减少`Floating Count`
- 减少`Connection Distance`
- 降低背景文字透明度(修改FloatingTextParticle.cs
## 下一步
- 阅读`README.md`了解完整功能
- 查看`LanguageParticleSystem_Summary.md`了解技术细节
- 自定义字符集和目标句子
- 添加音效和粒子特效
## 技术支持
遇到问题?检查:
1. Console是否有错误
2. 所有依赖是否已安装
3. 相机设置是否正确
4. 粒子是否在相机视野内
调试技巧:
- 在Scene视图中观察粒子位置
- 使用Gizmos显示连接关系
- 在Inspector中监控粒子状态
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7db05522201123d409abb24ec2f7f845
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,212 @@
# 语言粒子系统 - Unity实现
## 概述
这是一个基于P5.js原型的语言粒子互动系统,在Unity中复现。系统通过文字粒子的连接和交互,表现情绪的传播与平复过程。
## 文件结构
```
Language/
├── LanguageParticleSystem_Summary.md # 效果总结文档
├── README.md # 使用说明(本文件)
├── TextParticle.cs # 文字粒子基类
├── FloatingTextParticle.cs # 背景浮动文字粒子
├── CandidateParticle.cs # 可交互候选粒子
├── WaveformPoint.cs # 波形点数据
├── EffectPropagation.cs # 效果传播系统
├── ConnectionRenderer.cs # 连接线渲染器
├── WaveformRenderer.cs # 波形渲染器
└── LanguageParticleManager.cs # 主管理器
```
## 使用步骤
### 1. 场景设置
1. 创建一个空GameObject,命名为"LanguageParticleSystem"
2. 添加`LanguageParticleManager`组件
### 2. Canvas设置
系统会自动创建一个10x10的World Space Canvas,也可以手动配置:
1. 创建CanvasRender Mode: World Space
2. 设置Canvas尺寸为10x10
3. 将Canvas拖到Manager的`worldCanvas`字段
### 3. 参数配置
在Inspector中调整以下参数:
#### Canvas设置
- **Canvas Size**: Canvas大小(默认10
- **Text Margin**: 文字区域边距(默认1
#### 粒子数量
- **Floating Count**: 背景浮动文字数量(默认100)
- **Candidate Count**: 候选粒子数量(默认50
- **Red Particle Count**: 红色目标粒子数量(默认8)
#### 游戏参数
- **Connection Distance**: 连接距离阈值(默认2Unity单位)
- **Target Sentence**: 目标句子(默认"别过来我感觉害怕")
#### 波形参数
- **Waveform Amplitude**: 波形振幅(默认0.3
- **Interaction Radius**: 交互半径(默认0.5
### 4. 依赖项
确保项目中已安装以下插件:
- **TextMeshPro**: Unity内置,用于中文文字渲染
- **DOTween**: 用于动画效果
- **Shapes**: 用于绘制连接线和波形
### 5. 运行
点击Play即可运行。系统会自动:
1. 生成背景浮动文字
2. 生成候选粒子
3. 随机选择8个红色目标粒子
## 游戏玩法
### 第一阶段:连接红色粒子
**目标**:让所有红色粒子连接成一个整体,且与蓝色粒子分离
**交互**
- 左键点击:产生排斥波,推开附近粒子
- 右键点击:产生涟漪,吸引附近粒子
**机制**
- 红色粒子与蓝色粒子接触时,效果会传播(蓝色→橙色)
- 效果可以链式传播(最多8步)
- 橙色粒子字符变化加速,带有振动和光晕效果
### 第二阶段:文字揭示
完成第一阶段后:
1. 红色粒子自动排列成水平一行
2. 蓝色粒子淡出
3. 红色粒子逐个揭示目标文字:"别过来我感觉害怕"
4. 文字出现剧烈抖动(焦虑效果)
### 第三阶段:波形交互
文字显示完成后:
1. 文字下方出现异常波形(基于Perlin噪声)
2. 文字随波形剧烈抖动和漂浮
3. **目标**:通过鼠标抚平波形,平复文字的焦虑
**交互**
- 按住鼠标左键并拖动,抚平附近的波形
- 波形有"抵抗力",需要持续按住(0.5-1.5秒)
- 拖动速度越快,抚平效果越强
**效果**
- 波形从红色异常噪波→蓝色规律正弦波
- 文字抖动和漂浮逐渐减弱
- 波形完全稳定后,文字开始消散
- 所有文字消散后,游戏自动重置
## 技术要点
### 1. 粒子系统
- 使用TextMeshPro渲染中文字符
- 粒子具有速度、边界检测、相互排斥
- 字符定期随机变化
### 2. 连接检测
- 使用空间距离判断粒子间连接
- 连接关系存储在`connections`列表中
- 用于效果传播和完成条件判断
### 3. 效果传播
- 红色粒子与蓝色粒子建立新连接时触发
- 使用队列实现链式传播
- 脉冲动画沿连接线移动
### 4. 波形系统
- 使用Perlin噪声生成异常波形(多层噪声叠加)
- 每个波形点关联一个文字粒子
- 波形点有独立的抚平进度和抵抗力
- 抚平后波形变为规律正弦波
### 5. 渲染系统
- 使用Shapes的ImmediateModeShapeDrawer
- 连接线:实时绘制粒子间连线
- 波形:使用Polyline绘制平滑曲线
- 效果:脉冲光点、箭头、进度环
## 性能优化建议
1. **对象池**:粒子频繁创建销毁时使用对象池
2. **空间分区**:粒子数量超过100时考虑Grid分区
3. **LOD**:远距离粒子降低更新频率
4. **批量绘制**Shapes已支持批量绘制
5. **异步处理**:连接检测可使用Job System
## 自定义扩展
### 添加新的文字内容
修改`CandidateParticle.cs`中的字符集:
```csharp
protected static string chineseChars = "你的字符集...";
protected static string[] chineseWords = { "词组1", "词组2", ... };
```
### 修改目标句子
在Manager的Inspector中修改`Target Sentence`字段
### 调整波形特性
修改`WaveformPoint.cs`中的噪声参数:
```csharp
noiseScale1 = 0.05f; // 低频噪声
noiseScale2 = 0.15f; // 中频噪声
noiseScale3 = 0.4f; // 高频噪声
```
### 添加音效
在以下关键事件添加音效:
- `TriggerEffectPropagation()`: 效果传播时
- `StartCompletionSequence()`: 完成第一阶段时
- `WaveformPoint.Update()`: 波形抚平时(`calmAmount >= 0.95f`
- `ResetGame()`: 游戏重置时
## 调试技巧
1. **显示连接关系**:在Scene视图中观察连接线
2. **波形可视化**:查看波形点的`calmAmount`
3. **粒子状态**:在Inspector中实时查看粒子的`isRed``isOrange`等状态
4. **帧率监控**:粒子数量过多时注意帧率
## 常见问题
### Q: 文字显示为方块?
A: 确保使用支持中文的字体,并在TextMeshPro Font Asset中生成中文字符
### Q: 连接线不显示?
A: 检查Shapes插件是否正确安装,Camera是否正确配置
### Q: 波形交互没反应?
A: 检查鼠标世界坐标转换是否正确,确保Camera能看到粒子
### Q: 性能问题?
A: 减少粒子数量,或启用对象池和空间分区优化
## 许可
本项目基于P5.js原型复现,供学习和研究使用。
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 1500caf0ff27d574cbbc46a5b6954555
guid: 816db8f21057c5c4892f64913a92dccf
TextScriptImporter:
externalObjects: {}
userData:
@@ -0,0 +1,238 @@
using UnityEngine;
using TMPro;
using DG.Tweening;
using System.Collections.Generic;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 文字粒子基类
/// </summary>
public class TextParticle : MonoBehaviour
{
[Header("组件")]
public TextMeshPro textMesh;
public SpriteRenderer glowSprite;
[Header("属性")]
public Vector2 velocity;
public float alpha;
public float baseAlpha = 1f;
public float size = 3f;
public float probability = 1f;
public string currentChar;
[Header("状态")]
public bool isStatic = false;
public bool isCalmed = false;
protected float changeTimer;
protected float changeInterval = 1f;
protected Bounds movementBounds;
protected TMP_FontAsset customFont;
// 字符集
protected static string chineseChars = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞";
protected static string[] chineseWords = { "不安", "紧张", "焦虑", "担忧", "烦躁", "恐慌", "恐惧", "绝望", "痛苦", "悲伤", "愤怒", "孤独", "无助", "迷茫", "困惑", "压抑", "沉重", "疲惫", "空虚", "失落" };
protected virtual void Awake()
{
if (textMesh == null)
textMesh = GetComponentInChildren<TextMeshPro>();
if (textMesh == null)
{
var textObj = new GameObject("Text");
textObj.transform.SetParent(transform, false);
// 添加 RectTransform(在 Canvas 下必需)
RectTransform rectTransform = textObj.AddComponent<RectTransform>();
rectTransform.localPosition = Vector3.zero;
rectTransform.localScale = Vector3.one;
rectTransform.sizeDelta = new Vector2(10, 10); // 设置足够大的尺寸
rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
// 添加 TextMeshPro 组件
textMesh = textObj.AddComponent<TextMeshPro>();
textMesh.alignment = TextAlignmentOptions.Center;
textMesh.fontSize = size;
textMesh.enableAutoSizing = false;
textMesh.overflowMode = TextOverflowModes.Overflow; // 允许溢出,不裁剪
// 设置字体(优先使用自定义字体)
SetupFont();
// 设置渲染层级,确保文字显示在正确的层
textMesh.sortingOrder = 10;
}
else
{
// 如果已存在 textMesh,也要确保设置字体
SetupFont();
}
// 创建光晕
if (glowSprite == null)
{
var glowObj = new GameObject("Glow");
glowObj.transform.SetParent(transform);
glowObj.transform.localPosition = Vector3.zero;
glowObj.transform.localScale = Vector3.one * 0.5f;
glowSprite = glowObj.AddComponent<SpriteRenderer>();
glowSprite.sprite = CreateCircleSprite();
glowSprite.color = new Color(1, 1, 1, 0.2f);
}
changeTimer = Random.Range(0.5f, 1.5f);
currentChar = GetRandomChar();
UpdateText();
}
protected virtual void Update()
{
if (!isStatic && !isCalmed)
{
// 更新位置
transform.position += (Vector3)velocity * Time.deltaTime;
// 边界检测
CheckBounds();
// 字符变化
changeTimer -= Time.deltaTime;
if (changeTimer <= 0)
{
currentChar = GetRandomChar();
UpdateText();
changeTimer = changeInterval;
}
// 速度衰减
velocity *= 0.95f;
}
// 更新视觉效果
UpdateVisuals();
}
public void SetMovementBounds(Bounds bounds)
{
movementBounds = bounds;
}
protected void CheckBounds()
{
Vector3 pos = transform.position;
bool bounced = false;
if (pos.x < movementBounds.min.x || pos.x > movementBounds.max.x)
{
velocity.x *= -1;
pos.x = Mathf.Clamp(pos.x, movementBounds.min.x, movementBounds.max.x);
bounced = true;
}
if (pos.y < movementBounds.min.y || pos.y > movementBounds.max.y)
{
velocity.y *= -1;
pos.y = Mathf.Clamp(pos.y, movementBounds.min.y, movementBounds.max.y);
bounced = true;
}
if (bounced)
transform.position = pos;
}
protected string GetRandomChar()
{
return chineseChars[Random.Range(0, chineseChars.Length)].ToString();
}
protected string GetRandomWord()
{
return chineseWords[Random.Range(0, chineseWords.Length)];
}
protected void UpdateText()
{
if (textMesh != null)
textMesh.text = currentChar;
}
protected virtual void UpdateVisuals()
{
if (textMesh != null)
{
Color color = textMesh.color;
color.a = alpha; // alpha 已经是 0-1 范围
textMesh.color = color;
}
}
public void ApplyForce(Vector2 force)
{
velocity += force;
}
/// <summary>
/// 设置字体资源
/// </summary>
public void SetFont(TMP_FontAsset fontAsset)
{
customFont = fontAsset;
SetupFont();
}
/// <summary>
/// 内部方法:设置字体
/// </summary>
private void SetupFont()
{
if (textMesh == null) return;
// 优先级:自定义字体 > 默认字体 > 警告
if (customFont != null)
{
textMesh.font = customFont;
}
else if (TMP_Settings.defaultFontAsset != null)
{
textMesh.font = TMP_Settings.defaultFontAsset;
}
else
{
Debug.LogWarning("[TextParticle] TextMeshPro 字体未设置!请在 LanguageParticleManager 中指定中文字体资源,或在 Project Settings > TextMesh Pro > Settings 中设置默认字体。");
}
}
// 创建简单的圆形Sprite用于光晕
private Sprite CreateCircleSprite()
{
int resolution = 32;
Texture2D texture = new Texture2D(resolution, resolution);
Color[] pixels = new Color[resolution * resolution];
Vector2 center = new Vector2(resolution / 2f, resolution / 2f);
float radius = resolution / 2f;
for (int y = 0; y < resolution; y++)
{
for (int x = 0; x < resolution; x++)
{
float dist = Vector2.Distance(new Vector2(x, y), center);
float alpha = 1f - Mathf.Clamp01(dist / radius);
alpha = Mathf.Pow(alpha, 2); // 平滑衰减
pixels[y * resolution + x] = new Color(1, 1, 1, alpha);
}
}
texture.SetPixels(pixels);
texture.Apply();
return Sprite.Create(texture, new Rect(0, 0, resolution, resolution), new Vector2(0.5f, 0.5f));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6da7d4539a3ba0044bbdfda3959c396b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,210 @@
using UnityEngine;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 波形点:管理单个波形点的状态和动画
/// </summary>
public class WaveformPoint
{
// 位置
public float x; // X坐标(跟随文字)
public float y; // 当前Y坐标
public float targetY; // 目标Y坐标(规律波形)
public float smoothedY; // 平滑后的Y坐标
// 噪声参数
public float noiseOffset; // 噪声偏移
public float noiseScale1; // 噪声缩放1(低频)
public float noiseScale2; // 噪声缩放2(中频)
public float noiseScale3; // 噪声缩放3(高频)
public float noiseAmplitude; // 噪声振幅
public float noiseTime; // 噪声时间偏移
// 抚平状态
public float resistance; // 抵抗力(0-1,越高越难抚平)
public float calmAmount; // 当前被抚平的程度(0-1
public bool isBeingCalmed; // 是否正在被抚平
public float calmTimer; // 抚平计时器
// 关联
public CandidateParticle particle; // 关联的文字粒子
public int particleIndex; // 粒子索引
// 脉冲动画
public float pulseProgress; // 脉冲进度(0-1
public WaveformPoint(float x, float baseY, float amplitude, int index, CandidateParticle particle)
{
this.x = x;
this.particle = particle;
this.particleIndex = index;
// 初始化噪声参数
noiseScale1 = 0.05f;
noiseScale2 = 0.15f;
noiseScale3 = 0.4f;
noiseAmplitude = Random.Range(0.7f, 1f);
noiseOffset = Random.Range(-0.2f, 0.2f);
noiseTime = Random.Range(0f, 1000f);
// 计算初始Y(噪波)
float combinedNoise = GenerateNoise(Time.time);
y = baseY + combinedNoise * amplitude;
smoothedY = y;
// 计算目标Y(规律波形)
float targetWavePhase = index * 0.3f;
float targetWaveAmplitude = amplitude * 0.3f;
targetY = baseY + Mathf.Sin(targetWavePhase) * targetWaveAmplitude;
// 计算异常程度和抵抗力
float anomalyAmount = Mathf.Abs(y - targetY) / amplitude;
resistance = 0.1f + anomalyAmount * 0.25f;
calmAmount = 0f;
isBeingCalmed = false;
calmTimer = 0f;
pulseProgress = 0f;
}
/// <summary>
/// 生成多层Perlin噪声
/// </summary>
private float GenerateNoise(float time)
{
// 使用多层噪声叠加
float noiseValue1 = Mathf.PerlinNoise(x * noiseScale1, time * 0.01f);
float noiseValue2 = Mathf.PerlinNoise(x * noiseScale2, time * 0.02f + 100f);
float noiseValue3 = Mathf.PerlinNoise(x * noiseScale3, time * 0.03f + 200f);
// 映射到[-1, 1]范围
noiseValue1 = (noiseValue1 - 0.5f) * 2f;
noiseValue2 = (noiseValue2 - 0.5f) * 2f * 0.6f;
noiseValue3 = (noiseValue3 - 0.5f) * 2f * 0.3f;
// 叠加
float combinedNoise = noiseValue1 + noiseValue2 + noiseValue3 + noiseOffset;
return Mathf.Clamp(combinedNoise * noiseAmplitude, -1.5f, 1.5f);
}
/// <summary>
/// 更新波形点(动态噪波、交互、抚平)
/// </summary>
public void Update(float baseY, float amplitude, Vector2 mousePos, bool mousePressed, float mouseDragSpeed, float interactionRadius)
{
// 同步X位置(跟随文字)
if (particle != null)
{
x = particle.transform.position.x;
// 更新目标位置
float targetWavePhase = particleIndex * 0.3f;
float targetWaveAmplitude = amplitude * 0.3f;
targetY = baseY + Mathf.Sin(targetWavePhase) * targetWaveAmplitude;
}
// 重置状态
isBeingCalmed = false;
// 如果未抚平,添加动态噪波
if (calmAmount < 0.9f)
{
float currentTime = Time.time + noiseTime * 0.001f;
float dynamicNoise = GenerateNoise(currentTime);
// 根据抚平程度减少噪波强度
float noiseIntensity = (1 - calmAmount) * amplitude;
smoothedY = y + dynamicNoise * noiseIntensity;
}
// 处理鼠标交互
if (mousePressed)
{
float distToMouse = Vector2.Distance(mousePos, new Vector2(x, smoothedY));
if (distToMouse < interactionRadius)
{
// 计算影响强度
float distanceInfluence = Mathf.Lerp(1f, 0f, distToMouse / interactionRadius);
float speedBonus = Mathf.Min(1f, mouseDragSpeed / 0.25f); // 拖动速度加成
float influence = distanceInfluence * (0.5f + speedBonus * 0.5f);
// 标记为正在被抚平
isBeingCalmed = true;
calmTimer += Time.deltaTime;
// 需要持续按住才能抚平(抵抗机制)
float requiredTime = 0.5f + resistance * 1f; // 需要0.5-1.5秒
float calmProgressLocal = Mathf.Min(1f, calmTimer / requiredTime);
// 应用抚平效果(考虑抵抗力)
float calmStrength = influence * (1 - resistance) * 0.15f;
// 计算目标位置(从噪波位置向规律波形位置过渡)
float targetYLerp = Mathf.Lerp(y, targetY, calmProgressLocal);
smoothedY = Mathf.Lerp(smoothedY, targetYLerp, calmStrength);
// 更新抚平程度(一旦抚平,就不会恢复)
if (calmProgressLocal > calmAmount)
{
calmAmount = calmProgressLocal;
}
// 如果完全抚平
if (calmAmount >= 0.95f)
{
calmAmount = 1f;
smoothedY = targetY;
}
}
}
// 如果calmAmount接近1,强制向目标位置靠拢
if (calmAmount > 0.8f)
{
float forceStrength = Mathf.Lerp(0.1f, 0.5f, (calmAmount - 0.8f) / 0.2f);
smoothedY = Mathf.Lerp(smoothedY, targetY, forceStrength);
}
// 如果完全抚平,直接设置为目标位置
if (calmAmount >= 1f)
{
smoothedY = targetY;
}
// 限制波形点不会完全超出范围
float maxDeviation = amplitude * (1 - calmAmount * 0.8f);
smoothedY = Mathf.Clamp(smoothedY, baseY - maxDeviation, baseY + maxDeviation);
// 同步更新对应文字粒子的平静进度
if (particle != null)
{
particle.calmProgress = calmAmount;
// 如果完全抚平,标记为已稳定
if (calmAmount >= 0.95f && !particle.isCalmed)
{
particle.isCalmed = true;
particle.isStatic = true;
particle.changeSpeedMultiplier = 0;
particle.dissolveProgress = 0;
}
// 如果已抚平,检查波形是否完全稳定
if (particle.isCalmed)
{
float distanceToTarget = Mathf.Abs(smoothedY - targetY);
float stabilityThreshold = amplitude * 0.05f;
if (distanceToTarget < stabilityThreshold)
{
// 波形已稳定,开始消散
float dissolveSpeed = 0.02f + (calmAmount - 0.95f) * 0.15f;
particle.dissolveProgress = Mathf.Min(1f, particle.dissolveProgress + dissolveSpeed * Time.deltaTime * 50f);
}
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d995549b26518a4419234f98d89ec699
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,266 @@
using UnityEngine;
using System.Collections.Generic;
using Shapes;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// 波形渲染器:使用Shapes绘制波形和交互效果
/// </summary>
[ExecuteAlways]
public class WaveformRenderer : ImmediateModeShapeDrawer
{
[Header("波形设置")]
public Color normalWaveColor = new Color(0.59f, 0.78f, 1f, 0.8f);
public Color anxiousWaveColor = new Color(1f, 0.39f, 0.39f, 0.8f);
public float waveLineThickness = 0.04f;
public float baseY = 0f;
[Header("交互设置")]
public Color interactionColor = new Color(0.59f, 0.78f, 1f, 0.3f);
public Color interactionRingColor = new Color(0.59f, 0.78f, 1f, 0.6f);
public float interactionRadius = 0.5f;
[Header("粒子效果")]
public Color particleColor = new Color(0.59f, 0.78f, 1f, 0.8f);
private List<WaveformPoint> waveformPoints;
private CompletionPhase completionPhase;
private float calmProgress;
private Vector2 mouseWorldPos;
private bool mousePressed;
private float mouseDragSpeed;
private List<WaveformParticleEffect> waveParticles = new List<WaveformParticleEffect>();
public void SetData(
List<WaveformPoint> points,
CompletionPhase phase,
float calm,
Vector2 mousePos,
bool mouseDown,
float dragSpeed)
{
waveformPoints = points;
completionPhase = phase;
calmProgress = calm;
mouseWorldPos = mousePos;
mousePressed = mouseDown;
mouseDragSpeed = dragSpeed;
}
private void Update()
{
// 更新粒子效果
for (int i = waveParticles.Count - 1; i >= 0; i--)
{
waveParticles[i].Update(Time.deltaTime);
if (waveParticles[i].alpha <= 0 || waveParticles[i].size < 0.01f)
{
waveParticles.RemoveAt(i);
}
}
// 生成新粒子(如果正在交互)
if (mousePressed && waveformPoints != null && Random.value > 0.7f)
{
foreach (var point in waveformPoints)
{
if (point.isBeingCalmed && Random.value > 0.5f)
{
waveParticles.Add(new WaveformParticleEffect(
new Vector3(point.x, point.smoothedY, 0),
Random.Range(-0.1f, 0.1f),
Random.Range(-0.2f, -0.05f),
Random.Range(0.02f, 0.04f)
));
}
}
}
}
public override void DrawShapes(Camera cam)
{
if (waveformPoints == null || waveformPoints.Count == 0)
return;
if (completionPhase != CompletionPhase.Revealing && completionPhase != CompletionPhase.Completed)
return;
using (Draw.Command(cam))
{
Draw.BlendMode = ShapesBlendMode.Transparent;
Draw.ZTest = UnityEngine.Rendering.CompareFunction.Always;
// 绘制粒子效果
DrawWaveformParticles();
// 绘制波形线
DrawWaveformLine();
// 绘制波形点
DrawWaveformPoints();
// 绘制交互反馈
if (mousePressed)
{
DrawInteractionFeedback();
}
// 绘制基准线
if (calmProgress < 0.9f)
{
DrawBaseline();
}
}
}
private void DrawWaveformParticles()
{
foreach (var particle in waveParticles)
{
Color color = particleColor;
color.a = particle.alpha;
Draw.Disc(particle.position, particle.size, color);
// 光晕
color.a = particle.alpha * 0.3f;
Draw.Disc(particle.position, particle.size * 2f, color);
}
}
private void DrawWaveformLine()
{
Draw.LineGeometry = LineGeometry.Flat2D;
Draw.Thickness = waveLineThickness;
// 使用Polyline绘制波形
var points = new List<Vector3>();
var colors = new List<Color>();
foreach (var point in waveformPoints)
{
// 跳过完全消散的点
float dissolveProgress = point.particle != null ? point.particle.dissolveProgress : 0f;
if (dissolveProgress >= 1f) continue;
points.Add(new Vector3(point.x, point.smoothedY, 0));
// 根据抚平程度混合颜色
Color waveColor = Color.Lerp(anxiousWaveColor, normalWaveColor, point.calmAmount);
waveColor.a *= (1 - dissolveProgress);
colors.Add(waveColor);
}
if (points.Count >= 2)
{
for (int i = 0; i < points.Count - 1; i++)
{
Draw.Line(points[i], points[i + 1], colors[i], colors[i + 1]);
}
}
}
private void DrawWaveformPoints()
{
for (int i = 0; i < waveformPoints.Count; i += 3)
{
var point = waveformPoints[i];
// 跳过完全消散的点
float dissolveProgress = point.particle != null ? point.particle.dissolveProgress : 0f;
if (dissolveProgress >= 1f) continue;
Vector3 pos = new Vector3(point.x, point.smoothedY, 0);
// 如果正在被抚平,显示进度环
if (point.isBeingCalmed && point.calmTimer > 0)
{
float requiredTime = 0.5f + point.resistance * 1f;
float progress = Mathf.Min(1f, point.calmTimer / requiredTime);
Color ringColor = interactionRingColor;
ringColor.a *= (1 - dissolveProgress);
Draw.Ring(pos, 0.08f, 0.06f, ringColor);
Draw.Arc(pos, 0.08f, 0.02f, -Mathf.PI / 2f, progress * Mathf.PI * 2f, ringColor);
}
// 绘制波形点
float pointAlpha = (1 - point.calmAmount) * (1 - calmProgress * 0.5f) * (1 - dissolveProgress) * 0.6f;
if (pointAlpha > 0.05f)
{
Color pointColor = Color.Lerp(anxiousWaveColor, normalWaveColor, point.calmAmount);
pointColor.a = pointAlpha;
Draw.Disc(pos, 0.03f, pointColor);
}
}
}
private void DrawInteractionFeedback()
{
Vector3 mousePos3D = new Vector3(mouseWorldPos.x, mouseWorldPos.y, 0);
// 内圈(交互范围)
Color innerColor = interactionColor;
Draw.Disc(mousePos3D, interactionRadius, innerColor);
// 外圈(视觉反馈)
Color outerColor = interactionRingColor;
Draw.Ring(mousePos3D, interactionRadius, interactionRadius * 0.95f, outerColor);
// 根据拖动速度显示额外反馈
if (mouseDragSpeed > 0.1f)
{
Color speedColor = interactionRingColor;
speedColor.a = 0.4f;
Draw.Ring(mousePos3D, interactionRadius * 1.25f, interactionRadius * 1.2f, speedColor);
}
}
private void DrawBaseline()
{
float alpha = 0.2f * (1 - calmProgress);
Color baselineColor = normalWaveColor;
baselineColor.a = alpha;
Draw.LineGeometry = LineGeometry.Flat2D;
Draw.Thickness = 0.015f;
// 计算波形范围
if (waveformPoints.Count > 0)
{
float minX = waveformPoints[0].x;
float maxX = waveformPoints[waveformPoints.Count - 1].x;
Draw.Line(new Vector3(minX - 0.5f, baseY, 0), new Vector3(maxX + 0.5f, baseY, 0), baselineColor);
}
}
private class WaveformParticleEffect
{
public Vector3 position;
public float vx;
public float vy;
public float alpha;
public float size;
public WaveformParticleEffect(Vector3 pos, float velocityX, float velocityY, float initialSize)
{
position = pos;
vx = velocityX;
vy = velocityY;
alpha = 0.8f;
size = initialSize;
}
public void Update(float deltaTime)
{
position.x += vx * deltaTime;
position.y += vy * deltaTime;
vy += 0.1f * deltaTime; // 重力
alpha -= 0.015f;
size *= 0.98f;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc80f1f8eacb59c4ea4cf18ff3a109c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,195 +0,0 @@
using UnityEngine;
using TMPro;
using System.Collections.Generic;
using System.Linq;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 背景文字层 - 生成和管理大量随机变化的文字,文字间寻找关联形成词语连线
/// </summary>
public class BackgroundTextLayer
{
private List<TextNode> _allNodes;
private List<string> _chineseChars;
private float _fieldWidth;
private float _fieldHeight;
private int _nodeCount;
private Transform _parentTransform;
private GameObject _textPrefab;
// 文字变化参数
private float _changeInterval = 0.5f;
private float _lastChangeTime;
// 连线参数
private float _connectionRadius = 2f;
private float _connectionProbability = 0.3f;
public BackgroundTextLayer(float fieldWidth, float fieldHeight, int nodeCount, Transform parentTransform)
{
_fieldWidth = fieldWidth;
_fieldHeight = fieldHeight;
_nodeCount = nodeCount;
_parentTransform = parentTransform;
_allNodes = new List<TextNode>();
// 初始化中文字符集
InitializeChineseChars();
// 创建文字预制体
CreateTextPrefab();
// 生成初始节点
GenerateNodes();
}
private void InitializeChineseChars()
{
_chineseChars = new List<string>();
// 常用中文字符(可以扩展)
for (int i = 0x4e00; i <= 0x9fff; i++)
{
_chineseChars.Add(char.ConvertFromUtf32(i));
}
// 添加一些常用字
_chineseChars.AddRange(new[] { "的", "是", "在", "了", "和", "有", "就", "不", "人", "都", "一", "一个", "上", "也", "很", "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这" });
}
private void CreateTextPrefab()
{
_textPrefab = new GameObject("TextNodePrefab");
_textPrefab.hideFlags = HideFlags.HideAndDontSave;
var textMesh = _textPrefab.AddComponent<TextMeshPro>();
textMesh.fontSize = 24;
textMesh.alignment = TextAlignmentOptions.Center;
textMesh.color = Color.white;
textMesh.enableWordWrapping = false;
textMesh.overflowMode = TextOverflowModes.Overflow;
_textPrefab.SetActive(false);
}
private void GenerateNodes()
{
for (int i = 0; i < _nodeCount; i++)
{
Vector2 randomPos = new Vector2(
Random.Range(-_fieldWidth * 0.5f, _fieldWidth * 0.5f),
Random.Range(-_fieldHeight * 0.5f, _fieldHeight * 0.5f)
);
string randomChar = _chineseChars[Random.Range(0, _chineseChars.Count)];
TextNode node = new TextNode(randomPos, randomChar);
// 创建GameObject
GameObject nodeObj = Object.Instantiate(_textPrefab, _parentTransform);
nodeObj.SetActive(true);
nodeObj.name = $"TextNode_{i}";
node.GameObject = nodeObj;
node.TextComponent = nodeObj.GetComponent<TextMeshPro>();
node.UpdateVisual();
_allNodes.Add(node);
}
// 建立初始连接关系
UpdateConnections();
}
public void Update(float deltaTime)
{
_lastChangeTime += deltaTime;
if (_lastChangeTime >= _changeInterval)
{
_lastChangeTime = 0f;
// 随机改变一些文字(排除已选定的和候选的)
foreach (var node in _allNodes)
{
if (node.State == TextNodeState.Random)
{
// 随机改变文字
if (Random.value < 0.1f) // 10%概率改变
{
node.Text = _chineseChars[Random.Range(0, _chineseChars.Count)];
node.UpdateVisual();
}
// 随机改变亮度
node.Brightness = Random.Range(0.3f, 1f);
node.UpdateVisual();
}
}
// 更新连接关系
UpdateConnections();
}
}
private void UpdateConnections()
{
// 清除所有连接
foreach (var node in _allNodes)
{
node.ConnectedNodes.Clear();
}
// 重新建立连接(基于距离和概率)
for (int i = 0; i < _allNodes.Count; i++)
{
for (int j = i + 1; j < _allNodes.Count; j++)
{
var nodeA = _allNodes[i];
var nodeB = _allNodes[j];
float distance = Vector2.Distance(nodeA.Position, nodeB.Position);
if (distance < _connectionRadius && Random.value < _connectionProbability)
{
nodeA.AddConnection(nodeB);
nodeB.AddConnection(nodeA);
}
}
}
}
public List<TextNode> GetAllNodes()
{
return _allNodes;
}
public List<TextNode> GetNodesInRange(Vector2 center, float radius)
{
return _allNodes.Where(node =>
Vector2.Distance(node.Position, center) <= radius
).ToList();
}
public void SetNodeState(TextNode node, TextNodeState state)
{
node.State = state;
node.UpdateVisual();
}
public void Cleanup()
{
foreach (var node in _allNodes)
{
if (node.GameObject != null)
{
Object.Destroy(node.GameObject);
}
}
_allNodes.Clear();
if (_textPrefab != null)
{
Object.Destroy(_textPrefab);
}
}
}
}
@@ -1,155 +0,0 @@
using UnityEngine;
using Shapes;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 圆形可视区域 - 使用Shapes绘制圆形遮罩,支持鼠标拖拽移动
/// </summary>
public class CircularViewport : ImmediateModeShapeDrawer
{
[Header("圆形区域设置")]
public float viewportRadius = 5f;
public Color viewportBorderColor = Color.white;
public float borderWidth = 0.1f;
[Header("拖拽设置")]
public bool enableDrag = true;
public float dragSpeed = 1f;
[Header("Gizmo调试设置")]
public bool showGizmos = true;
public bool showViewportCircle = true;
public bool showCenterPoint = true;
private Vector2 _viewportCenter;
private bool _isDragging = false;
private Vector2 _lastMousePosition;
private Camera _mainCamera;
public Vector2 ViewportCenter => _viewportCenter;
public float ViewportRadius => viewportRadius;
private void Start()
{
_viewportCenter = Vector2.zero;
_mainCamera = Camera.main;
if (_mainCamera == null)
{
_mainCamera = FindObjectOfType<Camera>();
}
}
private void Update()
{
if (!enableDrag) return;
HandleMouseInput();
}
private void HandleMouseInput()
{
if (Input.GetMouseButtonDown(0))
{
Vector2 mouseWorldPos = GetMouseWorldPosition();
float distance = Vector2.Distance(mouseWorldPos, _viewportCenter);
// 如果点击在圆形区域内,开始拖拽
if (distance <= viewportRadius)
{
_isDragging = true;
_lastMousePosition = mouseWorldPos;
}
}
if (Input.GetMouseButton(0) && _isDragging)
{
Vector2 currentMousePos = GetMouseWorldPosition();
Vector2 delta = currentMousePos - _lastMousePosition;
_viewportCenter += delta * dragSpeed;
_lastMousePosition = currentMousePos;
}
if (Input.GetMouseButtonUp(0))
{
_isDragging = false;
}
}
private Vector2 GetMouseWorldPosition()
{
if (_mainCamera == null) return Vector2.zero;
Vector3 mousePos = Input.mousePosition;
mousePos.z = _mainCamera.nearClipPlane + 1f;
Vector3 worldPos = _mainCamera.ScreenToWorldPoint(mousePos);
return new Vector2(worldPos.x, worldPos.y);
}
public override void DrawShapes(Camera cam)
{
using (Draw.Command(cam))
{
Draw.ResetAllDrawStates();
Draw.LineGeometry = LineGeometry.Volumetric3D;
// 绘制圆形边框
Draw.Ring(new Vector3(_viewportCenter.x, _viewportCenter.y, 0),
viewportRadius, borderWidth, viewportBorderColor);
}
}
public bool IsPositionInViewport(Vector2 position)
{
return Vector2.Distance(position, _viewportCenter) <= viewportRadius;
}
public void SetViewportCenter(Vector2 center)
{
_viewportCenter = center;
}
private void OnDrawGizmos()
{
if (!showGizmos) return;
Vector3 center = new Vector3(_viewportCenter.x, _viewportCenter.y, 0) + transform.position;
// 绘制圆形视口
if (showViewportCircle)
{
Gizmos.color = viewportBorderColor;
// 使用多个线段绘制圆形
int segments = 32;
float angleStep = 360f / segments;
Vector3 prevPoint = center + new Vector3(viewportRadius, 0, 0);
for (int i = 1; i <= segments; i++)
{
float angle = i * angleStep * Mathf.Deg2Rad;
Vector3 currentPoint = center + new Vector3(
Mathf.Cos(angle) * viewportRadius,
Mathf.Sin(angle) * viewportRadius,
0
);
Gizmos.DrawLine(prevPoint, currentPoint);
prevPoint = currentPoint;
}
}
// 绘制中心点
if (showCenterPoint)
{
Gizmos.color = Color.green;
Gizmos.DrawSphere(center, 0.1f);
// 绘制坐标轴
Gizmos.color = Color.red;
Gizmos.DrawLine(center, center + Vector3.right * 0.5f);
Gizmos.color = Color.green;
Gizmos.DrawLine(center, center + Vector3.up * 0.5f);
}
}
}
}
@@ -1,325 +0,0 @@
using UnityEngine;
using System.Collections.Generic;
using Shapes;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 语言想象系统主管理器 - 协调各模块工作
/// </summary>
[RequireComponent(typeof(CircularViewport))]
public class LanguageImaginationSystem : ImmediateModeShapeDrawer
{
[Header("场设置")]
public float fieldWidth = 50f;
public float fieldHeight = 50f;
public int backgroundNodeCount = 200;
[Header("圆形视口设置")]
public float viewportRadius = 5f;
[Header("连线设置")]
public float connectionLineWidth = 0.05f;
public Color backgroundConnectionColor = new Color(1f, 1f, 1f, 0.3f);
public Color sentenceConnectionColor = Color.yellow;
public Color candidateConnectionColor = Color.cyan;
[Header("Gizmo调试设置")]
public bool showGizmos = true;
public bool showFieldBounds = true;
public bool showNodes = true;
public bool showConnections = true;
public bool showSentenceNodes = true;
public float nodeGizmoSize = 0.2f;
private BackgroundTextLayer _backgroundLayer;
private CircularViewport _viewport;
private SentenceBuilder _sentenceBuilder;
private void Start()
{
Initialize();
}
private void Initialize()
{
// 初始化圆形视口
_viewport = GetComponent<CircularViewport>();
if (_viewport == null)
{
_viewport = gameObject.AddComponent<CircularViewport>();
}
_viewport.viewportRadius = viewportRadius;
// 初始化背景文字层
_backgroundLayer = new BackgroundTextLayer(
fieldWidth,
fieldHeight,
backgroundNodeCount,
transform
);
// 初始化造句系统
_sentenceBuilder = new SentenceBuilder(_backgroundLayer, _viewport, this);
}
private void Update()
{
if (_backgroundLayer != null)
{
_backgroundLayer.Update(Time.deltaTime);
}
}
public override void DrawShapes(Camera cam)
{
using (Draw.Command(cam))
{
Draw.ResetAllDrawStates();
Draw.LineGeometry = LineGeometry.Volumetric3D;
Draw.Thickness = connectionLineWidth;
// 绘制背景文字之间的连线
if (_backgroundLayer != null)
{
DrawBackgroundConnections();
}
// 绘制句子和候选文字的连线
if (_sentenceBuilder != null)
{
DrawSentenceConnections();
}
}
}
private void DrawBackgroundConnections()
{
var allNodes = _backgroundLayer.GetAllNodes();
foreach (var node in allNodes)
{
// 只绘制在视口内的连线
if (!_viewport.IsPositionInViewport(node.Position))
continue;
foreach (var connectedNode in node.ConnectedNodes)
{
// 只绘制连接的另一端也在视口内的连线
if (!_viewport.IsPositionInViewport(connectedNode.Position))
continue;
// 跳过已经选定的节点之间的连线(由句子连线处理)
if (node.State == TextNodeState.Selected || connectedNode.State == TextNodeState.Selected)
continue;
if (node.State == TextNodeState.Candidate || connectedNode.State == TextNodeState.Candidate)
continue;
Vector3 posA = new Vector3(node.Position.x, node.Position.y, 0);
Vector3 posB = new Vector3(connectedNode.Position.x, connectedNode.Position.y, 0);
// 根据节点亮度调整连线颜色
float avgBrightness = (node.Brightness + connectedNode.Brightness) * 0.5f;
Color lineColor = backgroundConnectionColor * avgBrightness;
Draw.Line(posA, posB, connectionLineWidth, lineColor);
}
}
}
private void DrawSentenceConnections()
{
var sentenceNodes = _sentenceBuilder.GetSentenceNodes();
// 绘制句子节点之间的连线(稳定清晰)
for (int i = 0; i < sentenceNodes.Count - 1; i++)
{
var nodeA = sentenceNodes[i];
var nodeB = sentenceNodes[i + 1];
Vector3 posA = new Vector3(nodeA.Position.x, nodeA.Position.y, 0);
Vector3 posB = new Vector3(nodeB.Position.x, nodeB.Position.y, 0);
Draw.Line(posA, posB, connectionLineWidth * 2f, sentenceConnectionColor);
}
// 绘制候选文字与选定文字的连线
var allNodes = _backgroundLayer.GetAllNodes();
foreach (var node in allNodes)
{
if (node.State == TextNodeState.Candidate)
{
foreach (var connectedNode in node.ConnectedNodes)
{
if (connectedNode.State == TextNodeState.Selected)
{
Vector3 posA = new Vector3(node.Position.x, node.Position.y, 0);
Vector3 posB = new Vector3(connectedNode.Position.x, connectedNode.Position.y, 0);
// 根据概率调整连线颜色和宽度
float alpha = node.Probability;
Color lineColor = candidateConnectionColor;
lineColor.a = alpha;
Draw.Line(posA, posB, connectionLineWidth * (0.5f + alpha * 0.5f), lineColor);
}
}
}
}
}
/// <summary>
/// 开始构建句子(由Yarn指令调用)
/// </summary>
public void StartSentence(string sentence, int stuckIndex = -1)
{
if (_sentenceBuilder != null)
{
_sentenceBuilder.StartBuildingSentence(sentence, stuckIndex);
}
}
/// <summary>
/// 停止构建句子
/// </summary>
public void StopSentence()
{
if (_sentenceBuilder != null)
{
_sentenceBuilder.StopBuilding();
}
}
private void OnDestroy()
{
if (_backgroundLayer != null)
{
_backgroundLayer.Cleanup();
}
}
private void OnDrawGizmos()
{
if (!showGizmos) return;
// 绘制场边界
if (showFieldBounds)
{
Gizmos.color = new Color(0.5f, 0.5f, 0.5f, 0.3f);
Vector3 center = transform.position;
Vector3 size = new Vector3(fieldWidth, fieldHeight, 0.1f);
Gizmos.DrawWireCube(center, size);
}
// 绘制节点和连线
if (_backgroundLayer != null && Application.isPlaying)
{
var allNodes = _backgroundLayer.GetAllNodes();
// 绘制节点
if (showNodes)
{
foreach (var node in allNodes)
{
Vector3 nodePos = new Vector3(node.Position.x, node.Position.y, 0) + transform.position;
// 根据状态设置颜色
Color gizmoColor = GetNodeGizmoColor(node.State);
Gizmos.color = gizmoColor * node.Brightness;
// 根据状态设置大小
float size = nodeGizmoSize;
if (node.State == TextNodeState.Selected)
size = nodeGizmoSize * 1.5f;
else if (node.State == TextNodeState.Candidate)
size = nodeGizmoSize * 1.2f;
else if (node.State == TextNodeState.Stuck)
size = nodeGizmoSize * 1.3f;
Gizmos.DrawSphere(nodePos, size);
// 绘制文字标签
#if UNITY_EDITOR
UnityEditor.Handles.Label(nodePos + Vector3.up * 0.5f, node.Text);
#endif
}
}
// 绘制连线
if (showConnections)
{
foreach (var node in allNodes)
{
Vector3 nodePos = new Vector3(node.Position.x, node.Position.y, 0) + transform.position;
foreach (var connectedNode in node.ConnectedNodes)
{
Vector3 connectedPos = new Vector3(connectedNode.Position.x, connectedNode.Position.y, 0) + transform.position;
// 根据节点状态设置连线颜色
Color lineColor = GetConnectionGizmoColor(node, connectedNode);
Gizmos.color = lineColor;
Gizmos.DrawLine(nodePos, connectedPos);
}
}
}
// 绘制句子节点
if (showSentenceNodes && _sentenceBuilder != null)
{
var sentenceNodes = _sentenceBuilder.GetSentenceNodes();
for (int i = 0; i < sentenceNodes.Count; i++)
{
var node = sentenceNodes[i];
Vector3 nodePos = new Vector3(node.Position.x, node.Position.y, 0) + transform.position;
// 绘制句子节点高亮
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(nodePos, nodeGizmoSize * 2f);
// 绘制序号
#if UNITY_EDITOR
UnityEditor.Handles.Label(nodePos + Vector3.up * 1f, $"[{i}]");
#endif
// 绘制句子连线
if (i < sentenceNodes.Count - 1)
{
var nextNode = sentenceNodes[i + 1];
Vector3 nextPos = new Vector3(nextNode.Position.x, nextNode.Position.y, 0) + transform.position;
Gizmos.color = Color.yellow;
Gizmos.DrawLine(nodePos, nextPos);
}
}
}
}
}
private Color GetNodeGizmoColor(TextNodeState state)
{
return state switch
{
TextNodeState.Selected => Color.yellow,
TextNodeState.Candidate => Color.cyan,
TextNodeState.Stuck => Color.red,
_ => Color.white
};
}
private Color GetConnectionGizmoColor(TextNode nodeA, TextNode nodeB)
{
// 句子连线
if (nodeA.State == TextNodeState.Selected && nodeB.State == TextNodeState.Selected)
return Color.yellow;
// 候选连线
if ((nodeA.State == TextNodeState.Candidate && nodeB.State == TextNodeState.Selected) ||
(nodeA.State == TextNodeState.Selected && nodeB.State == TextNodeState.Candidate))
return Color.cyan;
// 背景连线
return new Color(1f, 1f, 1f, 0.3f);
}
}
}
@@ -1,55 +0,0 @@
using System.Collections;
using Yarn.Unity;
using UnityEngine;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 语言想象系统Yarn指令
/// </summary>
public static class LanguageImaginationYarnCommand
{
private static LanguageImaginationSystem GetSystem()
{
// 尝试从场景中查找LanguageImaginationSystem
LanguageImaginationSystem system = Object.FindObjectOfType<LanguageImaginationSystem>();
if (system == null)
{
Debug.LogError("[LanguageImaginationYarnCommand] 未找到LanguageImaginationSystem组件!请在场景中添加该组件。");
}
return system;
}
/// <summary>
/// 开始构建句子
/// </summary>
/// <param name="sentence">要构建的句子</param>
/// <param name="stuckIndex">卡壳位置(从0开始,-1表示不卡壳)</param>
[YarnCommand("language_imagine")]
public static IEnumerator LanguageImagine(string sentence, int stuckIndex = -1)
{
var system = GetSystem();
if (system != null)
{
system.StartSentence(sentence, stuckIndex);
// 等待句子构建完成(这里可以添加更复杂的等待逻辑)
yield return new WaitForSeconds(0.1f);
}
}
/// <summary>
/// 停止构建句子
/// </summary>
[YarnCommand("language_imagine_stop")]
public static void LanguageImagineStop()
{
var system = GetSystem();
if (system != null)
{
system.StopSentence();
}
}
}
}
@@ -1,96 +0,0 @@
# 语言想象功能使用说明
## 功能概述
这是一个语言想象系统,实现了以下功能:
1. **背景文字层**:大量文字不断变化字符和明暗,文字间寻找关联形成词语连线
2. **圆形可视区域**:通过鼠标拖拽移动圆形区域查看不同部分
3. **造句功能**:根据Yarn指令传入的句子,逐步构建句子
4. **卡壳功能**:可在指定位置卡壳,显示错误文字,需要点击移除
## 文件结构
- `TextNode.cs` - 文字节点类,管理单个文字的显示、状态和连接关系
- `BackgroundTextLayer.cs` - 背景文字层系统,生成和管理背景文字
- `CircularViewport.cs` - 圆形可视区域,使用Shapes绘制圆形遮罩
- `SentenceBuilder.cs` - 造句系统,处理句子生成流程
- `LanguageImaginationSystem.cs` - 主管理器,协调各模块工作
- `LanguageImaginationYarnCommand.cs` - Yarn指令接口
## 使用方法
### 1. 场景设置
1. 在场景中创建一个GameObject
2. 添加 `LanguageImaginationSystem` 组件
3. 调整参数:
- **场设置**fieldWidth, fieldHeight, backgroundNodeCount
- **圆形视口设置**viewportRadius
- **连线设置**connectionLineWidth, 各种连线颜色
### 2. Yarn指令使用
#### 开始构建句子
```
<<language_imagine "这是一个示例用来测试的" 3>>
```
- 第一个参数:要构建的句子
- 第二个参数(可选):卡壳位置(从0开始,-1表示不卡壳)。例如3表示在第4个字(索引3)处卡壳
#### 停止构建句子
```
<<language_imagine_stop>>
```
### 3. 功能说明
#### 背景文字层
- 自动生成大量随机文字
- 文字会随机变化字符和亮度
- 文字之间会根据距离和概率建立连接关系
- 连接关系会动态更新
#### 圆形可视区域
- 点击并拖拽圆形区域内的任意位置可以移动视口
- 只有视口内的文字和连线会被显示
#### 造句流程
1. 在当前可视范围内找到或创建第一个字(如"这")
2. 将该字设置为选定状态,放大并高亮显示
3. 移动到句子位置
4. 在选定字周围生成候选文字
5. 候选文字的概率动态变化,直到目标字概率最高
6. 目标字成为新的选定字,其他候选恢复随机
7. 重复直到句子完成
#### 卡壳功能
- 当到达指定的卡壳位置时,会显示错误文字("我是个傻逼")
- 错误文字显示为红色,概率100%
- 点击错误文字可以移除
- 全部移除后继续构建流程
### 4. 视觉表现
- **背景文字连线**:半透明白色,根据文字亮度调整
- **句子连线**:黄色,较粗,稳定清晰
- **候选文字连线**:青色,根据概率调整透明度和宽度
- **选定文字**:黄色,放大1.5倍
- **候选文字**:青色,亮度随概率变化
- **卡壳文字**:红色,放大1.2倍
## 注意事项
1. 确保场景中有主摄像机
2. 需要TextMeshPro支持
3. 需要DOTween支持(用于动画)
4. 需要Shapes库支持(用于绘制)
5. 建议使用正交摄像机以获得更好的2D效果
## 扩展建议
1. 可以调整候选文字的数量和范围
2. 可以自定义卡壳时的错误文字
3. 可以添加更多视觉效果(如粒子效果)
4. 可以优化性能(如对象池、LOD等)
@@ -1,375 +0,0 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 造句系统 - 处理句子生成流程:选定文字、候选文字、概率变化、连线
/// </summary>
public class SentenceBuilder
{
private BackgroundTextLayer _backgroundLayer;
private CircularViewport _viewport;
private List<TextNode> _sentenceNodes;
private string _targetSentence;
private int _currentIndex = 0;
private bool _isBuilding = false;
private int _stuckIndex = -1; // 卡壳位置,-1表示不卡壳
// 候选文字相关
private List<TextNode> _candidateNodes;
private float _probabilityChangeSpeed = 0.5f;
private Coroutine _buildingCoroutine;
private MonoBehaviour _coroutineRunner;
// 句子连线设置
private float _sentenceLineSpacing = 1.5f;
private Vector2 _sentenceDirection = Vector2.right;
public SentenceBuilder(BackgroundTextLayer backgroundLayer, CircularViewport viewport, MonoBehaviour coroutineRunner)
{
_backgroundLayer = backgroundLayer;
_viewport = viewport;
_coroutineRunner = coroutineRunner;
_sentenceNodes = new List<TextNode>();
_candidateNodes = new List<TextNode>();
}
/// <summary>
/// 开始构建句子
/// </summary>
public void StartBuildingSentence(string sentence, int stuckIndex = -1)
{
if (_isBuilding)
{
StopBuilding();
}
_targetSentence = sentence;
_currentIndex = 0;
_stuckIndex = stuckIndex;
_isBuilding = true;
// 清理之前的句子节点
ClearSentence();
// 开始构建协程
_buildingCoroutine = _coroutineRunner.StartCoroutine(BuildingCoroutine());
}
private IEnumerator BuildingCoroutine()
{
// 等待一帧确保背景层已初始化
yield return null;
while (_currentIndex < _targetSentence.Length && _isBuilding)
{
char targetChar = _targetSentence[_currentIndex];
// 检查是否到达卡壳位置
if (_currentIndex == _stuckIndex)
{
yield return _coroutineRunner.StartCoroutine(HandleStuck());
continue;
}
// 在当前可视范围内找到或创建目标文字
TextNode targetNode = FindOrCreateNodeInViewport(targetChar.ToString());
if (targetNode != null)
{
// 设置为选定状态
SetNodeAsSelected(targetNode, _currentIndex);
_sentenceNodes.Add(targetNode);
// 移动到句子位置
yield return MoveNodeToSentencePosition(targetNode, _currentIndex);
// 如果不是最后一个字,准备下一个字的候选
if (_currentIndex < _targetSentence.Length - 1)
{
yield return _coroutineRunner.StartCoroutine(PrepareNextCandidate());
}
_currentIndex++;
}
yield return new WaitForSeconds(0.5f);
}
_isBuilding = false;
}
private TextNode FindOrCreateNodeInViewport(string text)
{
// 首先尝试在可视范围内找到匹配的文字
var nodesInViewport = _backgroundLayer.GetNodesInRange(_viewport.ViewportCenter, _viewport.ViewportRadius);
var matchingNode = nodesInViewport.FirstOrDefault(n => n.Text == text && n.State == TextNodeState.Random);
if (matchingNode != null)
{
return matchingNode;
}
// 如果找不到,在可视范围内随机选择一个节点并改变其文字
if (nodesInViewport.Count > 0)
{
var randomNode = nodesInViewport[Random.Range(0, nodesInViewport.Count)];
randomNode.Text = text;
randomNode.UpdateVisual();
return randomNode;
}
return null;
}
private void SetNodeAsSelected(TextNode node, int index)
{
node.State = TextNodeState.Selected;
node.SentenceIndex = index;
node.Scale = 1.5f;
node.Brightness = 1f;
node.Color = Color.yellow;
node.UpdateVisual();
// 放大动画
node.GameObject.transform.DOScale(1.5f, 0.3f).SetEase(Ease.OutBack);
}
private IEnumerator MoveNodeToSentencePosition(TextNode node, int index)
{
Vector2 targetPos = GetSentencePosition(index);
// 使用DOTween移动
yield return node.GameObject.transform.DOMove(
new Vector3(targetPos.x, targetPos.y, 0),
0.5f
).SetEase(Ease.OutQuad).WaitForCompletion();
node.Position = targetPos;
}
private Vector2 GetSentencePosition(int index)
{
// 以视口中心为起点,沿方向排列
Vector2 startPos = _viewport.ViewportCenter;
Vector2 offset = _sentenceDirection * index * _sentenceLineSpacing;
return startPos + offset;
}
private IEnumerator PrepareNextCandidate()
{
if (_currentIndex >= _targetSentence.Length - 1) yield break;
char nextChar = _targetSentence[_currentIndex + 1];
// 清除之前的候选
ClearCandidates();
// 在当前选定节点周围找到几个节点作为候选
TextNode selectedNode = _sentenceNodes[_currentIndex];
var nearbyNodes = _backgroundLayer.GetNodesInRange(selectedNode.Position, 3f)
.Where(n => n.State == TextNodeState.Random)
.Take(5)
.ToList();
// 确保目标字符在候选列表中
bool hasTargetChar = false;
foreach (var node in nearbyNodes)
{
if (node.Text == nextChar.ToString())
{
hasTargetChar = true;
break;
}
}
if (!hasTargetChar && nearbyNodes.Count > 0)
{
nearbyNodes[0].Text = nextChar.ToString();
nearbyNodes[0].UpdateVisual();
}
// 设置候选状态
_candidateNodes.Clear();
foreach (var node in nearbyNodes)
{
node.State = TextNodeState.Candidate;
node.Color = Color.cyan;
node.Probability = Random.Range(0f, 0.5f);
node.AddConnection(selectedNode);
selectedNode.AddConnection(node);
node.UpdateVisual();
_candidateNodes.Add(node);
}
// 概率变化动画
yield return _coroutineRunner.StartCoroutine(AnimateCandidateProbabilities(nextChar.ToString()));
}
private IEnumerator AnimateCandidateProbabilities(string targetText)
{
float duration = 2f;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = elapsed / duration;
foreach (var node in _candidateNodes)
{
if (node.Text == targetText)
{
// 目标字符概率逐渐增加到1
node.Probability = Mathf.Lerp(node.Probability, 1f, t * 0.1f);
}
else
{
// 其他字符概率逐渐降低
node.Probability = Mathf.Lerp(node.Probability, 0f, t * 0.1f);
}
// 根据概率更新亮度
node.Brightness = 0.5f + node.Probability * 0.5f;
node.UpdateVisual();
}
yield return null;
}
// 找到概率最高的节点(应该是目标字符)
var selectedCandidate = _candidateNodes.OrderByDescending(n => n.Probability).FirstOrDefault();
if (selectedCandidate != null && selectedCandidate.Text == targetText)
{
// 清除候选状态,恢复随机
ClearCandidates();
selectedCandidate.State = TextNodeState.Random;
}
}
private IEnumerator HandleStuck()
{
// 生成卡壳的错误文字
string stuckText = "我是个傻逼";
// 在当前位置创建卡壳文字
List<TextNode> stuckNodes = new List<TextNode>();
Vector2 currentPos = GetSentencePosition(_currentIndex);
for (int i = 0; i < stuckText.Length; i++)
{
TextNode stuckNode = FindOrCreateNodeInViewport(stuckText[i].ToString());
if (stuckNode != null)
{
stuckNode.State = TextNodeState.Stuck;
stuckNode.IsStuck = true;
stuckNode.SentenceIndex = _currentIndex + i;
stuckNode.Color = Color.red;
stuckNode.Probability = 1f;
stuckNode.Scale = 1.2f;
stuckNode.Position = currentPos + Vector2.right * i * _sentenceLineSpacing;
stuckNode.UpdateVisual();
stuckNodes.Add(stuckNode);
}
}
// 等待玩家点击移除所有卡壳文字
while (stuckNodes.Count > 0)
{
yield return null;
// 检查点击
if (Input.GetMouseButtonDown(0))
{
Vector2 mousePos = GetMouseWorldPosition();
for (int i = stuckNodes.Count - 1; i >= 0; i--)
{
var node = stuckNodes[i];
float distance = Vector2.Distance(mousePos, node.Position);
if (distance < 1f) // 点击范围
{
// 移除节点
node.State = TextNodeState.Random;
node.IsStuck = false;
node.Color = Color.white;
node.UpdateVisual();
stuckNodes.RemoveAt(i);
// 销毁GameObject
if (node.GameObject != null)
{
Object.Destroy(node.GameObject);
}
}
}
}
}
// 卡壳处理完成,继续构建
}
private Vector2 GetMouseWorldPosition()
{
Camera cam = Camera.main;
if (cam == null) return Vector2.zero;
Vector3 mousePos = Input.mousePosition;
mousePos.z = cam.nearClipPlane + 1f;
Vector3 worldPos = cam.ScreenToWorldPoint(mousePos);
return new Vector2(worldPos.x, worldPos.y);
}
private void ClearCandidates()
{
foreach (var node in _candidateNodes)
{
if (node.State == TextNodeState.Candidate)
{
node.State = TextNodeState.Random;
node.Color = Color.white;
node.Probability = 0f;
node.Brightness = 1f;
node.UpdateVisual();
}
}
_candidateNodes.Clear();
}
private void ClearSentence()
{
foreach (var node in _sentenceNodes)
{
if (node.State == TextNodeState.Selected)
{
node.State = TextNodeState.Random;
node.SentenceIndex = -1;
node.Scale = 1f;
node.Color = Color.white;
node.UpdateVisual();
}
}
_sentenceNodes.Clear();
}
public void StopBuilding()
{
_isBuilding = false;
if (_buildingCoroutine != null)
{
_coroutineRunner.StopCoroutine(_buildingCoroutine);
_buildingCoroutine = null;
}
ClearCandidates();
}
public List<TextNode> GetSentenceNodes()
{
return _sentenceNodes;
}
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: fcab091b0b0e39142ae616cc9a548074
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,75 +0,0 @@
using UnityEngine;
using TMPro;
using System.Collections.Generic;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 文字节点类 - 管理单个文字的显示、状态和连接关系
/// </summary>
public class TextNode
{
public Vector2 Position { get; set; }
public string Text { get; set; }
public TextNodeState State { get; set; }
public float Brightness { get; set; } = 1f;
public float Scale { get; set; } = 1f;
public Color Color { get; set; } = Color.white;
public float Probability { get; set; } = 0f; // 候选文字的概率
// 连接关系
public List<TextNode> ConnectedNodes { get; private set; }
// UI组件引用
public TextMeshPro TextComponent { get; set; }
public GameObject GameObject { get; set; }
// 句子相关
public int SentenceIndex { get; set; } = -1; // 在句子中的索引,-1表示不在句子中
public bool IsStuck { get; set; } = false; // 是否处于卡壳状态
public TextNode(Vector2 position, string text)
{
Position = position;
Text = text;
State = TextNodeState.Random;
ConnectedNodes = new List<TextNode>();
}
public void AddConnection(TextNode node)
{
if (!ConnectedNodes.Contains(node))
{
ConnectedNodes.Add(node);
}
}
public void RemoveConnection(TextNode node)
{
ConnectedNodes.Remove(node);
}
public void UpdateVisual()
{
if (TextComponent != null)
{
TextComponent.text = Text;
TextComponent.color = Color * Brightness;
TextComponent.transform.localScale = Vector3.one * Scale;
TextComponent.transform.position = new Vector3(Position.x, Position.y, 0);
}
}
}
/// <summary>
/// 文字节点状态
/// </summary>
public enum TextNodeState
{
Random, // 随机变化状态
Selected, // 已选定的文字(句子中的字)
Candidate, // 候选文字
Stuck // 卡壳状态
}
}
@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 27e691dacc3b63545924959f964de0bb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: