diff --git a/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset index 40d3e1120..51f09fa9f 100644 --- a/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset +++ b/Assets/Plugins/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3ff50e3133d989180b187748ffc67903f1fede099a5b85b4fcbd8cb6e8be958 -size 2256862 +oid sha256:8d6c87f520f1c076c6a23d015fbfacbef24af6f0a6312c00b1a7e2ff34f40c27 +size 2264562 diff --git a/Assets/Render/Renderer2Dtest.asset b/Assets/Render/Renderer2Dtest.asset index 197d64d62..fdae0d5da 100644 --- a/Assets/Render/Renderer2Dtest.asset +++ b/Assets/Render/Renderer2Dtest.asset @@ -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 diff --git a/Assets/Scenes/languageTest.unity b/Assets/Scenes/languageTest.unity new file mode 100644 index 000000000..b59dddfd8 --- /dev/null +++ b/Assets/Scenes/languageTest.unity @@ -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} diff --git a/Assets/Scenes/languageTest.unity.meta b/Assets/Scenes/languageTest.unity.meta new file mode 100644 index 000000000..e356c7ee0 --- /dev/null +++ b/Assets/Scenes/languageTest.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a10c0994c14362743ab6f65c6382410d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Framework/OtherKit/CameraKit.cs b/Assets/Scripts/Framework/OtherKit/CameraKit.cs index b3f2635a1..c5b500772 100644 --- a/Assets/Scripts/Framework/OtherKit/CameraKit.cs +++ b/Assets/Scripts/Framework/OtherKit/CameraKit.cs @@ -419,6 +419,8 @@ namespace AibisDream.Framework BreakCamera, DreamCamera, Subway, - SubwayTV + SubwayTV, + Language, + LanguageDeep } } \ No newline at end of file diff --git a/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt b/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt new file mode 100644 index 000000000..a9b058f6a --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt @@ -0,0 +1,1020 @@ +// 粒子类 +class Particle { + constructor(x, y, targetX, targetY) { + this.x = x; + this.y = y; + this.targetX = targetX; + this.targetY = targetY; + this.life = 1.0; + this.speed = random(0.02, 0.05); + this.size = random(2, 4); + this.alpha = random(150, 255); + } + + update() { + this.x = lerp(this.x, this.targetX, this.speed); + this.y = lerp(this.y, this.targetY, this.speed); + + let dist = distance(this.x, this.y, this.targetX, this.targetY); + if (dist < 5) { + this.life -= 0.05; + } + } + + display() { + push(); + noStroke(); + fill(100, 200, 255, this.alpha * this.life); + circle(this.x, this.y, this.size); + pop(); + } + + isDead() { + return this.life <= 0; + } +} + +function distance(x1, y1, x2, y2) { + return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); +} + +// 文字对象类 +class FloatingText { + constructor(x, y, radius) { + this.centerX = x; + this.centerY = y; + this.maxRadius = radius; + + let angle = random(TWO_PI); + let r = random(this.maxRadius * 0.9); + this.x = this.centerX + cos(angle) * r; + this.y = this.centerY + sin(angle) * r; + + this.targetX = this.x; + this.targetY = this.y; + + this.speedX = random(-0.3, 0.3); + this.speedY = random(-0.3, 0.3); + + this.noiseOffsetX = random(1000); + this.noiseOffsetY = random(1000); + + this.updateText(); + + this.baseSize = random(20, 32); + this.size = this.baseSize; + this.targetSize = this.baseSize; + + this.baseAlpha = random(50, 150); + this.alpha = this.baseAlpha; + this.targetAlpha = this.baseAlpha; + + this.changeTimer = random(1, 2); + this.changeCounter = 0; + + // 造句相关 + this.isCandidate = false; + this.isSelected = false; + this.isFinal = false; + this.isEliminated = false; + + // 概率相关 + this.probability = 0; + this.targetProbability = 0; + + // 候选字变化 + this.candidateChars = []; + this.candidateIndex = 0; + this.candidateChangeTimer = 0; + + // 抖动效果 + this.shakeAmount = 0; + } + + updateText() { + const chars = [ + '梦', '想', '希', '望', '光', '影', '星', '月', '云', '风', + '诗', '歌', '舞', '画', '音', '色', '情', '爱', '心', '灵', + '天', '地', '人', '和', '美', '真', '善', '雅', '韵', '意', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + ]; + + let numChars = random() > 0.6 ? 2 : 1; + this.text = ''; + for (let i = 0; i < numChars; i++) { + this.text += random(chars); + } + } + + setCandidate(candidateChars, initialProbability) { + this.isCandidate = true; + this.candidateChars = candidateChars; + this.candidateIndex = 0; + this.text = candidateChars[0]; + this.candidateChangeTimer = 0; + this.probability = initialProbability; + this.targetProbability = initialProbability; + + // 根据初始概率设置大小 + this.updateSizeByProbability(); + this.shakeAmount = 3; // 不确定时抖动 + } + + updateSizeByProbability() { + // 概率越高,字越大 + let sizeMultiplier = map(this.targetProbability, 0, 100, 1.2, 2.0); + this.targetSize = this.baseSize * sizeMultiplier; + this.targetAlpha = map(this.targetProbability, 0, 100, 150, 255); + } + + setProbability(prob) { + this.targetProbability = prob; + this.updateSizeByProbability(); + + // 概率高的时候抖动减少 + this.shakeAmount = map(prob, 0, 100, 5, 0); + } + + eliminate() { + this.isEliminated = true; + this.targetAlpha = 0; + this.targetSize = this.baseSize * 0.5; + } + + setSelected(targetX, targetY, isFirst) { + this.isCandidate = false; + this.isSelected = true; + this.shakeAmount = 0; + + let dx = targetX - this.x; + let dy = targetY - this.y; + let maxMove = 40; + + if (abs(dx) > maxMove || abs(dy) > maxMove) { + let angle = atan2(dy, dx); + this.targetX = this.x + cos(angle) * maxMove; + this.targetY = this.y + sin(angle) * maxMove; + } else { + this.targetX = targetX; + this.targetY = targetY; + } + + if (isFirst) { + this.targetSize = 55; + this.targetAlpha = 255; + } else { + this.targetSize = 42; + this.targetAlpha = 255; + } + } + + cancelCandidate() { + this.isCandidate = false; + this.isEliminated = false; + this.probability = 0; + this.targetProbability = 0; + this.targetSize = this.baseSize; + this.targetAlpha = this.baseAlpha; + this.candidateChars = []; + this.shakeAmount = 0; + this.updateText(); + } + + finalize() { + this.isFinal = true; + this.shakeAmount = 0; + } + + reset() { + this.isCandidate = false; + this.isSelected = false; + this.isFinal = false; + this.isEliminated = false; + this.probability = 0; + this.targetProbability = 0; + this.targetSize = this.baseSize; + this.targetAlpha = this.baseAlpha; + this.size = this.baseSize; + this.alpha = this.baseAlpha; + this.candidateChars = []; + this.shakeAmount = 0; + this.updateText(); + } + + update() { + // 候选字缓慢变化 + if (this.isCandidate && this.candidateChars.length > 1 && !this.isEliminated) { + this.candidateChangeTimer++; + if (this.candidateChangeTimer >= 12) { + this.candidateIndex = (this.candidateIndex + 1) % this.candidateChars.length; + this.text = this.candidateChars[this.candidateIndex]; + this.candidateChangeTimer = 0; + } + } + + // 概率平滑过渡 + this.probability = lerp(this.probability, this.targetProbability, 0.1); + + if (this.isSelected || this.isCandidate) { + this.x = lerp(this.x, this.targetX, 0.08); + this.y = lerp(this.y, this.targetY, 0.08); + this.size = lerp(this.size, this.targetSize, 0.1); + this.alpha = lerp(this.alpha, this.targetAlpha, 0.1); + } else { + let noiseX = noise(this.noiseOffsetX) * 2 - 1; + let noiseY = noise(this.noiseOffsetY) * 2 - 1; + + this.x += noiseX * 0.5 + this.speedX; + this.y += noiseY * 0.5 + this.speedY; + + this.noiseOffsetX += 0.01; + this.noiseOffsetY += 0.01; + + let dx = this.x - this.centerX; + let dy = this.y - this.centerY; + let dist = sqrt(dx * dx + dy * dy); + + if (dist > this.maxRadius) { + let angle = atan2(dy, dx); + this.x = this.centerX + cos(angle) * this.maxRadius; + this.y = this.centerY + sin(angle) * this.maxRadius; + this.speedX *= -0.8; + this.speedY *= -0.8; + } + + this.changeCounter++; + if (this.changeCounter >= this.changeTimer) { + this.updateText(); + this.changeCounter = 0; + this.changeTimer = random(5, 10); + } + } + } + + display() { + push(); + + // 计算抖动偏移 + let shakeX = random(-this.shakeAmount, this.shakeAmount); + let shakeY = random(-this.shakeAmount, this.shakeAmount); + + // 根据概率显示彩色光晕 + if (this.isCandidate && !this.isEliminated) { + let hue = map(this.probability, 0, 100, 200, 0); // 蓝色到红色 + let glowSize = map(this.probability, 0, 100, 20, 50); + + noStroke(); + fill(hue, 200, 255, 30); + circle(this.x + shakeX, this.y + shakeY, this.size + glowSize); + + // 边框 + stroke(hue, 200, 255, this.alpha * 0.5); + strokeWeight(2); + noFill(); + circle(this.x + shakeX, this.y + shakeY, this.size + 15); + } + + // 显示文字 + fill(255, this.alpha); + noStroke(); + textAlign(CENTER, CENTER); + textSize(this.size); + text(this.text, this.x + shakeX, this.y + shakeY); + + // 显示概率 + if (this.isCandidate && !this.isEliminated && this.probability > 0) { + textSize(12); + fill(255, 200); + text(int(this.probability) + '%', this.x + shakeX, this.y + shakeY + this.size * 0.6); + } + + pop(); + } +} + +let floatingTexts = []; +let numTexts = 60; +let circleRadius = 280; + +// 造句相关 +let targetSentence = "我想要被看到"; +let sentenceProgress = 0; +let selectedTexts = []; +let candidateTexts = []; +let sentenceStarted = false; +let sentenceComplete = false; + +// 粒子系统 +let particles = []; + +// 预测状态机 +let predictionState = 'idle'; // idle, round1, round2, round3, confirming +let stateTimer = 0; + +function setup() { + createCanvas(windowWidth, windowHeight); + + // 自动创建词云 + for (let i = 0; i < numTexts; i++) { + floatingTexts.push(new FloatingText(width / 2, height / 2, circleRadius)); + } +} + +function draw() { + background(0); + + // 更新和显示所有文字 + for (let text of floatingTexts) { + text.update(); + text.display(); + } + + // 更新和显示粒子 + for (let i = particles.length - 1; i >= 0; i--) { + particles[i].update(); + particles[i].display(); + if (particles[i].isDead()) { + particles.splice(i, 1); + } + } + + // 绘制已确定字符的连线 + if (selectedTexts.length > 1) { + stroke(255, 180); + strokeWeight(2); + for (let i = 0; i < selectedTexts.length - 1; i++) { + let current = selectedTexts[i]; + let next = selectedTexts[i + 1]; + if (next.isFinal) { + line(current.x, current.y, next.x, next.y); + } + } + } + + // 发射粒子(在预测阶段) + if ((predictionState === 'round1' || predictionState === 'round2' || predictionState === 'round3') + && selectedTexts.length > 0 && frameCount % 3 === 0) { + let lastSelected = selectedTexts[selectedTexts.length - 1]; + + for (let candidate of candidateTexts) { + if (!candidate.text.isEliminated) { + // 粒子密度根据概率 + let particleCount = map(candidate.text.probability, 0, 100, 0.5, 3); + if (random() < particleCount / 3) { + particles.push(new Particle( + lastSelected.x, + lastSelected.y, + candidate.text.x, + candidate.text.y + )); + } + } + } + } + + // 显示状态提示 + if (sentenceStarted && !sentenceComplete) { + displayStateHint(); + } + + // 预测过程 + if (sentenceStarted && sentenceProgress < targetSentence.length) { + handlePrediction(); + } +} + +function displayStateHint() { + push(); + textAlign(LEFT, TOP); + textSize(14); + fill(150, 200, 255, 200); + noStroke(); + + let hint = ''; + switch(predictionState) { + case 'round1': + hint = '正在分析可能性...'; + break; + case 'round2': + hint = '匹配语法结构...'; + break; + case 'round3': + hint = '计算最终概率...'; + break; + case 'confirming': + hint = '确认结果中...'; + break; + } + + if (hint) { + text(hint, 30, 30); + } + pop(); +} + +function handlePrediction() { + stateTimer++; + + switch(predictionState) { + case 'idle': + startPrediction(); + break; + + case 'round1': + // 第一轮:6-7个候选,概率20-40%(持续0.8秒) + if (stateTimer >= 48) { + eliminateRound1(); + predictionState = 'round2'; + stateTimer = 0; + } + break; + + case 'round2': + // 第二轮:3-4个候选,概率40-60%(持续0.7秒) + if (stateTimer >= 42) { + eliminateRound2(); + predictionState = 'round3'; + stateTimer = 0; + } + break; + + case 'round3': + // 第三轮:2个候选竞争,概率60-80%(持续1秒) + if (stateTimer >= 60) { + predictionState = 'confirming'; + stateTimer = 0; + confirmPrediction(); + } + break; + + case 'confirming': + // 确认阶段:正确答案概率飙升到95%+(持续0.5秒) + if (stateTimer >= 30) { + finalizePrediction(); + predictionState = 'idle'; + stateTimer = 0; + sentenceProgress++; + + if (sentenceProgress >= targetSentence.length) { + sentenceComplete = true; + setTimeout(() => { + resetSentence(); + }, 1500); + } + } + break; + } +} + +function startPrediction() { + let correctChar = targetSentence[sentenceProgress]; + + // 如果是第一个字,直接显示 + if (sentenceProgress === 0) { + let availableTexts = floatingTexts.filter(t => !t.isSelected); + let selectedText = random(availableTexts); + + let startX = width / 2 - (targetSentence.length * 45) / 2; + selectedText.text = correctChar; + selectedText.setSelected(startX, height / 2, true); + selectedText.finalize(); + selectedTexts.push(selectedText); + + sentenceProgress++; + predictionState = 'idle'; + stateTimer = 0; + return; + } + + // 生成候选字(6-7个) + let candidates = [correctChar]; + const possibleChars = ['要', '爱', '见', '到', '你', '他', '她', '们', '的', '了', '吗', '呢', '想', '看', '被', '着', '给', '和', '在']; + + let numCandidates = int(random(6, 8)); + while (candidates.length < numCandidates) { + let distractor = random(possibleChars); + if (!candidates.includes(distractor)) { + candidates.push(distractor); + } + } + + // 找到可用的文字对象 + candidateTexts = []; + let availableTexts = floatingTexts.filter(t => !t.isSelected && !t.isCandidate); + + for (let i = 0; i < candidates.length && i < availableTexts.length; i++) { + let text = availableTexts[i]; + let charVariations = [candidates[i]]; + + // 添加变化字符 + for (let j = 0; j < 2; j++) { + let variation = random(possibleChars); + if (!charVariations.includes(variation)) { + charVariations.push(variation); + } + } + charVariations.push(candidates[i]); + + // 初始概率随机分配 + let initialProb = random(15, 35); + text.setCandidate(charVariations, initialProb); + + candidateTexts.push({ + text: text, + correctChar: candidates[i], + isCorrect: candidates[i] === correctChar + }); + } + + predictionState = 'round1'; + stateTimer = 0; +} + +function eliminateRound1() { + // 淘汰概率最低的3个 + candidateTexts.sort((a, b) => { + if (a.isCorrect) return 1; // 确保正确答案不被淘汰 + if (b.isCorrect) return -1; + return a.text.probability - b.text.probability; + }); + + let toEliminate = min(3, candidateTexts.length - 3); + for (let i = 0; i < toEliminate; i++) { + candidateTexts[i].text.eliminate(); + } + + // 剩余候选概率上升到40-60% + for (let i = toEliminate; i < candidateTexts.length; i++) { + let newProb = random(40, 60); + if (candidateTexts[i].isCorrect) { + newProb = random(50, 65); // 正确答案稍高 + } + candidateTexts[i].text.setProbability(newProb); + } +} + +function eliminateRound2() { + // 移除已淘汰的 + candidateTexts = candidateTexts.filter(c => !c.text.isEliminated); + + // 再淘汰1-2个 + candidateTexts.sort((a, b) => { + if (a.isCorrect) return 1; + if (b.isCorrect) return -1; + return a.text.probability - b.text.probability; + }); + + let toEliminate = min(candidateTexts.length - 2, 2); + for (let i = 0; i < toEliminate; i++) { + candidateTexts[i].text.eliminate(); + } + + // 剩余2个候选概率上升到60-80% + for (let i = toEliminate; i < candidateTexts.length; i++) { + let newProb = random(60, 75); + if (candidateTexts[i].isCorrect) { + newProb = random(70, 82); // 正确答案更高 + } + candidateTexts[i].text.setProbability(newProb); + } +} + +function confirmPrediction() { + candidateTexts = candidateTexts.filter(c => !c.text.isEliminated); + + // 正确答案概率飙升到95%+ + for (let candidate of candidateTexts) { + if (candidate.isCorrect) { + candidate.text.setProbability(random(95, 99)); + } else { + candidate.text.setProbability(random(5, 15)); + candidate.text.eliminate(); + } + } +} + +function finalizePrediction() { + let correctText = null; + + for (let candidate of candidateTexts) { + if (candidate.isCorrect) { + correctText = candidate.text; + } else { + candidate.text.cancelCandidate(); + } + } + + if (correctText) { + let startX = width / 2 - (targetSentence.length * 45) / 2; + let targetX = startX + sentenceProgress * 45; + let targetY = height / 2; + + correctText.setSelected(targetX, targetY, false); + + setTimeout(() => { + correctText.finalize(); + }, 300); + + selectedTexts.push(correctText); + } + + candidateTexts = []; +} + +function resetSentence() { + sentenceStarted = false; + sentenceComplete = false; + sentenceProgress = 0; + predictionState = 'idle'; + stateTimer = 0; + particles = []; + + for (let text of selectedTexts) { + text.reset(); + } + selectedTexts = []; + candidateTexts = []; +} + +function windowResized() { + resizeCanvas(windowWidth, windowHeight); + for (let text of floatingTexts) { + text.centerX = width / 2; + text.centerY = height / 2; + } +} + +function mousePressed() { + if (!sentenceStarted && !sentenceComplete) { + sentenceStarted = true; + predictionState = 'idle'; + stateTimer = 0; + } +} +能把这个的背景变成 +// 文字粒子类 +class TextParticle { + constructor(x, y) { + this.x = x; + this.y = y; + this.vx = random(-0.5, 0.5); + this.vy = random(-0.5, 0.5); + this.char = this.randomChar(); + this.alpha = random(40, 180); + this.baseAlpha = this.alpha; + this.size = random(25, 35); + this.changeTimer = random(10, 30); + this.connections = []; + this.connectionTimer = 0; + this.isConnected = false; + this.attractionTimer = 0; + } + + randomChar() { + const chars = '的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严'; + return chars[floor(random(chars.length))]; + } + + update(offsetX, offsetY) { + this.x += this.vx; + this.y += this.vy; + + // 轻微漂浮 + this.vx += random(-0.1, 0.1); + this.vy += random(-0.1, 0.1); + + // 限制速度 + this.vx = constrain(this.vx, -1, 1); + this.vy = constrain(this.vy, -1, 1); + + // 边界处理 + const boundary = 800; + if (this.x < -boundary) this.x = -boundary; + if (this.x > boundary) this.x = boundary; + if (this.y < -boundary) this.y = -boundary; + if (this.y > boundary) this.y = boundary; + + // 只有在没有连接时才变换文字 + if (!this.isConnected) { + this.changeTimer--; + if (this.changeTimer <= 0) { + this.char = this.randomChar(); + this.changeTimer = random(2, 4); + } + } + + // 更新吸引计时器 + if (this.attractionTimer > 0) { + this.attractionTimer--; + } + } + + display(offsetX, offsetY) { + push(); + + // 如果有连接,显示更明显:放大 + let displaySize = this.size; + if (this.isConnected) { + displaySize = this.size * 1.3; + } + + // 绘制文字 + fill(255, this.alpha); + noStroke(); + textSize(displaySize); + textAlign(CENTER, CENTER); + text(this.char, this.x + offsetX, this.y + offsetY); + pop(); + } + + // 计算到其他粒子的距离 + distTo(other) { + return dist(this.x, this.y, other.x, other.y); + } + + // 检查粒子是否在遮罩范围内(考虑offset) + isInMask(offsetX, offsetY, centerX, centerY, radius) { + let screenX = this.x + offsetX; + let screenY = this.y + offsetY; + let d = dist(screenX, screenY, centerX, centerY); + return d < radius; + } +} + +// ========== 文字密度调节区域 ========== +const PARTICLE_COUNT = 250; +// ===================================== + +// ========== 连线粗细调节区域 ========== +const LINE_THICKNESS = 2.5; +// ===================================== + +// 全局变量 +let particles = []; +let offsetX = 0, offsetY = 0; +let lastMouseX = 0, lastMouseY = 0; +let isDragging = false; +let maskRadius = 250; +let phraseGroups = []; + +// 有意义的词组和短句(2-4字,像潜意识的想法) +const meaningfulPhrases = [ + '我想', '可以', '不行', '为什么', + '去哪', '做梦', '忘记', '记得', + '爱你', '讨厌', '喜欢', '害怕', + '自由', '困住', '离开', '回来', + '明天', '昨天', '现在', '以后', + '真的', '假的', '也许', '一定', + '孤独', '热闹', '安静', '吵闹', + '快乐', '悲伤', '愤怒', '平静', + '梦想', '现实', '理想', '幻想', + '逃避', '面对', '接受', '拒绝', + '开始', '结束', '继续', '放弃', + '找到', '失去', '得到', '放下', + '相信', '怀疑', '确定', '迷茫', + '成长', '退缩', '前进', '后退', + '温暖', '冷漠', '善良', '残忍', + '希望', '绝望', '勇气', '懦弱', + '坚持', '动摇', '改变', '守护', + '理解', '误解', '清楚', '混乱', + '简单', '复杂', '容易', '困难', + '靠近', '远离', '拥抱', '推开', + '说谎', '诚实', '隐藏', '坦白', + '醒来', '睡去', '清醒', '迷糊', + '选择', '犹豫', '决定', '后悔', + '珍惜', '浪费', '把握', '错过', + '等待', '追逐', '寻找', '遇见', + '重要', '无聊', '有趣', '平凡', + '特别', '普通', '独特', '相同', + '永远', '瞬间', '长久', '短暂', + '完整', '破碎', '圆满', '遗憾', + '美好', '糟糕', '幸福', '痛苦', + '想起', '遗忘', '回忆', '未来', + '过去', '此刻', '那时', '现在', + '或许', '肯定', '否定', '承认', + '否认', '相遇', '别离', '重逢', + '陌生', '熟悉', '新鲜', '厌倦', + '期待', '失望', '满足', '渴望', + '需要', '多余', '必须', '随意', + '认真', '敷衍', '真心', '假意', + '明白', '糊涂', '聪明', '愚蠢', + '清醒', '沉醉', '冷静', '疯狂' +]; + +function setup() { + createCanvas(800, 600); + textFont('Arial'); + + // 创建粒子 + for (let i = 0; i < PARTICLE_COUNT; i++) { + particles.push(new TextParticle( + random(-600, 600), + random(-600, 600) + )); + } +} + +function draw() { + background(20, 25, 30); + + // 更新粒子 + for (let p of particles) { + p.update(offsetX, offsetY); + } + + // 建立和断开连接 + updateConnections(); + + // 应用轻微的吸引力 + applyConnectionForces(); + + // 开始圆形遮罩 + push(); + drawingContext.save(); + drawingContext.beginPath(); + drawingContext.arc(width / 2, height / 2, maskRadius, 0, TWO_PI); + drawingContext.clip(); + + // 绘制连接线 + stroke(120, 180, 255, 180); + strokeWeight(LINE_THICKNESS); + for (let group of phraseGroups) { + for (let i = 0; i < group.particles.length - 1; i++) { + let p1 = group.particles[i]; + let p2 = group.particles[i + 1]; + line( + p1.x + offsetX, p1.y + offsetY, + p2.x + offsetX, p2.y + offsetY + ); + } + } + + // 绘制粒子 + for (let p of particles) { + p.display(offsetX, offsetY); + } + + drawingContext.restore(); + pop(); + + // 绘制圆形边界 + push(); + noFill(); + stroke(100, 150, 255, 150); + strokeWeight(2); + circle(width / 2, height / 2, maskRadius * 2); + pop(); + + // 绘制提示信息 + fill(255, 100); + noStroke(); + textSize(14); + textAlign(LEFT); + text('拖拽鼠标查看不同区域', 10, 20); + text('当前文字数量: ' + PARTICLE_COUNT, 10, 40); +} + +// 更新连接关系 +function updateConnections() { + // 更新现有短语组的计时器 + for (let i = phraseGroups.length - 1; i >= 0; i--) { + let group = phraseGroups[i]; + group.timer--; + + if (group.timer <= 0) { + // 时间到,解除连接 + for (let p of group.particles) { + p.isConnected = false; + p.connections = []; + p.alpha = p.baseAlpha; + } + phraseGroups.splice(i, 1); + } + } + + // 提高连接频率:随机建立新的短语连接 + if (random() < 0.08) { + // 选择一个随机短语 + let phrase = random(meaningfulPhrases); + let phraseLength = phrase.length; + + // 筛选在遮罩范围内且未连接的粒子 + let centerX = width / 2; + let centerY = height / 2; + let availableParticles = particles.filter(p => + !p.isConnected && p.isInMask(offsetX, offsetY, centerX, centerY, maskRadius - 50) + ); + + // 如果遮罩内粒子不够,放宽范围 + if (availableParticles.length < phraseLength) { + availableParticles = particles.filter(p => + !p.isConnected && p.isInMask(offsetX, offsetY, centerX, centerY, maskRadius + 100) + ); + } + + if (availableParticles.length < phraseLength) return; + + // 选择一个起始粒子 + let startParticle = random(availableParticles); + let selectedParticles = [startParticle]; + + // 移除已选择的粒子 + availableParticles = availableParticles.filter(p => p !== startParticle); + + // 选择附近的其他粒子 + for (let i = 1; i < phraseLength; i++) { + if (availableParticles.length === 0) break; + + // 找到距离最后选择的粒子较近的粒子 + let lastSelected = selectedParticles[selectedParticles.length - 1]; + let nearbyParticles = availableParticles.filter(p => + lastSelected.distTo(p) < 200 + ); + + if (nearbyParticles.length > 0) { + let nextParticle = random(nearbyParticles); + selectedParticles.push(nextParticle); + availableParticles = availableParticles.filter(p => p !== nextParticle); + } else { + // 如果没有附近的,就随机选择 + let nextParticle = random(availableParticles); + selectedParticles.push(nextParticle); + availableParticles = availableParticles.filter(p => p !== nextParticle); + } + } + + if (selectedParticles.length === phraseLength) { + // 设置每个粒子的文字为短语中的字,并提高透明度 + for (let i = 0; i < phraseLength; i++) { + selectedParticles[i].char = phrase[i]; + selectedParticles[i].isConnected = true; + selectedParticles[i].attractionTimer = 20; + selectedParticles[i].alpha = random(190, 210); + } + + // 创建连接 + for (let i = 0; i < phraseLength - 1; i++) { + selectedParticles[i].connections = [selectedParticles[i + 1]]; + } + + // 添加到短语组 + phraseGroups.push({ + particles: selectedParticles, + timer: random(60, 120) + }); + } + } +} + +// 应用轻微的吸引力 +function applyConnectionForces() { + for (let group of phraseGroups) { + for (let i = 0; i < group.particles.length - 1; i++) { + let p1 = group.particles[i]; + let p2 = group.particles[i + 1]; + + // 只在吸引计时器大于0时应用力 + if (p1.attractionTimer > 0 || p2.attractionTimer > 0) { + let dx = p2.x - p1.x; + let dy = p2.y - p1.y; + let d = sqrt(dx * dx + dy * dy); + + if (d > 0 && d > 60) { + let force = 0.15; + p1.vx += (dx / d) * force; + p1.vy += (dy / d) * force; + p2.vx -= (dx / d) * force; + p2.vy -= (dy / d) * force; + } + } + } + } +} + +// 鼠标按下 +function mousePressed() { + let d = dist(mouseX, mouseY, width / 2, height / 2); + if (d < maskRadius) { + isDragging = true; + lastMouseX = mouseX; + lastMouseY = mouseY; + } +} + +// 鼠标拖拽 +function mouseDragged() { + if (isDragging) { + let dx = mouseX - lastMouseX; + let dy = mouseY - lastMouseY; + offsetX += dx; + offsetY += dy; + lastMouseX = mouseX; + lastMouseY = mouseY; + } +} + +// 鼠标释放 +function mouseReleased() { + isDragging = false; +}这样的么 \ No newline at end of file diff --git a/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt.meta b/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt.meta new file mode 100644 index 000000000..f260afc06 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Emo/Untitled-2.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c7cbe6739626ae5458cf9c27e963b310 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt b/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt new file mode 100644 index 000000000..eb95947b7 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt @@ -0,0 +1,1871 @@ +// ==================== 全局变量 ==================== +// 粒子数组 +let floatingTextParticles = []; // 背景浮动文字粒子数组 +let candidateParticles = []; // 候选粒子数组(可交互的粒子) +let rippleEffects = []; // 涟漪效果数组 +let repelBursts = []; // 排斥爆发效果数组 + +// 游戏参数 +let fontSize = 28; // 字体大小 +let floatingCount = 100; // 背景浮动文字数量 +let candidateCount = 50; // 候选粒子数量 +let redParticleCount = 8; // 红色粒子(目标粒子)数量 +let connectionDistance = 200; // 连接距离阈值(像素) + +// 游戏状态 +let isCompleted = false; // 游戏是否完成 +let completionPhase = 'none'; // 完成阶段:'none'(未完成), 'arranging'(排列中), 'revealing'(揭示中), 'completed'(已完成) +let completionTimer = 0; // 完成动画计时器 +let fadeOutAlpha = 255; // 淡出透明度(0-255) +let orderedRedParticles = []; // 按顺序排列的红色粒子数组 +let targetPositions = []; // 目标位置数组(用于完成动画) +let targetSentence = "别过来我感觉害怕"; // 目标句子 + +// 波形交互状态 +let calmProgress = 0; // 平静进度(0-1,影响文字的抖动和颜色) +let waveformPoints = []; // 波形点数组(用于绘制波形) +let waveformLength = 200; // 波形长度(点数) +let waveformAmplitude = 30; // 波形振幅(像素) +let waveformBaseY = 0; // 波形基准Y坐标 +let mouseInteractionActive = false; // 鼠标交互是否激活 +let interactionRadius = 50; // 交互影响半径 +let waveformParticles = []; // 波形粒子效果数组 +let waveformResistance = 0.15; // 波形抵抗力(0-1,越高越难抚平) +let lastMouseX = 0; // 上一帧鼠标X位置(用于检测拖动速度) +let lastMouseY = 0; // 上一帧鼠标Y位置 +let mouseDragSpeed = 0; // 鼠标拖动速度 + +// 文字区域 +let textAreaMargin = 100; // 文字区域边距 +let textAreaX = 0; // 文字区域X坐标 +let textAreaY = 0; // 文字区域Y坐标 +let textAreaWidth = 0; // 文字区域宽度 +let textAreaHeight = 0; // 文字区域高度 + +// 字符集 +let chineseChars = '的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞'; +let chineseWords = ['不安', '紧张', '焦虑', '担忧', '烦躁', '恐慌', '恐惧', '绝望', '痛苦', '悲伤', '愤怒', '孤独', '无助', '迷茫', '困惑', '压抑', '沉重', '疲惫', '空虚', '失落']; + +// 连接状态跟踪 +let previousConnections = new Map(); // 上一帧的连接状态映射(用于检测连接变化) +let effectDisplays = []; // 效果显示对象数组(用于传递效果) +let initializationComplete = false; // 初始化是否完成 +let initializationFrames = 0; // 初始化帧数计数 + +// 词组模式 +let phraseMode = true; // 是否启用词组模式 +let phraseGroups = []; // 词组分组数组 + +// ==================== 初始化 ==================== +function setup() { + createCanvas(windowWidth, windowHeight); + textAlign(CENTER, CENTER); + + textAreaX = textAreaMargin; + textAreaY = textAreaMargin; + textAreaWidth = width - textAreaMargin * 2; + textAreaHeight = height - textAreaMargin * 2; + + for (let i = 0; i < floatingCount; i++) { + floatingTextParticles.push(new FloatingText( + random(textAreaX, textAreaX + textAreaWidth), + random(textAreaY, textAreaY + textAreaHeight) + )); + } + for (let i = 0; i < candidateCount; i++) { + candidateParticles.push(new CandidateParticle( + random(textAreaX, textAreaX + textAreaWidth), + random(textAreaY, textAreaY + textAreaHeight) + )); + } + + selectRedParticles(); +} + +// ==================== 主循环 ==================== +/** + * 主绘制循环:更新和渲染所有游戏元素 + */ +function draw() { + background(20, 25, 35); + + // 初始化检查:等待一定帧数后再开始检测连接变化 + if (!initializationComplete) { + initializationFrames++; + if (initializationFrames >= 30) { + initializationComplete = true; + } + } + + // 更新词组分组 + updatePhraseGroups(); + + // 检测连接变化(只在初始化完成后进行) + if (initializationComplete) { + detectConnectionChanges(); + } else { + // 初始化阶段:建立初始连接状态(不触发效果) + if (initializationFrames >= 20) { + let currentConnections = new Map(); + for (let i = 0; i < candidateParticles.length; i++) { + for (let j = i + 1; j < candidateParticles.length; j++) { + let distance = dist(candidateParticles[i].x, candidateParticles[i].y, + candidateParticles[j].x, candidateParticles[j].y); + if (distance < connectionDistance) { + let connectionKey = `${min(i, j)}-${max(i, j)}`; + currentConnections.set(connectionKey, { + p1: candidateParticles[i], + p2: candidateParticles[j], + d: distance + }); + } + } + } + previousConnections = currentConnections; + } + } + + // 更新和显示背景浮动文字粒子 + for (let particle of floatingTextParticles) { + particle.update(); + particle.display(); + } + + // 更新和显示候选粒子 + for (let particle of candidateParticles) { + // 如果游戏完成且不是红色粒子,停止移动 + if (isCompleted && !particle.isRed && completionPhase !== 'none') { + particle.vx = 0; + particle.vy = 0; + } else { + particle.update(); + // 在正常游戏阶段应用涟漪和排斥效果 + if (completionPhase === 'none') { + particle.applyRippleAndRepel(); + } + } + particle.display(); + } + + // 更新和显示涟漪效果 + for (let i = rippleEffects.length - 1; i >= 0; i--) { + rippleEffects[i].update(); + rippleEffects[i].display(); + if (rippleEffects[i].alpha <= 0) rippleEffects.splice(i, 1); + } + + // 更新和显示排斥爆发效果 + for (let i = repelBursts.length - 1; i >= 0; i--) { + repelBursts[i].update(); + repelBursts[i].display(); + if (repelBursts[i].alpha <= 0) repelBursts.splice(i, 1); + } + + // 更新和显示效果传递(只在未完成或排列阶段) + if (completionPhase === 'none' || completionPhase === 'arranging') { + for (let i = effectDisplays.length - 1; i >= 0; i--) { + if (!effectDisplays[i].update()) { + // 效果结束,清理并移除 + effectDisplays[i].cleanup(); + effectDisplays.splice(i, 1); + } else { + // 效果仍在进行,显示效果 + effectDisplays[i].display(); + } + } + } else { + // 完成阶段:清理所有效果 + for (let i = effectDisplays.length - 1; i >= 0; i--) { + effectDisplays[i].cleanup(); + effectDisplays.splice(i, 1); + } + } + + // 绘制连接线 + drawConnections(); + + // 检测完成状态:所有红色粒子是否连接且与蓝色粒子分离 + let newCompleted = checkRedParticlesConnected(); + if (newCompleted && !isCompleted) { + isCompleted = true; + startCompletionSequence(); + } + + // 更新完成动画 + if (isCompleted) { + updateCompletionAnimation(); + } + + // 绘制波形(在完成阶段) + if (completionPhase === 'revealing' || completionPhase === 'completed') { + drawWaveform(); + } + + // 显示波形交互提示和UI + if (completionPhase === 'completed') { + displayWaveformInteraction(); + } +} + +// ==================== 连接检测 ==================== +/** + * 检测粒子之间的连接变化,并在红色粒子和蓝色粒子新建立连接时触发效果传递 + * 此函数会更新每个粒子的connections数组,记录真正连接的粒子 + */ +function detectConnectionChanges() { + let currentConnections = new Map(); + + // 遍历所有候选粒子对,检测连接关系 + for (let i = 0; i < candidateParticles.length; i++) { + let particle1 = candidateParticles[i]; + particle1.connections = []; // 重置连接数组 + + for (let j = i + 1; j < candidateParticles.length; j++) { + let particle2 = candidateParticles[j]; + let distance = dist(particle1.x, particle1.y, particle2.x, particle2.y); + + // 如果距离小于连接阈值,则建立连接 + if (distance < connectionDistance) { + let connectionKey = `${min(i, j)}-${max(i, j)}`; + currentConnections.set(connectionKey, { + p1: particle1, + p2: particle2, + d: distance + }); + + // 在双方的connections数组中记录连接关系 + particle1.connections.push(particle2); + particle2.connections.push(particle1); + } + } + } + + // 检测新建立的连接(红色粒子与蓝色粒子之间的连接) + if (initializationComplete && previousConnections.size > 0) { + for (let [connectionKey, connection] of currentConnections) { + // 如果这是一个新建立的连接 + if (!previousConnections.has(connectionKey)) { + let particle1 = connection.p1; + let particle2 = connection.p2; + + // 检查是否是红色粒子与蓝色粒子的新连接 + // 条件:一个是红色粒子(原始红色),另一个是蓝色粒子 + let isRedToBlueConnection = + (particle1.isRed && !particle2.isRed && !particle2.isOrange && + !particle1.isStatic && !particle1.hasEffect && particle1.originalIsRed === true) || + (!particle1.isRed && !particle1.isOrange && particle2.isRed && + !particle2.isStatic && !particle2.hasEffect && particle2.originalIsRed === true); + + if (isRedToBlueConnection) { + let redParticle = particle1.isRed ? particle1 : particle2; + let blueParticle = particle1.isRed ? particle2 : particle1; + + // 确保蓝色粒子可以接收效果 + if (!blueParticle.isStatic && !blueParticle.hasEffect && !blueParticle.isOrange) { + triggerEffectPropagation(redParticle, blueParticle); + } + } + } + } + } + + // 更新上一帧的连接状态 + previousConnections = currentConnections; +} + +/** + * 触发效果传递(从红色粒子传递到蓝色粒子) + * @param {CandidateParticle} redParticle - 红色粒子(效果源) + * @param {CandidateParticle} blueParticle - 蓝色粒子(效果目标) + */ +function triggerEffectPropagation(redParticle, blueParticle) { + if (blueParticle) { + effectDisplays.push(new EffectDisplay(redParticle, blueParticle)); + } +} + +// ==================== 绘制连接线 ==================== +function drawConnections() { + // 蓝色粒子之间的连线 + if (completionPhase === 'none' || completionPhase === 'arranging') { + strokeWeight(1.2); + for (let i = 0; i < candidateParticles.length; i++) { + for (let j = i + 1; j < candidateParticles.length; j++) { + if (candidateParticles[i].isRed && candidateParticles[j].isRed) continue; + + let d = dist(candidateParticles[i].x, candidateParticles[i].y, candidateParticles[j].x, candidateParticles[j].y); + if (d < connectionDistance) { + let alpha = map(d, 0, connectionDistance, 180, 0) * (fadeOutAlpha / 255); + stroke(100, 200, 255, alpha); + line(candidateParticles[i].x, candidateParticles[i].y, candidateParticles[j].x, candidateParticles[j].y); + } + } + } + } + + // 红色粒子之间的连线 + if (completionPhase === 'none' || completionPhase === 'arranging') { + let redParticles = candidateParticles.filter(p => p.isRed); + strokeWeight(2.5); + for (let i = 0; i < redParticles.length; i++) { + for (let j = i + 1; j < redParticles.length; j++) { + let d = dist(redParticles[i].x, redParticles[i].y, redParticles[j].x, redParticles[j].y); + if (d < connectionDistance) { + let alpha = map(d, 0, connectionDistance, 255, 100); + stroke(255, 100, 100, alpha); + line(redParticles[i].x, redParticles[i].y, redParticles[j].x, redParticles[j].y); + } + } + } + } +} + +// ==================== 完成序列 ==================== +function startCompletionSequence() { + completionPhase = 'arranging'; + completionTimer = 0; + fadeOutAlpha = 255; + + // 重置波形和平静进度 + calmProgress = 0; + waveformPoints = []; + + orderedRedParticles = getOrderedRedParticles(); + let displayLength = min(orderedRedParticles.length, targetSentence.length); + + let totalWidth = displayLength * (fontSize + 20); + let startX = (width - totalWidth) / 2 + fontSize / 2; + let centerY = height / 2; + + targetPositions = []; + for (let i = 0; i < displayLength; i++) { + targetPositions.push({ + x: startX + i * (fontSize + 20), + y: centerY + }); + } + + if (orderedRedParticles.length > displayLength) { + orderedRedParticles = orderedRedParticles.slice(0, displayLength); + } + + effectDisplays = []; + + for (let p of candidateParticles) { + if (!p.isRed) { + p.vx = 0; + p.vy = 0; + if (p.isOrange) { + p.hasEffect = false; + p.changeSpeedMultiplier = 1; + p.vibrationOffsetX = 0; + p.vibrationOffsetY = 0; + } + } + } +} + +function getOrderedRedParticles() { + let redParticles = candidateParticles.filter(p => p.isRed); + if (redParticles.length === 0) return []; + if (redParticles.length === 1) return redParticles; + + let startIdx = 0; + let minX = redParticles[0].x; + for (let i = 1; i < redParticles.length; i++) { + if (redParticles[i].x < minX) { + minX = redParticles[i].x; + startIdx = i; + } + } + + let ordered = []; + let visited = new Set(); + let stack = [startIdx]; + visited.add(startIdx); + + while (stack.length > 0) { + let currentIdx = stack.pop(); + ordered.push(redParticles[currentIdx]); + + for (let i = 0; i < redParticles.length; i++) { + if (!visited.has(i)) { + let d = dist(redParticles[currentIdx].x, redParticles[currentIdx].y, + redParticles[i].x, redParticles[i].y); + if (d < connectionDistance) { + visited.add(i); + stack.push(i); + } + } + } + } + + return ordered; +} + +/** + * 更新完成动画:包括文字揭示、抖动效果和波形交互 + */ +function updateCompletionAnimation() { + completionTimer++; + + // 更新波形交互 + if (completionPhase === 'revealing' || completionPhase === 'completed') { + updateWaveform(); + } + + if (completionPhase === 'arranging') { + let allArrived = true; + for (let i = 0; i < orderedRedParticles.length; i++) { + let p = orderedRedParticles[i]; + let target = targetPositions[i]; + + p.x = lerp(p.x, target.x, 0.1); + p.y = lerp(p.y, target.y, 0.1); + + if (dist(p.x, p.y, target.x, target.y) > 1) { + allArrived = false; + } + } + + fadeOutAlpha = max(0, fadeOutAlpha - 5); + + if (allArrived && completionTimer > 30) { + completionPhase = 'revealing'; + completionTimer = 0; + + // 初始化波形(如果还没有初始化) + if (waveformPoints.length === 0) { + initializeWaveform(); + } + + // 初始化抖动和漂浮效果 + for (let p of orderedRedParticles) { + p.changeTimer = 5; + p.changeSpeedMultiplier = 10; + p.isStatic = false; + // 初始化抖动参数 + p.anxietyShakePhase = random(TWO_PI); + p.anxietyShakeIntensity = random(2, 4); + p.anxietyFloatPhase = random(TWO_PI); + p.anxietyFloatSpeed = random(0.02, 0.04); + } + } + } else if (completionPhase === 'revealing') { + let revealDelay = 15; + let currentIndex = floor(completionTimer / revealDelay); + + for (let i = 0; i < orderedRedParticles.length; i++) { + let p = orderedRedParticles[i]; + + if (i < currentIndex) { + if (i < targetSentence.length) { + p.char = targetSentence.charAt(i); + } + p.isStatic = false; // 允许抖动 + p.changeSpeedMultiplier = 1; + p.phraseGroup = null; + // 更新抖动和漂浮 + updateAnxietyEffect(p); + } else if (i === currentIndex) { + p.changeTimer = max(0, p.changeTimer - 1); + p.changeSpeedMultiplier = 20; + p.isStatic = false; + p.phraseGroup = null; + + if (completionTimer % revealDelay >= revealDelay - 5) { + if (i < targetSentence.length) { + p.char = targetSentence.charAt(i); + } + // 初始化抖动参数 + if (p.anxietyShakePhase === undefined) { + p.anxietyShakePhase = random(TWO_PI); + p.anxietyShakeIntensity = random(2, 4); + p.anxietyFloatPhase = random(TWO_PI); + p.anxietyFloatSpeed = random(0.02, 0.04); + } + } else { + if (p.changeTimer <= 0) { + p.char = chineseChars.charAt(floor(random(chineseChars.length))); + p.changeTimer = 2; + } + } + } else { + p.changeTimer = max(0, p.changeTimer - 1); + p.changeSpeedMultiplier = 20; + p.isStatic = false; + p.phraseGroup = null; + + if (p.changeTimer <= 0) { + p.char = chineseChars.charAt(floor(random(chineseChars.length))); + p.changeTimer = 2; + } + } + } + + if (currentIndex >= orderedRedParticles.length) { + completionPhase = 'completed'; + for (let i = 0; i < orderedRedParticles.length && i < targetSentence.length; i++) { + orderedRedParticles[i].char = targetSentence.charAt(i); + orderedRedParticles[i].isStatic = false; // 允许抖动 + orderedRedParticles[i].changeSpeedMultiplier = 1; + orderedRedParticles[i].phraseGroup = null; + // 确保有抖动参数 + if (orderedRedParticles[i].anxietyShakePhase === undefined) { + orderedRedParticles[i].anxietyShakePhase = random(TWO_PI); + orderedRedParticles[i].anxietyShakeIntensity = random(2, 4); + orderedRedParticles[i].anxietyFloatPhase = random(TWO_PI); + orderedRedParticles[i].anxietyFloatSpeed = random(0.02, 0.04); + } + } + } + } else if (completionPhase === 'completed') { + // 在完成阶段,更新所有文字的抖动和漂浮效果 + for (let p of orderedRedParticles) { + updateAnxietyEffect(p); + } + + // 检查所有文字是否都已消散完成 + let allDissolved = true; + for (let p of orderedRedParticles) { + let dissolveProgress = p.dissolveProgress !== undefined ? p.dissolveProgress : 0; + if (dissolveProgress < 1) { + allDissolved = false; + break; + } + } + + // 如果所有文字都已消散,触发游戏结束 + if (allDissolved && orderedRedParticles.length > 0) { + // 可以在这里添加结束动画或回调 + // 例如:延迟一段时间后重置游戏或显示结束画面 + if (completionTimer > 60) { // 等待1秒(假设60fps)后重置 + resetGame(); + } + } + } +} + +/** + * 更新焦虑效果(抖动和漂浮)- 根据对应波形点的抚平程度 + * @param {CandidateParticle} particle - 要更新的粒子 + */ +function updateAnxietyEffect(particle) { + if (particle.anxietyShakePhase === undefined) { + particle.anxietyShakePhase = random(TWO_PI); + particle.anxietyShakeIntensity = random(2, 4); + particle.anxietyFloatPhase = random(TWO_PI); + particle.anxietyFloatSpeed = random(0.02, 0.04); + } + + // 获取对应波形点的抚平程度(如果没有关联,使用整体平静进度) + let localCalmProgress = particle.calmProgress !== undefined ? particle.calmProgress : calmProgress; + + // 根据对应波形点的抚平程度减少抖动强度(0 = 完全抖动,1 = 完全平静) + let shakeIntensity = particle.anxietyShakeIntensity * (1 - localCalmProgress); + let floatAmplitude = 3 * (1 - localCalmProgress); + + // 如果抚平程度高,停止抖动更新 + if (localCalmProgress < 0.9) { + // 更新抖动相位 + particle.anxietyShakePhase += 0.3; + particle.anxietyFloatPhase += particle.anxietyFloatSpeed; + } + + // 计算抖动偏移(随机方向的小幅度抖动) + particle.anxietyShakeX = cos(particle.anxietyShakePhase) * shakeIntensity + + sin(particle.anxietyShakePhase * 1.7) * shakeIntensity * 0.5; + particle.anxietyShakeY = sin(particle.anxietyShakePhase * 1.3) * shakeIntensity + + cos(particle.anxietyShakePhase * 0.9) * shakeIntensity * 0.5; + + // 计算漂浮偏移(缓慢的上下浮动) + particle.anxietyFloatY = sin(particle.anxietyFloatPhase) * floatAmplitude; +} + +/** + * 初始化波形数据(与文字位置对应) + */ +function initializeWaveform() { + waveformPoints = []; + waveformParticles = []; + + // 确保orderedRedParticles已经初始化 + if (orderedRedParticles.length === 0) return; + + // 波形显示在文字位置(与文字重叠) + waveformBaseY = height / 2; // 与文字中心对齐 + + // 为每个文字粒子创建一个对应的波形点 + for (let i = 0; i < orderedRedParticles.length; i++) { + let particle = orderedRedParticles[i]; + let x = particle.x; + + // 异常波形:使用Perlin噪声生成真实的噪波效果 + // 使用多个频率的噪声叠加,产生更自然的噪波 + let noiseScale1 = 0.05; // 低频噪声(大范围变化) + let noiseScale2 = 0.15; // 中频噪声 + let noiseScale3 = 0.4; // 高频噪声(细节变化) + + // 使用p5.js的noise函数生成平滑的噪声值 + let noiseValue1 = noise(x * noiseScale1, frameCount * 0.01); + let noiseValue2 = noise(x * noiseScale2, frameCount * 0.02 + 100); + let noiseValue3 = noise(x * noiseScale3, frameCount * 0.03 + 200); + + // 将噪声值从[0,1]映射到[-1,1],并叠加多层 + noiseValue1 = map(noiseValue1, 0, 1, -1, 1); + noiseValue2 = map(noiseValue2, 0, 1, -1, 1) * 0.6; + noiseValue3 = map(noiseValue3, 0, 1, -1, 1) * 0.3; + + // 叠加多层噪声,产生更复杂的噪波 + let combinedNoise = noiseValue1 + noiseValue2 + noiseValue3; + combinedNoise = constrain(combinedNoise, -1.5, 1.5); + + // 添加一些随机性,使每个点的噪波特征不同 + let noiseAmplitude = random(0.7, 1.0); + let noiseOffset = random(-0.2, 0.2); + let initialY = waveformBaseY + (combinedNoise + noiseOffset) * noiseAmplitude * waveformAmplitude; + + // 抚平后的目标位置:规律的波形(正弦波) + let targetWavePhase = i * 0.3; // 波形相位 + let targetWaveAmplitude = waveformAmplitude * 0.3; // 规律波形的振幅(较小) + let targetY = waveformBaseY + sin(targetWavePhase) * targetWaveAmplitude; + + // 计算每个点的异常程度(距离规律波形的距离) + let anomalyAmount = abs(initialY - targetY) / waveformAmplitude; + + waveformPoints.push({ + x: x, + y: initialY, // 初始Y坐标(噪波) + targetY: targetY, // 目标位置(规律的波形) + smoothedY: initialY, // 平滑后的Y坐标 + noiseOffset: noiseOffset, // 噪声偏移(用于动态噪波) + noiseScale1: noiseScale1, // 噪声缩放1 + noiseScale2: noiseScale2, // 噪声缩放2 + noiseScale3: noiseScale3, // 噪声缩放3 + noiseAmplitude: noiseAmplitude, // 噪声振幅 + noiseTime: random(0, 1000), // 噪声时间偏移(使每个点有不同的时间相位) + resistance: 0.1 + anomalyAmount * 0.25, // 抵抗力(异常程度越高,抵抗力越强) + calmAmount: 0, // 当前被抚平的程度(0-1) + isBeingCalmed: false, // 是否正在被抚平 + calmTimer: 0, // 抚平计时器 + particle: particle, // 关联的文字粒子 + particleIndex: i // 粒子索引 + }); + + // 在文字粒子上添加波形关联 + particle.waveformPoint = waveformPoints[waveformPoints.length - 1]; + particle.calmProgress = 0; // 文字粒子的平静进度 + particle.dissolveProgress = 0; // 文字粒子的消散进度 + particle.isCalmed = false; // 是否已被抚平 + } + + // 初始化鼠标位置 + lastMouseX = mouseX; + lastMouseY = mouseY; +} + +/** + * 更新波形:根据平静进度平滑波形,并处理鼠标交互(带抵抗力和愈合机制) + */ +function updateWaveform() { + if (waveformPoints.length === 0) { + initializeWaveform(); + } + + // 计算鼠标拖动速度 + mouseDragSpeed = dist(mouseX, mouseY, lastMouseX, lastMouseY); + lastMouseX = mouseX; + lastMouseY = mouseY; + + // 更新粒子效果 + for (let i = waveformParticles.length - 1; i >= 0; i--) { + let p = waveformParticles[i]; + p.x += p.vx; + p.y += p.vy; + p.vy += 0.1; // 重力 + p.alpha -= 3; + p.size *= 0.98; + + if (p.alpha <= 0 || p.size < 0.5) { + waveformParticles.splice(i, 1); + } + } + + // 更新每个波形点 + for (let point of waveformPoints) { + // 同步更新波形点的X位置(跟随文字位置) + if (point.particle) { + point.x = point.particle.x; + // 更新规律波形的目标位置(跟随文字位置) + let targetWavePhase = point.particleIndex * 0.3; + let targetWaveAmplitude = waveformAmplitude * 0.3; + point.targetY = waveformBaseY + sin(targetWavePhase) * targetWaveAmplitude; + } + + // 重置状态 + point.isBeingCalmed = false; + + // 如果未抚平,添加动态噪波效果(表现异常波形的"生命力") + if (point.calmAmount < 0.9) { + // 使用Perlin噪声生成动态噪波 + let currentTime = frameCount * 0.01 + point.noiseTime * 0.001; + + // 生成多层噪声 + let noiseValue1 = noise(point.x * point.noiseScale1, currentTime); + let noiseValue2 = noise(point.x * point.noiseScale2, currentTime * 1.5 + 100); + let noiseValue3 = noise(point.x * point.noiseScale3, currentTime * 2 + 200); + + // 映射并叠加 + noiseValue1 = map(noiseValue1, 0, 1, -1, 1); + noiseValue2 = map(noiseValue2, 0, 1, -1, 1) * 0.6; + noiseValue3 = map(noiseValue3, 0, 1, -1, 1) * 0.3; + + let dynamicNoise = noiseValue1 + noiseValue2 + noiseValue3 + point.noiseOffset; + dynamicNoise = constrain(dynamicNoise, -1.5, 1.5); + + // 根据抚平程度减少噪波强度(抚平程度越高,噪波越弱) + let noiseIntensity = (1 - point.calmAmount) * waveformAmplitude * point.noiseAmplitude; + point.smoothedY = point.y + dynamicNoise * noiseIntensity; + } + + // 处理鼠标交互:如果鼠标在附近,尝试抚平该区域的波形 + if (mouseIsPressed && mouseButton === LEFT) { + let distToMouse = dist(mouseX, mouseY, point.x, point.smoothedY); + if (distToMouse < interactionRadius) { + // 计算影响强度(距离越近影响越大,拖动速度越快影响越大) + let distanceInfluence = map(distToMouse, 0, interactionRadius, 1, 0); + let speedBonus = min(1, mouseDragSpeed / 5); // 拖动速度加成 + let influence = distanceInfluence * (0.5 + speedBonus * 0.5); + + // 标记为正在被抚平 + point.isBeingCalmed = true; + point.calmTimer++; + + // 需要持续按住才能抚平(抵抗机制) + // 抵抗力越高,需要按住的时间越长 + let requiredTime = 30 + point.resistance * 60; // 需要按住30-90帧 + let calmProgressLocal = min(1, point.calmTimer / requiredTime); + + // 应用抚平效果(考虑抵抗力) + let calmStrength = influence * (1 - point.resistance) * 0.15; + + // 计算目标位置(从噪波位置向规律波形位置过渡) + let targetY = lerp(point.y, point.targetY, calmProgressLocal); + point.smoothedY = lerp(point.smoothedY, targetY, calmStrength); + + // 更新抚平程度(一旦抚平,就不会恢复) + if (calmProgressLocal > point.calmAmount) { + point.calmAmount = calmProgressLocal; + } + + // 如果完全抚平,标记为已抚平 + if (point.calmAmount >= 0.95) { + point.calmAmount = 1; + point.smoothedY = point.targetY; // 强制设置为目标位置 + } + + // 如果成功抚平了一点,生成粒子效果 + if (calmProgressLocal > 0.3 && random() > 0.7) { + for (let j = 0; j < 2; j++) { + waveformParticles.push({ + x: point.x, + y: point.smoothedY, + vx: random(-1, 1), + vy: random(-2, -0.5), + alpha: 200, + size: random(2, 4), + color: [150, 200, 255] + }); + } + } + + // 增加整体平静进度(需要持续交互) + if (calmProgressLocal > 0.5) { + calmProgress = min(1, calmProgress + influence * 0.0005); + } + } else { + // 不在交互范围内,保持当前状态(不重置) + } + } + + // 关键:如果calmAmount接近1,强制让波形显示为规律波形 + if (point.calmAmount > 0.8) { + // 高度抚平的点,强制向目标位置(规律波形)靠拢 + let forceStrength = map(point.calmAmount, 0.8, 1, 0.1, 0.5); + point.smoothedY = lerp(point.smoothedY, point.targetY, forceStrength); + } + + // 如果完全抚平,直接设置为目标位置(规律波形) + if (point.calmAmount >= 1) { + point.smoothedY = point.targetY; + } + + // 限制波形点不会完全超出范围(抚平的点限制更严格) + let maxDeviation = waveformAmplitude * (1 - calmProgress * 0.5) * (1 - point.calmAmount * 0.8); + point.smoothedY = constrain(point.smoothedY, + waveformBaseY - maxDeviation, + waveformBaseY + maxDeviation); + + // 同步更新对应文字粒子的平静进度 + if (point.particle) { + let oldCalmProgress = point.particle.calmProgress || 0; + point.particle.calmProgress = point.calmAmount; + + // 如果完全抚平(calmAmount >= 0.95),标记为已稳定 + if (point.calmAmount >= 0.95 && !point.particle.isCalmed) { + point.particle.isCalmed = true; + // 停止字符变化和抖动 + point.particle.isStatic = true; + point.particle.changeSpeedMultiplier = 0; + // 初始化消散进度(从0开始,等待波形完全稳定) + point.particle.dissolveProgress = 0; + point.particle.stableTimer = 0; // 稳定计时器 + } + + // 如果已抚平,检查波形是否完全稳定(接近目标位置) + if (point.particle.isCalmed) { + // 检查波形是否稳定(距离目标位置很近) + let distanceToTarget = abs(point.smoothedY - point.targetY); + let stabilityThreshold = waveformAmplitude * 0.05; // 稳定阈值 + + if (distanceToTarget < stabilityThreshold) { + // 波形已稳定,开始增加稳定计时器 + if (point.particle.stableTimer === undefined) { + point.particle.stableTimer = 0; + } + point.particle.stableTimer++; + + // 稳定一段时间后(例如30帧),开始消散 + if (point.particle.stableTimer > 30 && point.particle.dissolveProgress < 1) { + // 消散速度:根据波形稳定程度决定 + let dissolveSpeed = 0.02 + (point.calmAmount - 0.95) * 0.15; // 0.02到0.07之间 + point.particle.dissolveProgress = min(1, point.particle.dissolveProgress + dissolveSpeed); + } + } else { + // 波形还未完全稳定,重置稳定计时器 + point.particle.stableTimer = 0; + } + } + } + } + + // 如果平静进度达到1,波形完全平滑 + if (calmProgress >= 1) { + for (let point of waveformPoints) { + point.smoothedY = lerp(point.smoothedY, point.targetY, 0.05); + point.calmAmount = 1; + } + } +} + +/** + * 绘制波形(带粒子效果和动态颜色) + */ +function drawWaveform() { + if (waveformPoints.length === 0) return; + + // 绘制粒子效果 + for (let p of waveformParticles) { + fill(p.color[0], p.color[1], p.color[2], p.alpha); + noStroke(); + ellipse(p.x, p.y, p.size); + + // 添加光晕效果 + fill(p.color[0], p.color[1], p.color[2], p.alpha * 0.3); + ellipse(p.x, p.y, p.size * 2); + } + + // 绘制波形线(根据抚平程度改变颜色:红色=异常,蓝色=正常) + noFill(); + strokeWeight(2.5); + + beginShape(); + for (let i = 0; i < waveformPoints.length; i++) { + let point = waveformPoints[i]; + + // 同步更新波形点的X位置(跟随文字位置) + if (point.particle) { + point.x = point.particle.x; + } + + // 获取对应文字的消散进度 + let particleDissolve = point.particle && point.particle.dissolveProgress !== undefined ? + point.particle.dissolveProgress : 0; + + // 根据抚平程度混合颜色:红色(255,100,100) -> 蓝色(150,200,255) + let redValue = lerp(255, 150, point.calmAmount); + let greenValue = lerp(100, 200, point.calmAmount); + let blueValue = lerp(100, 255, point.calmAmount); + let alphaValue = 200 * (1 - calmProgress * 0.3) * (1 - particleDissolve); + + // 如果文字完全消散,不绘制该点 + if (particleDissolve < 1) { + stroke(redValue, greenValue, blueValue, alphaValue); + vertex(point.x, point.smoothedY); + } + } + endShape(); + + // 绘制波形点(显示抵抗力和抚平进度) + for (let i = 0; i < waveformPoints.length; i += 3) { + let point = waveformPoints[i]; + + // 获取对应文字的消散进度 + let particleDissolve = point.particle && point.particle.dissolveProgress !== undefined ? + point.particle.dissolveProgress : 0; + + // 如果文字完全消散,不绘制 + if (particleDissolve >= 1) continue; + + // 如果正在被抚平,显示进度 + if (point.isBeingCalmed && point.calmTimer > 0) { + let requiredTime = 30 + point.resistance * 60; + let progress = min(1, point.calmTimer / requiredTime); + + // 绘制进度环 + push(); + translate(point.x, point.smoothedY); + noFill(); + stroke(150, 200, 255, 150 * (1 - particleDissolve)); + strokeWeight(1); + arc(0, 0, 8, 8, -HALF_PI, -HALF_PI + TWO_PI * progress); + pop(); + } + + // 绘制波形点(根据抚平程度和消散进度) + let pointAlpha = 150 * (1 - point.calmAmount) * (1 - calmProgress * 0.5) * (1 - particleDissolve); + if (pointAlpha > 10) { + let redValue = lerp(255, 150, point.calmAmount); + let greenValue = lerp(100, 200, point.calmAmount); + let blueValue = lerp(100, 255, point.calmAmount); + fill(redValue, greenValue, blueValue, pointAlpha); + noStroke(); + ellipse(point.x, point.smoothedY, 3); + } + } + + // 绘制交互反馈(如果鼠标按下) + if (mouseIsPressed && mouseButton === LEFT) { + // 内圈(交互范围) + fill(150, 200, 255, 30); + noStroke(); + ellipse(mouseX, mouseY, interactionRadius * 2); + + // 外圈(视觉反馈) + stroke(150, 200, 255, 150); + strokeWeight(2); + noFill(); + ellipse(mouseX, mouseY, interactionRadius * 2); + + // 根据拖动速度显示额外反馈 + if (mouseDragSpeed > 2) { + stroke(150, 200, 255, 100); + strokeWeight(1); + noFill(); + ellipse(mouseX, mouseY, interactionRadius * 2.5); + } + } + + // 绘制基准线(目标线) + if (calmProgress < 0.9) { + stroke(150, 200, 255, 50 * (1 - calmProgress)); + strokeWeight(1); + line(width * 0.2, waveformBaseY, width * 0.8, waveformBaseY); + } +} + +// ==================== 游戏逻辑 ==================== +function updatePhraseGroups() { + phraseGroups = []; + let visited = new Set(); + + for (let i = 0; i < candidateParticles.length; i++) { + if (visited.has(i)) continue; + + let group = []; + let stack = [i]; + visited.add(i); + + while (stack.length > 0) { + let currentIdx = stack.pop(); + let current = candidateParticles[currentIdx]; + group.push(current); + + for (let j = 0; j < candidateParticles.length; j++) { + if (j === currentIdx || visited.has(j)) continue; + let d = dist(current.x, current.y, candidateParticles[j].x, candidateParticles[j].y); + if (d < connectionDistance) { + visited.add(j); + stack.push(j); + } + } + } + + if (group.length >= 2) { + phraseGroups.push(group); + for (let p of group) { + p.phraseGroup = group; + p.phraseIndex = group.indexOf(p); + } + } + } +} + +function selectRedParticles() { + let selected = []; + let minDistance = min(textAreaWidth, textAreaHeight) / (redParticleCount * 0.8); + + let shuffled = [...candidateParticles]; + for (let i = shuffled.length - 1; i > 0; i--) { + let j = floor(random(i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; + } + + for (let particle of shuffled) { + if (selected.length >= redParticleCount) break; + + let tooClose = false; + for (let selectedP of selected) { + if (dist(particle.x, particle.y, selectedP.x, selectedP.y) < minDistance) { + tooClose = true; + break; + } + } + + if (!tooClose || selected.length === 0) { + particle.isRed = true; + particle.originalIsRed = true; + selected.push(particle); + } + } + + let remaining = redParticleCount - selected.length; + for (let i = 0; i < remaining && i < shuffled.length; i++) { + if (!shuffled[i].isRed) { + shuffled[i].isRed = true; + shuffled[i].originalIsRed = true; + selected.push(shuffled[i]); + } + } +} + +function checkRedParticlesConnected() { + let redParticles = candidateParticles.filter(p => p.isRed); + if (redParticles.length === 0) return false; + if (redParticles.length === 1) return true; + + let visited = new Set(); + let stack = [0]; + visited.add(0); + + while (stack.length > 0) { + let current = stack.pop(); + for (let i = 0; i < redParticles.length; i++) { + if (!visited.has(i)) { + let d = dist(redParticles[current].x, redParticles[current].y, + redParticles[i].x, redParticles[i].y); + if (d < connectionDistance) { + visited.add(i); + stack.push(i); + } + } + } + } + + let allRedConnected = visited.size === redParticles.length; + if (!allRedConnected) return false; + + for (let redP of redParticles) { + for (let p of candidateParticles) { + if (p.isRed) continue; + + let d = dist(redP.x, redP.y, p.x, p.y); + if (d < connectionDistance) { + return false; + } + } + } + + return true; +} + +function resetGame() { + isCompleted = false; + completionPhase = 'none'; + completionTimer = 0; + fadeOutAlpha = 255; + orderedRedParticles = []; + targetPositions = []; + effectDisplays = []; + previousConnections = new Map(); + initializationComplete = false; + initializationFrames = 0; + + // 重置波形交互状态 + calmProgress = 0; + waveformPoints = []; + waveformParticles = []; + mouseInteractionActive = false; + lastMouseX = 0; + lastMouseY = 0; + mouseDragSpeed = 0; + + for (let p of candidateParticles) { + p.isRed = false; + p.isOrange = false; + p.hasEffect = false; + p.isStatic = false; + p.originalIsRed = false; + p.changeSpeedMultiplier = 1; + p.vibrationOffsetX = 0; + p.vibrationOffsetY = 0; + p.vx = random(-0.5, 0.5); + p.vy = random(-0.5, 0.5); + p.x = random(textAreaX, textAreaX + textAreaWidth); + p.y = random(textAreaY, textAreaY + textAreaHeight); + p.alpha = p.baseAlpha + p.prob * 105; + p.phraseGroup = null; + p.phraseIndex = 0; + + // 重置波形相关状态 + p.calmProgress = undefined; + p.dissolveProgress = undefined; + p.isCalmed = false; + p.waveformPoint = undefined; + p.stableTimer = undefined; + + // 重置抖动和漂浮参数 + p.anxietyShakePhase = undefined; + p.anxietyShakeIntensity = undefined; + p.anxietyFloatPhase = undefined; + p.anxietyFloatSpeed = undefined; + p.anxietyShakeX = undefined; + p.anxietyShakeY = undefined; + p.anxietyFloatY = undefined; + } + + selectRedParticles(); + rippleEffects = []; + repelBursts = []; +} + +// ==================== UI ==================== +/** + * 显示波形交互提示和UI + */ +function displayWaveformInteraction() { + // 显示提示文字 + let hintY = height / 2 + 180; + fill(200, 220, 255, 180); + textSize(16); + textAlign(CENTER, CENTER); + + // 检查是否有文字正在消散 + let hasDissolving = false; + for (let p of orderedRedParticles) { + if (p.dissolveProgress !== undefined && p.dissolveProgress > 0 && p.dissolveProgress < 1) { + hasDissolving = true; + break; + } + } + + // 检查是否所有文字都已消散 + let allDissolved = true; + for (let p of orderedRedParticles) { + if (p.dissolveProgress === undefined || p.dissolveProgress < 1) { + allDissolved = false; + break; + } + } + + if (allDissolved) { + fill(150, 255, 150, 200); + text("所有文字已消散,情绪已完全平静", width / 2, hintY); + } else if (hasDissolving) { + fill(150, 220, 255, 200); + text("波形已稳定,文字正在消散...", width / 2, hintY); + } else if (calmProgress < 1) { + if (mouseIsPressed && mouseButton === LEFT) { + text("持续按住并拖动,抚平波形(波形稳定后文字会消散)", width / 2, hintY); + } else { + text("按住鼠标左键持续拖动,抚平异常波形(波形稳定后文字会消散)", width / 2, hintY); + } + + // 显示平静进度条 + let progressBarX = width / 2; + let progressBarY = hintY + 25; + let progressBarWidth = 200; + let progressBarHeight = 8; + + // 背景 + fill(50, 50, 70, 150); + rectMode(CENTER); + rect(progressBarX, progressBarY, progressBarWidth, progressBarHeight, 4); + + // 进度 + fill(150, 200, 255, 200); + rectMode(CORNER); + rect(progressBarX - progressBarWidth / 2, progressBarY - progressBarHeight / 2, + progressBarWidth * calmProgress, progressBarHeight, 4); + } else { + fill(150, 255, 150, 200); + text("异常波形已抚平,等待文字消散...", width / 2, hintY); + } +} + +function displayNextButton() { + // 不再显示下一步按钮,游戏会在所有文字消散后自动重置 + // 保留函数以防其他地方调用 +} + +// ==================== 事件处理 ==================== +/** + * 鼠标按下事件 + */ +function mousePressed() { + if (completionPhase === 'completed') { + // 在完成阶段,鼠标交互用于抚平波形(在updateWaveform中处理) + // 游戏会在所有文字消散后自动重置,不需要手动点击按钮 + } + + if (isCompleted && completionPhase !== 'completed') { + return; + } + + // 正常游戏阶段的交互 + if (mouseButton === LEFT) { + repelBursts.push(new RepelBurst(mouseX, mouseY)); + } else if (mouseButton === RIGHT) { + rippleEffects.push(new Ripple(mouseX, mouseY)); + } +} + +/** + * 键盘按下事件 + */ +function keyPressed() { + // 切换词组模式 + if (key === 'p' || key === 'P') { + phraseMode = !phraseMode; + for (let p of candidateParticles) { + p.phraseGroup = null; + p.phraseIndex = 0; + } + } +} + +// ==================== 类定义 ==================== +/** + * 效果显示类:管理从红色粒子到蓝色粒子的效果传递和显示 + * 效果会沿着连接线传播,形成连锁反应 + */ +class EffectDisplay { + /** + * 构造函数:初始化效果传递 + * @param {CandidateParticle} redParticle - 红色粒子(效果源) + * @param {CandidateParticle} startBlueParticle - 起始蓝色粒子(第一个接收效果的粒子) + */ + constructor(redParticle, startBlueParticle) { + this.redParticle = redParticle; // 红色粒子(效果源) + this.startBlueParticle = startBlueParticle; // 起始蓝色粒子 + this.lifeTimer = 20; // 效果持续时间(帧数) + + this.propagatedParticles = []; // 已传播的粒子数组(包含传播信息) + this.propagationQueue = []; // 传播队列(待传播的粒子) + this.propagationDelay = 3; // 传播延迟(帧数) + this.propagationCounter = 0; // 传播计数器 + this.maxPropagationSteps = 8; // 最大传播步数 + this.currentPropagationStep = 0; // 当前传播步数 + + // 从红色粒子开始,找到第一个连接的蓝色粒子并应用效果 + let connectedBlueParticles = this.findConnectedBlues(this.redParticle); + if (connectedBlueParticles.length > 0 && this.currentPropagationStep < this.maxPropagationSteps) { + // 随机选择一个连接的蓝色粒子 + let targetParticle = connectedBlueParticles[floor(random(connectedBlueParticles.length))]; + this.applyEffectToParticle(targetParticle); + + // 记录传播信息 + this.propagatedParticles.push({ + particle: targetParticle, + source: this.redParticle, + step: 1, + pulseProgress: 0 // 脉冲动画进度(0-1) + }); + + // 加入传播队列,用于下一步传播 + this.propagationQueue.push(targetParticle); + this.currentPropagationStep = 1; + } + } + + /** + * 对粒子应用效果(将其变为橙色并添加动画效果) + * @param {CandidateParticle} particle - 要应用效果的粒子 + */ + applyEffectToParticle(particle) { + // 如果粒子已经有效果,则跳过 + if (particle.hasEffect) return; + // 如果是原始红色粒子,则跳过 + if (particle.isRed && particle.originalIsRed === true) return; + + // 标记粒子已有效果 + particle.hasEffect = true; + + // 保存原始状态 + if (particle.originalIsRed === undefined) { + particle.originalIsRed = particle.isRed; + } + particle.originalChangeTimer = particle.changeTimer; + particle.originalX = particle.x; + particle.originalY = particle.y; + + // 设置效果属性 + particle.effectTimer = this.lifeTimer; + particle.isOrange = true; // 变为橙色 + particle.isRed = false; // 不再是红色 + particle.changeSpeedMultiplier = 3; // 字符变化速度加快 + particle.vibrationOffsetX = 0; // 振动偏移X + particle.vibrationOffsetY = 0; // 振动偏移Y + particle.vibrationPhase = random(TWO_PI); // 振动相位(随机) + } + + /** + * 查找与指定粒子真正连接的蓝色粒子(通过连接线连接) + * @param {CandidateParticle} particle - 源粒子 + * @returns {Array} 连接的蓝色粒子数组 + */ + findConnectedBlues(particle) { + let connected = []; + let usedParticles = new Set(); + + // 记录已使用的粒子(红色粒子和已传播的粒子) + usedParticles.add(this.redParticle); + for (let prop of this.propagatedParticles) { + usedParticles.add(prop.particle); + } + + // 只检查粒子connections数组中真正连接的粒子 + // 这样可以确保只在有连接线的粒子之间传递效果 + for (let connectedParticle of particle.connections) { + // 检查条件: + // 1. 粒子还没有效果 + // 2. 粒子不在已使用列表中 + // 3. 粒子不是红色 + // 4. 粒子不是橙色(已受影响的粒子) + if (!connectedParticle.hasEffect && + !usedParticles.has(connectedParticle) && + !connectedParticle.isRed && + !connectedParticle.isOrange) { + connected.push(connectedParticle); + } + } + + return connected; + } + + /** + * 更新效果传播和动画 + * @returns {boolean} 效果是否仍然有效 + */ + update() { + this.lifeTimer--; + this.propagationCounter++; + + // 每隔一定帧数进行一次传播 + if (this.propagationCounter >= this.propagationDelay && + this.currentPropagationStep < this.maxPropagationSteps) { + this.propagationCounter = 0; + + let nextPropagationQueue = []; + + // 从当前传播队列中的每个粒子继续传播 + for (let sourceParticle of this.propagationQueue) { + if (this.currentPropagationStep >= this.maxPropagationSteps) break; + + // 找到与源粒子连接的蓝色粒子(只查找真正有连接线的粒子) + let connectedBlueParticles = this.findConnectedBlues(sourceParticle); + + if (connectedBlueParticles.length > 0) { + // 随机选择一个连接的蓝色粒子 + let targetParticle = connectedBlueParticles[floor(random(connectedBlueParticles.length))]; + this.applyEffectToParticle(targetParticle); + + // 更新传播步数 + this.currentPropagationStep++; + + // 记录传播信息 + this.propagatedParticles.push({ + particle: targetParticle, + source: sourceParticle, + step: this.currentPropagationStep, + pulseProgress: 0 + }); + + // 如果还有传播步数,加入下一轮传播队列 + if (this.currentPropagationStep < this.maxPropagationSteps) { + nextPropagationQueue.push(targetParticle); + } + } + } + + // 更新传播队列 + this.propagationQueue = nextPropagationQueue.length > 0 ? nextPropagationQueue : []; + } + + // 更新所有已传播粒子的效果动画 + for (let propagationInfo of this.propagatedParticles) { + this.updateParticleEffectAnimation(propagationInfo.particle); + // 更新脉冲动画进度 + propagationInfo.pulseProgress += 0.15; + if (propagationInfo.pulseProgress > 1) { + propagationInfo.pulseProgress = 1; + } + } + + // 返回效果是否仍然有效 + return this.lifeTimer > 0; + } + + /** + * 更新粒子的效果动画(振动、透明度、缩放、发光) + * @param {CandidateParticle} particle - 要更新动画的粒子 + */ + updateParticleEffectAnimation(particle) { + if (!particle.hasEffect) return; + + particle.effectTimer--; + + // 振动效果:粒子位置轻微振动 + let vibrationIntensity = 2; + particle.vibrationPhase += 0.4; + particle.vibrationOffsetX = cos(particle.vibrationPhase) * vibrationIntensity; + particle.vibrationOffsetY = sin(particle.vibrationPhase * 1.3) * vibrationIntensity; + + // 透明度动画:周期性变化 + let alphaPhase = (frameCount * 0.1 + particle.vibrationPhase) % (TWO_PI); + particle.effectAlpha = map(sin(alphaPhase), -1, 1, 150, 255); + + // 缩放动画:周期性缩放 + let scalePhase = (frameCount * 0.15 + particle.vibrationPhase) % (TWO_PI); + particle.effectScale = map(sin(scalePhase), -1, 1, 0.95, 1.05); + + // 发光效果:根据透明度计算发光强度 + particle.effectGlow = map(particle.effectAlpha, 150, 255, 15, 25); + } + + /** + * 显示效果传播的视觉效果(连接线、脉冲、箭头) + */ + display() { + strokeWeight(2); + + // 遍历所有已传播的粒子,绘制传播效果 + for (let propagationInfo of this.propagatedParticles) { + if (propagationInfo.source) { + // 计算透明度:根据传播步数和剩余时间 + let stepAlpha = map(propagationInfo.step, 1, this.maxPropagationSteps, 255, 100); + let timeAlpha = map(this.lifeTimer, 0, 20, 0, 255); + let alpha = min(stepAlpha, timeAlpha); + + // 绘制连接线(从源粒子到目标粒子) + stroke(255, 150, 50, alpha * 0.3); + line(propagationInfo.source.x, propagationInfo.source.y, + propagationInfo.particle.x, propagationInfo.particle.y); + + // 绘制脉冲动画(沿着连接线移动的光点) + if (propagationInfo.pulseProgress < 1) { + let pulseX = lerp(propagationInfo.source.x, propagationInfo.particle.x, + propagationInfo.pulseProgress); + let pulseY = lerp(propagationInfo.source.y, propagationInfo.particle.y, + propagationInfo.pulseProgress); + + let pulseSize = map(propagationInfo.pulseProgress, 0, 1, 3, 8); + let pulseAlpha = map(propagationInfo.pulseProgress, 0, 1, 255, 0); + + // 绘制脉冲光点 + noStroke(); + fill(255, 200, 100, pulseAlpha); + ellipse(pulseX, pulseY, pulseSize); + + // 绘制脉冲光晕 + fill(255, 180, 80, pulseAlpha * 0.5); + ellipse(pulseX, pulseY, pulseSize * 2); + } + + // 绘制箭头(在连接线中间,表示传播方向) + if (propagationInfo.pulseProgress > 0.5 && propagationInfo.pulseProgress < 1) { + let arrowX = lerp(propagationInfo.source.x, propagationInfo.particle.x, 0.5); + let arrowY = lerp(propagationInfo.source.y, propagationInfo.particle.y, 0.5); + let angle = atan2(propagationInfo.particle.y - propagationInfo.source.y, + propagationInfo.particle.x - propagationInfo.source.x); + + push(); + translate(arrowX, arrowY); + rotate(angle); + stroke(255, 200, 100, alpha * 0.8); + strokeWeight(1.5); + fill(255, 200, 100, alpha * 0.8); + triangle(0, 0, -8, -4, -8, 4); + pop(); + } + } + } + } + + /** + * 清理效果:移除所有粒子的效果 + */ + cleanup() { + for (let propagationInfo of this.propagatedParticles) { + this.removeEffectFromParticle(propagationInfo.particle); + } + } + + /** + * 从粒子移除效果,恢复原始状态 + * @param {CandidateParticle} particle - 要移除效果的粒子 + */ + removeEffectFromParticle(particle) { + if (!particle.hasEffect) return; + + // 移除效果标记 + particle.hasEffect = false; + + // 恢复颜色状态 + if (particle.isOrange) { + particle.isOrange = false; + particle.isRed = false; + } + + // 恢复属性 + particle.changeSpeedMultiplier = 1; + particle.vibrationOffsetX = 0; + particle.vibrationOffsetY = 0; + + // 恢复原始计时器 + if (particle.originalChangeTimer !== undefined) { + particle.changeTimer = particle.originalChangeTimer; + } + } +} + +class FloatingText { + constructor(x, y) { + this.x = x; + this.y = y; + this.vx = random(-0.3, 0.3); + this.vy = random(-0.3, 0.3); + this.char = this.randomChar(); + this.alpha = random(10, 80); + this.size = random(fontSize - 10, fontSize + 2); + this.changeTimer = int(random(30, 80)); + } + + randomChar() { + return chineseChars.charAt(floor(random(chineseChars.length))); + } + + update() { + this.x += this.vx; + this.y += this.vy; + + if (this.x < textAreaX || this.x > textAreaX + textAreaWidth) { + this.vx *= -1; + this.x = constrain(this.x, textAreaX, textAreaX + textAreaWidth); + } + if (this.y < textAreaY || this.y > textAreaY + textAreaHeight) { + this.vy *= -1; + this.y = constrain(this.y, textAreaY, textAreaY + textAreaHeight); + } + + this.changeTimer--; + if (this.changeTimer <= 0) { + this.char = this.randomChar(); + this.changeTimer = int(random(30, 80)); + } + } + + display() { + let displayAlpha = this.alpha; + if (isCompleted && completionPhase !== 'none') { + displayAlpha = this.alpha * (fadeOutAlpha / 255); + } + + noStroke(); + fill(180, 200, 255, displayAlpha); + textSize(this.size); + text(this.char, this.x, this.y); + } +} + +class CandidateParticle { + constructor(x, y) { + this.x = x; + this.y = y; + this.vx = random(-0.5, 0.5); + this.vy = random(-0.5, 0.5); + this.char = this.randomChar(); + this.baseAlpha = 150; + this.size = fontSize; + this.prob = random(0.1, 1.0); + this.isRed = false; + this.isOrange = false; + this.changeTimer = int(random(30, 80)); + this.phraseGroup = null; + this.phraseIndex = 0; + this.isTemporaryRed = false; + this.tempRedTimer = 0; + this.connections = []; + this.isStatic = false; + this.originalIsRed = false; + this.hasEffect = false; + this.changeSpeedMultiplier = 1; + this.vibrationOffsetX = 0; + this.vibrationOffsetY = 0; + this.vibrationPhase = 0; + } + + randomChar() { + return chineseChars.charAt(floor(random(chineseChars.length))); + } + + randomWord() { + return chineseWords[floor(random(chineseWords.length))]; + } + + update() { + this.x += this.vx; + this.y += this.vy; + + if (this.x < textAreaX || this.x > textAreaX + textAreaWidth) { + this.vx *= -1; + this.x = constrain(this.x, textAreaX, textAreaX + textAreaWidth); + } + if (this.y < textAreaY || this.y > textAreaY + textAreaHeight) { + this.vy *= -1; + this.y = constrain(this.y, textAreaY, textAreaY + textAreaHeight); + } + + this.alpha = this.baseAlpha + this.prob * 105; + this.displaySize = this.size * (0.5 + this.prob); + + this.vx *= 0.95; + this.vy *= 0.95; + + if (!this.isStatic && !this.isCalmed) { + // 获取对应波形点的抚平程度(如果没有关联,使用整体平静进度) + let localCalmProgress = this.calmProgress !== undefined ? this.calmProgress : calmProgress; + + // 如果抚平程度高,停止字符变化 + if (localCalmProgress < 0.7) { + let changeSpeed = this.changeSpeedMultiplier || 1; + // 根据抚平程度减慢变化速度 + changeSpeed *= (1 - localCalmProgress); + this.changeTimer -= changeSpeed; + + if (this.changeTimer <= 0) { + if (phraseMode && this.phraseGroup) { + let word = this.randomWord(); + for (let i = 0; i < this.phraseGroup.length; i++) { + if (!this.phraseGroup[i].isStatic && !this.phraseGroup[i].isCalmed) { + let groupCalmProgress = this.phraseGroup[i].calmProgress !== undefined ? + this.phraseGroup[i].calmProgress : calmProgress; + if (groupCalmProgress < 0.7) { + if (i < word.length) { + this.phraseGroup[i].char = word.charAt(i); + } else { + this.phraseGroup[i].char = this.phraseGroup[i].randomChar(); + } + this.phraseGroup[i].changeTimer = int(random(30, 80)); + } + } + } + } else { + this.char = this.randomChar(); + this.changeTimer = int(random(30, 80)); + } + } + } + } + + if (this.hasEffect) { + if (this.changeSpeedMultiplier === undefined) { + this.changeSpeedMultiplier = 1; + } + if (this.vibrationOffsetX === undefined) { + this.vibrationOffsetX = 0; + this.vibrationOffsetY = 0; + this.vibrationPhase = random(TWO_PI); + } + } + + if (this.isTemporaryRed) { + this.tempRedTimer--; + if (this.tempRedTimer <= 0) { + this.isTemporaryRed = false; + } + } + } + + applyRippleAndRepel() { + for (let other of candidateParticles) { + if (other === this) continue; + let d = dist(this.x, this.y, other.x, other.y); + let minDist = (this.displaySize + other.displaySize) * 0.4; + if (d < minDist && d > 0) { + let angle = atan2(this.y - other.y, this.x - other.x); + let force = map(d, 0, minDist, 1.5, 0); + this.vx += cos(angle) * force * 0.1; + this.vy += sin(angle) * force * 0.1; + } + } + + for (let ripple of rippleEffects) { + let rd = dist(ripple.x, ripple.y, this.x, this.y); + if (rd < ripple.r + 60) { + let angle = atan2(ripple.y - this.y, ripple.x - this.x); + let force = map(ripple.r + 60 - rd, 0, ripple.r + 60, 0, 0.3); + this.vx += cos(angle) * force; + this.vy += sin(angle) * force; + } + } + + for (let repel of repelBursts) { + let d = dist(repel.x, repel.y, this.x, this.y); + if (d < repel.r) { + let angle = atan2(this.y - repel.y, this.x - repel.x); + let force = map(repel.r - d, 0, repel.r, 0, 1.2); + this.vx += cos(angle) * force; + this.vy += sin(angle) * force; + } + } + } + + display() { + noStroke(); + + // 计算显示位置:基础位置 + 效果振动 + 焦虑抖动 + 焦虑漂浮 + let displayX = this.x + (this.vibrationOffsetX || 0); + let displayY = this.y + (this.vibrationOffsetY || 0); + + // 获取对应波形点的抚平程度(如果没有关联,使用整体平静进度) + let localCalmProgress = this.calmProgress !== undefined ? this.calmProgress : calmProgress; + let dissolveProgress = this.dissolveProgress !== undefined ? this.dissolveProgress : 0; + + // 如果是红色粒子且在完成阶段,应用抖动和漂浮效果(根据抚平程度减少) + if (this.isRed && (completionPhase === 'revealing' || completionPhase === 'completed')) { + if (this.anxietyShakeX !== undefined && this.anxietyShakeY !== undefined) { + // 根据抚平程度减少抖动 + displayX += (this.anxietyShakeX || 0) * (1 - localCalmProgress); + displayY += ((this.anxietyShakeY || 0) + (this.anxietyFloatY || 0)) * (1 - localCalmProgress); + } + } + + let displaySize = this.displaySize; + if (this.hasEffect && this.effectScale !== undefined) { + displaySize = this.displaySize * this.effectScale; + } + + let glowBlur = 12 * this.prob; + if (this.hasEffect && this.effectGlow !== undefined) { + glowBlur = this.effectGlow; + } + drawingContext.shadowBlur = glowBlur; + + let displayAlpha = this.alpha; + if (this.hasEffect && this.effectAlpha !== undefined) { + displayAlpha = this.effectAlpha; + } + + // 根据消散进度减少透明度 + displayAlpha *= (1 - dissolveProgress); + + // 根据平静进度调整红色粒子的颜色(逐渐变淡) + if (this.isRed) { + if (completionPhase === 'revealing' || completionPhase === 'completed') { + // 在完成阶段,根据对应波形点的抚平程度调整颜色 + let redValue = map(localCalmProgress, 0, 1, 255, 200); + let greenValue = map(localCalmProgress, 0, 1, 100, 150); + let blueValue = map(localCalmProgress, 0, 1, 100, 150); + drawingContext.shadowColor = `rgba(${redValue},${greenValue},${blueValue},${this.prob * (1 - localCalmProgress * 0.5) * (1 - dissolveProgress)})`; + fill(redValue, greenValue, blueValue, displayAlpha); + } else { + // 正常游戏阶段,显示正常的红色 + drawingContext.shadowColor = `rgba(255,100,100,${this.prob})`; + fill(255, 100, 100, displayAlpha); + } + } else if (this.isOrange) { + if (isCompleted && completionPhase !== 'none') { + displayAlpha = displayAlpha * (fadeOutAlpha / 255); + } + drawingContext.shadowColor = `rgba(255,150,50,${this.prob * (fadeOutAlpha / 255)})`; + fill(255, 150, 50, displayAlpha); + } else if (this.isTemporaryRed) { + drawingContext.shadowColor = `rgba(255,150,150,${this.prob})`; + fill(255, 150, 150, displayAlpha); + } else { + if (isCompleted && completionPhase !== 'none') { + displayAlpha = displayAlpha * (fadeOutAlpha / 255); + } + drawingContext.shadowColor = `rgba(50,200,255,${this.prob * (fadeOutAlpha / 255)})`; + fill(50, 200, 255, displayAlpha); + } + + if (this.hasEffect && this.changeTimer !== undefined && this.changeTimer > 0 && this.changeTimer < 5) { + let highlightAlpha = map(this.changeTimer, 0, 5, 255, displayAlpha); + if (this.isOrange) { + fill(255, 180, 80, highlightAlpha); + } else if (this.isRed) { + fill(255, 150, 150, highlightAlpha); + } else { + fill(50, 200, 255, highlightAlpha); + } + } + + textSize(displaySize); + text(this.char, displayX, displayY); + drawingContext.shadowBlur = 0; + } +} + +class Ripple { + constructor(x, y) { + this.x = x; + this.y = y; + this.r = 10; + this.alpha = 200; + } + + update() { + this.r += 3; + this.alpha -= 4; + } + + display() { + noFill(); + stroke(100, 200, 255, this.alpha); + strokeWeight(2); + ellipse(this.x, this.y, this.r * 2); + } +} + +class RepelBurst { + constructor(x, y) { + this.x = x; + this.y = y; + this.r = 10; + this.alpha = 180; + } + + update() { + this.r += 6; + this.alpha -= 5; + } + + display() { + noFill(); + stroke(255, 150, 100, this.alpha); + strokeWeight(2); + ellipse(this.x, this.y, this.r * 2); + } +} diff --git a/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt.meta b/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt.meta new file mode 100644 index 000000000..46d227e23 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Emo/尝试2.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 632d2ea6fbd2c684e90cbd7f7e77036a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language.meta b/Assets/Scripts/MiniGame/HuoShan/Language.meta new file mode 100644 index 000000000..7569e51dd --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a1958c693a891564ca52d09de765d438 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs new file mode 100644 index 000000000..323a8022a --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs @@ -0,0 +1,203 @@ +using UnityEngine; +using System.Collections.Generic; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 候选粒子(可交互的主要粒子) + /// + 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 connections = new List(); + + [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; + } + } +} diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs.meta similarity index 83% rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta rename to Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs.meta index 0959e71e6..b69762fce 100644 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs.meta +++ b/Assets/Scripts/MiniGame/HuoShan/Language/CandidateParticle.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: bc6c154e0dac2e6469211e12b1f05fce +guid: fe9b66f677d7a024c8f232d03839b2ff MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs new file mode 100644 index 000000000..48b50169d --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs @@ -0,0 +1,210 @@ +using UnityEngine; +using System.Collections.Generic; +using Shapes; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 连接线渲染器:使用Shapes绘制粒子间的连接线 + /// + [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 candidateParticles; + private List effectPropagations; + private CompletionPhase completionPhase; + private float fadeOutAlpha; + + public void SetData( + List candidates, + List 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(); + 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); + } + } +} + diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs.meta similarity index 83% rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta rename to Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs.meta index b2b973262..5c9a8c9e1 100644 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs.meta +++ b/Assets/Scripts/MiniGame/HuoShan/Language/ConnectionRenderer.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c5da4de45a1d4c741b06d9c9c0519ff4 +guid: d7feb80748ec75e4eb986d0d676dfa04 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs b/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs new file mode 100644 index 000000000..66ff537f9 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs @@ -0,0 +1,154 @@ +using UnityEngine; +using System.Collections.Generic; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 效果传播信息 + /// + 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; + } + } + + /// + /// 效果传播系统:管理从红色粒子到蓝色粒子的效果传递 + /// + public class EffectPropagation + { + private CandidateParticle redParticle; + private List propagatedParticles = new List(); + private List propagationQueue = new List(); + + 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 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 nextQueue = new List(); + + 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 FindConnectedBlues(CandidateParticle particle) + { + List connected = new List(); + HashSet usedParticles = new HashSet(); + + // 记录已使用的粒子 + 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(); + } + } + } +} + diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs.meta similarity index 83% rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta rename to Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs.meta index 8dbc7fa7f..b2fcf2d61 100644 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs.meta +++ b/Assets/Scripts/MiniGame/HuoShan/Language/EffectPropagation.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: ce8085db413e67d4c9ddd8a3415ae3cf +guid: 13d2bba97b998f049bb52f097b0f597c MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs new file mode 100644 index 000000000..a43b3fd9b --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs @@ -0,0 +1,42 @@ +using UnityEngine; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 背景浮动文字粒子 + /// + 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); + } + } + } +} diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs.meta similarity index 83% rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta rename to Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs.meta index 70d934783..93cfa4990 100644 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs.meta +++ b/Assets/Scripts/MiniGame/HuoShan/Language/FloatingTextParticle.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 68f10137fe541b44485fa63022e29524 +guid: f7f33e95ffe9c50418758691861b6ebd MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs new file mode 100644 index 000000000..b86560dd2 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs @@ -0,0 +1,795 @@ +using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using DG.Tweening; +using Shapes; +using TMPro; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 游戏完成阶段 + /// + public enum CompletionPhase + { + None, // 未完成 + Arranging, // 排列中 + Revealing, // 揭示中 + Completed // 已完成 + } + + /// + /// 语言粒子系统主管理器 + /// + 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 floatingParticles = new List(); + private List candidateParticles = new List(); + private List redParticles = new List(); + + // 效果系统 + private List effectPropagations = new List(); + private Dictionary previousConnections = new Dictionary(); + 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 orderedRedParticles = new List(); + private List targetPositions = new List(); + + // 波形系统 + private List waveformPoints = new List(); + 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(); + worldCanvas.renderMode = RenderMode.WorldSpace; + + RectTransform rectTransform = canvasObj.GetComponent(); + 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(); + 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(); + 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.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.manager = this; + connectionRenderer.connectionDistance = connectionDistance; + + // 波形渲染器 + GameObject waveObj = new GameObject("WaveformRenderer"); + waveObj.transform.SetParent(transform); + waveObj.transform.localPosition = Vector3.zero; + waveformRenderer = waveObj.AddComponent(); + waveformRenderer.interactionRadius = interactionRadius; + } + + private void SelectRedParticles() + { + redParticles.Clear(); + List 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 currentConnections = new Dictionary(); + + 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 visited = new HashSet(); + Queue queue = new Queue(); + 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 GetOrderedRedParticles() + { + if (redParticles.Count == 0) return new List(); + if (redParticles.Count == 1) return new List(redParticles); + + // 从最左边的粒子开始 + var ordered = new List(); + var startParticle = redParticles.OrderBy(p => p.transform.position.x).First(); + + HashSet visited = new HashSet(); + Stack stack = new Stack(); + 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(); + } + } +} + diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs.meta new file mode 100644 index 000000000..33ab1f0a7 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 79af040068968934c9a7d092267034f6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleSystem_Summary.md b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleSystem_Summary.md new file mode 100644 index 000000000..498976d24 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleSystem_Summary.md @@ -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. 线条使用批量绘制 + diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleSystem_Summary.md.meta b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleSystem_Summary.md.meta new file mode 100644 index 000000000..a43faea28 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/LanguageParticleSystem_Summary.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 82c45afa1fdda6d45af591a380e1f4ec +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md b/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md new file mode 100644 index 000000000..74c63f1fe --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md @@ -0,0 +1,150 @@ +# 快速开始指南 + +## 5分钟快速设置 + +### 步骤1:创建场景对象 + +1. 在Hierarchy中右键 → Create Empty +2. 命名为 "LanguageParticleSystem" +3. 添加组件:`LanguageParticleManager` + +### 步骤2:检查依赖 + +确保项目已安装: +- ✅ TextMeshPro(Unity内置) +- ✅ DOTween(Asset Store) +- ✅ Shapes(Asset 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中监控粒子状态 + diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md.meta b/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md.meta new file mode 100644 index 000000000..e4685bd87 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/QuickStart.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7db05522201123d409abb24ec2f7f845 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/README.md b/Assets/Scripts/MiniGame/HuoShan/Language/README.md new file mode 100644 index 000000000..7692eaf64 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/README.md @@ -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. 创建Canvas(Render 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**: 连接距离阈值(默认2,Unity单位) +- **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原型复现,供学习和研究使用。 + diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md.meta b/Assets/Scripts/MiniGame/HuoShan/Language/README.md.meta similarity index 75% rename from Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md.meta rename to Assets/Scripts/MiniGame/HuoShan/Language/README.md.meta index 489023e4c..2e4a7ed20 100644 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md.meta +++ b/Assets/Scripts/MiniGame/HuoShan/Language/README.md.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 1500caf0ff27d574cbbc46a5b6954555 +guid: 816db8f21057c5c4892f64913a92dccf TextScriptImporter: externalObjects: {} userData: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs b/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs new file mode 100644 index 000000000..2466c0246 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs @@ -0,0 +1,238 @@ +using UnityEngine; +using TMPro; +using DG.Tweening; +using System.Collections.Generic; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 文字粒子基类 + /// + 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(); + + if (textMesh == null) + { + var textObj = new GameObject("Text"); + textObj.transform.SetParent(transform, false); + + // 添加 RectTransform(在 Canvas 下必需) + RectTransform rectTransform = textObj.AddComponent(); + 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(); + 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(); + 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; + } + + /// + /// 设置字体资源 + /// + public void SetFont(TMP_FontAsset fontAsset) + { + customFont = fontAsset; + SetupFont(); + } + + /// + /// 内部方法:设置字体 + /// + 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)); + } + } +} + diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs.meta new file mode 100644 index 000000000..82b863276 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/TextParticle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6da7d4539a3ba0044bbdfda3959c396b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/WaveformPoint.cs b/Assets/Scripts/MiniGame/HuoShan/Language/WaveformPoint.cs new file mode 100644 index 000000000..93b6d1350 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/WaveformPoint.cs @@ -0,0 +1,210 @@ +using UnityEngine; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 波形点:管理单个波形点的状态和动画 + /// + 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; + } + + /// + /// 生成多层Perlin噪声 + /// + 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); + } + + /// + /// 更新波形点(动态噪波、交互、抚平) + /// + 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); + } + } + } + } + } +} + diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/WaveformPoint.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/WaveformPoint.cs.meta new file mode 100644 index 000000000..0896904d7 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/WaveformPoint.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d995549b26518a4419234f98d89ec699 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/WaveformRenderer.cs b/Assets/Scripts/MiniGame/HuoShan/Language/WaveformRenderer.cs new file mode 100644 index 000000000..89e84035c --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/WaveformRenderer.cs @@ -0,0 +1,266 @@ +using UnityEngine; +using System.Collections.Generic; +using Shapes; + +namespace AibisDream.MiniGame.Language +{ + /// + /// 波形渲染器:使用Shapes绘制波形和交互效果 + /// + [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 waveformPoints; + private CompletionPhase completionPhase; + private float calmProgress; + private Vector2 mouseWorldPos; + private bool mousePressed; + private float mouseDragSpeed; + private List waveParticles = new List(); + + public void SetData( + List 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(); + var colors = new List(); + + 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; + } + } + } +} + diff --git a/Assets/Scripts/MiniGame/HuoShan/Language/WaveformRenderer.cs.meta b/Assets/Scripts/MiniGame/HuoShan/Language/WaveformRenderer.cs.meta new file mode 100644 index 000000000..0bc738d58 --- /dev/null +++ b/Assets/Scripts/MiniGame/HuoShan/Language/WaveformRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc80f1f8eacb59c4ea4cf18ff3a109c4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs deleted file mode 100644 index acb371ceb..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/BackgroundTextLayer.cs +++ /dev/null @@ -1,195 +0,0 @@ -using UnityEngine; -using TMPro; -using System.Collections.Generic; -using System.Linq; - -namespace AibisDream.MiniGame.HuoShan -{ - /// - /// 背景文字层 - 生成和管理大量随机变化的文字,文字间寻找关联形成词语连线 - /// - public class BackgroundTextLayer - { - private List _allNodes; - private List _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(); - - // 初始化中文字符集 - InitializeChineseChars(); - - // 创建文字预制体 - CreateTextPrefab(); - - // 生成初始节点 - GenerateNodes(); - } - - private void InitializeChineseChars() - { - _chineseChars = new List(); - // 常用中文字符(可以扩展) - 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(); - 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(); - - 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 GetAllNodes() - { - return _allNodes; - } - - public List 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); - } - } - } -} - diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs deleted file mode 100644 index 831330086..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/CircularViewport.cs +++ /dev/null @@ -1,155 +0,0 @@ -using UnityEngine; -using Shapes; - -namespace AibisDream.MiniGame.HuoShan -{ - /// - /// 圆形可视区域 - 使用Shapes绘制圆形遮罩,支持鼠标拖拽移动 - /// - 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(); - } - } - - 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); - } - } - } -} - diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs deleted file mode 100644 index 38ab64240..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationSystem.cs +++ /dev/null @@ -1,325 +0,0 @@ -using UnityEngine; -using System.Collections.Generic; -using Shapes; - -namespace AibisDream.MiniGame.HuoShan -{ - /// - /// 语言想象系统主管理器 - 协调各模块工作 - /// - [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(); - if (_viewport == null) - { - _viewport = gameObject.AddComponent(); - } - _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); - } - } - } - } - } - - /// - /// 开始构建句子(由Yarn指令调用) - /// - public void StartSentence(string sentence, int stuckIndex = -1) - { - if (_sentenceBuilder != null) - { - _sentenceBuilder.StartBuildingSentence(sentence, stuckIndex); - } - } - - /// - /// 停止构建句子 - /// - 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); - } - } -} - diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs deleted file mode 100644 index 6fae24ca1..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/LanguageImaginationYarnCommand.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections; -using Yarn.Unity; -using UnityEngine; - -namespace AibisDream.MiniGame.HuoShan -{ - /// - /// 语言想象系统Yarn指令 - /// - public static class LanguageImaginationYarnCommand - { - private static LanguageImaginationSystem GetSystem() - { - // 尝试从场景中查找LanguageImaginationSystem - LanguageImaginationSystem system = Object.FindObjectOfType(); - if (system == null) - { - Debug.LogError("[LanguageImaginationYarnCommand] 未找到LanguageImaginationSystem组件!请在场景中添加该组件。"); - } - return system; - } - - /// - /// 开始构建句子 - /// - /// 要构建的句子 - /// 卡壳位置(从0开始,-1表示不卡壳) - [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); - } - } - - /// - /// 停止构建句子 - /// - [YarnCommand("language_imagine_stop")] - public static void LanguageImagineStop() - { - var system = GetSystem(); - if (system != null) - { - system.StopSentence(); - } - } - } -} - diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md b/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md deleted file mode 100644 index fc883a68b..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/README.md +++ /dev/null @@ -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指令使用 - -#### 开始构建句子 -``` -<> -``` -- 第一个参数:要构建的句子 -- 第二个参数(可选):卡壳位置(从0开始,-1表示不卡壳)。例如3表示在第4个字(索引3)处卡壳 - -#### 停止构建句子 -``` -<> -``` - -### 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等) - diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs deleted file mode 100644 index 9b19bbc25..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs +++ /dev/null @@ -1,375 +0,0 @@ -using UnityEngine; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using DG.Tweening; - -namespace AibisDream.MiniGame.HuoShan -{ - /// - /// 造句系统 - 处理句子生成流程:选定文字、候选文字、概率变化、连线 - /// - public class SentenceBuilder - { - private BackgroundTextLayer _backgroundLayer; - private CircularViewport _viewport; - private List _sentenceNodes; - private string _targetSentence; - private int _currentIndex = 0; - private bool _isBuilding = false; - private int _stuckIndex = -1; // 卡壳位置,-1表示不卡壳 - - // 候选文字相关 - private List _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(); - _candidateNodes = new List(); - } - - /// - /// 开始构建句子 - /// - 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 stuckNodes = new List(); - 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 GetSentenceNodes() - { - return _sentenceNodes; - } - } -} - diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs.meta deleted file mode 100644 index cec804d5c..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/SentenceBuilder.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: fcab091b0b0e39142ae616cc9a548074 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs b/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs deleted file mode 100644 index c431deb0a..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs +++ /dev/null @@ -1,75 +0,0 @@ -using UnityEngine; -using TMPro; -using System.Collections.Generic; - -namespace AibisDream.MiniGame.HuoShan -{ - /// - /// 文字节点类 - 管理单个文字的显示、状态和连接关系 - /// - 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 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(); - } - - 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); - } - } - } - - /// - /// 文字节点状态 - /// - public enum TextNodeState - { - Random, // 随机变化状态 - Selected, // 已选定的文字(句子中的字) - Candidate, // 候选文字 - Stuck // 卡壳状态 - } -} - diff --git a/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs.meta b/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs.meta deleted file mode 100644 index 4f69f4ade..000000000 --- a/Assets/Scripts/MiniGame/HuoShan/NewExpress/TextNode.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 27e691dacc3b63545924959f964de0bb -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: