From d6b9c517d9210d1d1bc20124de204cbec77e7b57 Mon Sep 17 00:00:00 2001 From: Pixel Date: Tue, 4 Aug 2026 20:56:53 +0200 Subject: [PATCH] Cleanup --- .../{New Material.mat => Test_Green.mat} | 6 +- ... Material.mat.meta => Test_Green.mat.meta} | 0 Assets/Materials/Test_Red.mat | 137 + Assets/Materials/Test_Red.mat.meta | 8 + Assets/Scenes/Sandbox_Rainer.unity | 422 ++- Assets/Scripts/PilleCollisionSystem.cs | 88 - Assets/Scripts/PilleCollisionSystem.cs.meta | 2 - Assets/Scripts/PilleComponent.cs | 11 - Assets/Scripts/PilleComponent.cs.meta | 2 - Assets/Scripts/PilleDotsBootstrap.cs | 238 -- Assets/Scripts/PilleDotsBootstrap.cs.meta | 2 - Assets/Scripts/PilleGameObjectMover.cs | 38 - Assets/Scripts/PilleGameObjectMover.cs.meta | 2 - Assets/Scripts/PilleMovementSystem.cs | 67 - Assets/Scripts/PilleMovementSystem.cs.meta | 2 - Assets/Scripts/PilleSimulationConfig.cs | 16 - Assets/Scripts/PilleSimulationConfig.cs.meta | 2 - Assets/Scripts/PilleSpawnerWithPrefab.cs | 111 - Assets/Scripts/PilleSpawnerWithPrefab.cs.meta | 2 - Docs/CodeDokumentation.html | 2353 +++++++++++++++++ Happy_Forest.slnx | 2 +- Packages/manifest.json | 1 + Packages/packages-lock.json | 15 + 23 files changed, 2938 insertions(+), 589 deletions(-) rename Assets/Materials/{New Material.mat => Test_Green.mat} (97%) rename Assets/Materials/{New Material.mat.meta => Test_Green.mat.meta} (100%) create mode 100644 Assets/Materials/Test_Red.mat create mode 100644 Assets/Materials/Test_Red.mat.meta delete mode 100644 Assets/Scripts/PilleCollisionSystem.cs delete mode 100644 Assets/Scripts/PilleCollisionSystem.cs.meta delete mode 100644 Assets/Scripts/PilleComponent.cs delete mode 100644 Assets/Scripts/PilleComponent.cs.meta delete mode 100644 Assets/Scripts/PilleDotsBootstrap.cs delete mode 100644 Assets/Scripts/PilleDotsBootstrap.cs.meta delete mode 100644 Assets/Scripts/PilleGameObjectMover.cs delete mode 100644 Assets/Scripts/PilleGameObjectMover.cs.meta delete mode 100644 Assets/Scripts/PilleMovementSystem.cs delete mode 100644 Assets/Scripts/PilleMovementSystem.cs.meta delete mode 100644 Assets/Scripts/PilleSimulationConfig.cs delete mode 100644 Assets/Scripts/PilleSimulationConfig.cs.meta delete mode 100644 Assets/Scripts/PilleSpawnerWithPrefab.cs delete mode 100644 Assets/Scripts/PilleSpawnerWithPrefab.cs.meta create mode 100644 Docs/CodeDokumentation.html diff --git a/Assets/Materials/New Material.mat b/Assets/Materials/Test_Green.mat similarity index 97% rename from Assets/Materials/New Material.mat rename to Assets/Materials/Test_Green.mat index 9c339fd..78eb877 100644 --- a/Assets/Materials/New Material.mat +++ b/Assets/Materials/Test_Green.mat @@ -20,7 +20,7 @@ Material: m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_Name: New Material + m_Name: Test_Green m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} m_Parent: {fileID: 0} m_ModifiedSerializedProperties: 0 @@ -129,8 +129,8 @@ Material: - _XRMotionVectorsPass: 1 - _ZWrite: 1 m_Colors: - - _BaseColor: {r: 1, g: 1, b: 1, a: 1} - - _Color: {r: 1, g: 1, b: 1, a: 1} + - _BaseColor: {r: 0, g: 1, b: 0, a: 1} + - _Color: {r: 0, g: 1, b: 0, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - _SpecColor: {r: 0.2, g: 0.2, b: 0.2, a: 1} m_BuildTextureStacks: [] diff --git a/Assets/Materials/New Material.mat.meta b/Assets/Materials/Test_Green.mat.meta similarity index 100% rename from Assets/Materials/New Material.mat.meta rename to Assets/Materials/Test_Green.mat.meta diff --git a/Assets/Materials/Test_Red.mat b/Assets/Materials/Test_Red.mat new file mode 100644 index 0000000..7d73e83 --- /dev/null +++ b/Assets/Materials/Test_Red.mat @@ -0,0 +1,137 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &-7128443880667998594 +MonoBehaviour: + m_ObjectHideFlags: 11 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion + version: 10 +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Test_Red + m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 1 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: + RenderType: Opaque + disabledShaderPasses: + - MOTIONVECTORS + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _BaseMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _BumpMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailAlbedoMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _DetailNormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _EmissionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MetallicGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OcclusionMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _ParallaxMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _SpecGlossMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_Lightmaps: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_LightmapsInd: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - unity_ShadowMasks: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _AddPrecomputedVelocity: 0 + - _AlphaClip: 0 + - _AlphaToMask: 0 + - _Blend: 0 + - _BlendModePreserveSpecular: 1 + - _BumpScale: 1 + - _ClearCoatMask: 0 + - _ClearCoatSmoothness: 0 + - _Cull: 2 + - _Cutoff: 0.5 + - _DetailAlbedoMapScale: 1 + - _DetailNormalMapScale: 1 + - _DstBlend: 0 + - _DstBlendAlpha: 0 + - _EnvironmentReflections: 1 + - _GlossMapScale: 0 + - _Glossiness: 0 + - _GlossyReflections: 0 + - _Metallic: 0 + - _OcclusionStrength: 1 + - _Parallax: 0.005 + - _QueueOffset: 0 + - _ReceiveShadows: 1 + - _Smoothness: 0.5 + - _SmoothnessTextureChannel: 0 + - _SpecularHighlights: 1 + - _SrcBlend: 1 + - _SrcBlendAlpha: 1 + - _Surface: 0 + - _WorkflowMode: 1 + - _XRMotionVectorsPass: 1 + - _ZWrite: 1 + m_Colors: + - _BaseColor: {r: 1, g: 0, b: 0, a: 1} + - _Color: {r: 1, g: 0, b: 0, a: 1} + - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} + - _SpecColor: {r: 0.2, g: 0.2, b: 0.2, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Assets/Materials/Test_Red.mat.meta b/Assets/Materials/Test_Red.mat.meta new file mode 100644 index 0000000..2c10271 --- /dev/null +++ b/Assets/Materials/Test_Red.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5be67e6e3b8bc074182b97e61d8d4c37 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/Sandbox_Rainer.unity b/Assets/Scenes/Sandbox_Rainer.unity index 4da248a..73a4fc5 100644 --- a/Assets/Scenes/Sandbox_Rainer.unity +++ b/Assets/Scenes/Sandbox_Rainer.unity @@ -119,6 +119,139 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1 &105983835 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 105983838} + - component: {fileID: 105983837} + - component: {fileID: 105983836} + m_Layer: 0 + m_Name: Team2_Base + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &105983836 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 105983835} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b97066bf235990144b39e9bc214b1cca, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.PlayerEconomy + startingGold: 100 + incomePerWave: 50 +--- !u!114 &105983837 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 105983835} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d2d4e074c26982c4c8752ff97c4654ed, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.BaseGrid + columns: 10 + rows: 10 + cellSize: 2 +--- !u!4 &105983838 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 105983835} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 90, y: 2, z: -10} + 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 &190020080 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 190020083} + - component: {fileID: 190020082} + - component: {fileID: 190020081} + m_Layer: 0 + m_Name: Team2_Shop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &190020081 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 190020080} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f49cec0c77e4fce4694ff6f19a72eea8, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.PlacementController + baseGrid: {fileID: 105983837} + economy: {fileID: 105983836} + shopUI: {fileID: 190020082} + shopUnits: + - {fileID: 11400000, guid: 3086cd18cc645174296981d67778f969, type: 2} + - {fileID: 11400000, guid: a1c5636849772f34e92425663d2dbc9c, type: 2} + targetCamera: {fileID: 0} +--- !u!114 &190020082 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 190020080} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: acaf0cdd984399e44b7cb2114276e4c1, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.ShopUI + economy: {fileID: 105983836} + placementController: {fileID: 190020081} + shopUnits: + - {fileID: 11400000, guid: 3086cd18cc645174296981d67778f969, type: 2} + - {fileID: 11400000, guid: a1c5636849772f34e92425663d2dbc9c, type: 2} + teamLabel: Team B + barRow: 1 +--- !u!4 &190020083 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 190020080} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 90.7, y: 0.5, z: -25.8} + 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 &484453422 GameObject: m_ObjectHideFlags: 0 @@ -339,6 +472,7 @@ GameObject: - component: {fileID: 1109898417} - component: {fileID: 1109898416} - component: {fileID: 1109898415} + - component: {fileID: 1109898418} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera @@ -413,13 +547,57 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1109898414} serializedVersion: 2 - m_LocalRotation: {x: 0.15304591, y: -0.6903456, z: 0.15304591, w: 0.6903456} - m_LocalPosition: {x: 120, y: 30, z: 0} + m_LocalRotation: {x: 0.2933424, y: 0.636742, z: -0.28755635, w: 0.6525498} + m_LocalPosition: {x: -113.66025, y: 15.64744, z: -1.2121267} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 25, y: -90, z: 0} +--- !u!114 &1109898418 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1109898414} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData + 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_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 + m_Version: 2 --- !u!1 &1401811863 GameObject: m_ObjectHideFlags: 0 @@ -456,6 +634,56 @@ Transform: - {fileID: 2056509223} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1418310696 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1418310698} + - component: {fileID: 1418310697} + m_Layer: 0 + m_Name: WaveManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1418310697 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1418310696} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cf2b749247b4c2e4ead03cd9a384c4b3, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.WaveManager + bootstrap: {fileID: 1460528677} + team0Grid: {fileID: 1687991685} + team1Grid: {fileID: 105983837} + team0Economy: {fileID: 1687991687} + team1Economy: {fileID: 105983836} + playerCount: 2 +--- !u!4 &1418310698 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1418310696} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -68.88014, y: 0.5, z: -0.41516} + 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 &1432930633 GameObject: m_ObjectHideFlags: 0 @@ -568,6 +796,57 @@ Transform: m_Children: [] m_Father: {fileID: 1401811864} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1460528676 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1460528678} + - component: {fileID: 1460528677} + m_Layer: 0 + m_Name: SkirmishBootstrap + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1460528677 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1460528676} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 55a4d60b2bf8ed14893e1569403c209a, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Skirmish.SkirmishBootstrap + unitTypes: + - {fileID: 11400000, guid: 3086cd18cc645174296981d67778f969, type: 2} + - {fileID: 11400000, guid: a1c5636849772f34e92425663d2dbc9c, type: 2} + wallTag: Wall + spawnHeight: 2 + spawnSpreadX: 8 + debugWaveCountPerTeam: 20 +--- !u!4 &1460528678 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1460528676} + 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!1 &1465384989 GameObject: m_ObjectHideFlags: 0 @@ -680,6 +959,68 @@ Transform: m_Children: [] m_Father: {fileID: 1401811864} m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0} +--- !u!1 &1687991684 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1687991686} + - component: {fileID: 1687991685} + - component: {fileID: 1687991687} + m_Layer: 0 + m_Name: Team1_Base + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1687991685 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1687991684} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d2d4e074c26982c4c8752ff97c4654ed, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.BaseGrid + columns: 10 + rows: 10 + cellSize: 2 +--- !u!4 &1687991686 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1687991684} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -110, y: 2, z: -10} + 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 &1687991687 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1687991684} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b97066bf235990144b39e9bc214b1cca, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.PlayerEconomy + startingGold: 100 + incomePerWave: 50 --- !u!1 &1929976483 GameObject: m_ObjectHideFlags: 0 @@ -731,6 +1072,77 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1932593963 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1932593966} + - component: {fileID: 1932593965} + - component: {fileID: 1932593964} + m_Layer: 0 + m_Name: Team1_Shop + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1932593964 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1932593963} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f49cec0c77e4fce4694ff6f19a72eea8, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.PlacementController + baseGrid: {fileID: 1687991685} + economy: {fileID: 1687991687} + shopUI: {fileID: 1932593965} + shopUnits: + - {fileID: 11400000, guid: 3086cd18cc645174296981d67778f969, type: 2} + - {fileID: 11400000, guid: a1c5636849772f34e92425663d2dbc9c, type: 2} + targetCamera: {fileID: 0} +--- !u!114 &1932593965 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1932593963} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: acaf0cdd984399e44b7cb2114276e4c1, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::HappyForest.Base.ShopUI + economy: {fileID: 1687991687} + placementController: {fileID: 1932593964} + shopUnits: + - {fileID: 11400000, guid: 3086cd18cc645174296981d67778f969, type: 2} + - {fileID: 11400000, guid: a1c5636849772f34e92425663d2dbc9c, type: 2} + teamLabel: Team A + barRow: 0 +--- !u!4 &1932593966 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1932593963} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -105.85, y: 0.5, z: -22.32} + 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 &2016336478 GameObject: m_ObjectHideFlags: 0 @@ -963,3 +1375,9 @@ SceneRoots: - {fileID: 925142574} - {fileID: 1401811864} - {fileID: 1929976485} + - {fileID: 1460528678} + - {fileID: 1687991686} + - {fileID: 105983838} + - {fileID: 1932593966} + - {fileID: 190020083} + - {fileID: 1418310698} diff --git a/Assets/Scripts/PilleCollisionSystem.cs b/Assets/Scripts/PilleCollisionSystem.cs deleted file mode 100644 index 3afed6f..0000000 --- a/Assets/Scripts/PilleCollisionSystem.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Unity.Burst; -using Unity.Collections; -using Unity.Entities; -using Unity.Jobs; -using Unity.Mathematics; -using Unity.Transforms; - -namespace HappyForest.Prototypen -{ - // Pille-gegen-Pille Kollision. Bewusst als einfacher O(n^2) Burst-Job gebaut, - // damit man per "K"-Taste sauber A/B vergleichen kann, was die Kollisionsprüfung - // an Performance kostet. Läuft nur, wenn PilleCollisionEnabled = true ist. - // (Für sehr große Mengen, z.B. >10.000, wäre ein Spatial-Grid der nächste Schritt.) - public partial struct PilleCollisionSystem : ISystem - { - public void OnCreate(ref SystemState state) - { - state.RequireForUpdate(); - } - - public void OnUpdate(ref SystemState state) - { - var config = SystemAPI.GetSingleton(); - if (!config.PilleCollisionEnabled) - return; - - var query = SystemAPI.QueryBuilder().WithAll().Build(); - int count = query.CalculateEntityCount(); - if (count < 2) - return; - - var transforms = query.ToComponentDataArray(Allocator.TempJob); - var pilles = query.ToComponentDataArray(Allocator.TempJob); - - float radiusSum = config.PilleRadius * 2f; - - var job = new ResolvePairCollisionsJob - { - Transforms = transforms, - Pilles = pilles, - RadiusSq = radiusSum * radiusSum - }; - - state.Dependency = job.Schedule(state.Dependency); - state.Dependency.Complete(); - - query.CopyFromComponentDataArray(pilles); - - transforms.Dispose(); - pilles.Dispose(); - } - } - - [BurstCompile] - public struct ResolvePairCollisionsJob : IJob - { - [ReadOnly] public NativeArray Transforms; - public NativeArray Pilles; - public float RadiusSq; - - public void Execute() - { - int count = Transforms.Length; - for (int i = 0; i < count; i++) - { - for (int j = i + 1; j < count; j++) - { - float3 delta = Transforms[i].Position - Transforms[j].Position; - float distSq = math.lengthsq(delta); - - if (distSq < RadiusSq && distSq > 0.0001f) - { - float3 normal = math.normalize(delta); - - var pi = Pilles[i]; - var pj = Pilles[j]; - - pi.Velocity = math.reflect(pi.Velocity, normal); - pj.Velocity = math.reflect(pj.Velocity, -normal); - - Pilles[i] = pi; - Pilles[j] = pj; - } - } - } - } - } -} diff --git a/Assets/Scripts/PilleCollisionSystem.cs.meta b/Assets/Scripts/PilleCollisionSystem.cs.meta deleted file mode 100644 index e8c8eb6..0000000 --- a/Assets/Scripts/PilleCollisionSystem.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c5c8d21e0eed6a948a3d402044a4dc1d \ No newline at end of file diff --git a/Assets/Scripts/PilleComponent.cs b/Assets/Scripts/PilleComponent.cs deleted file mode 100644 index ea2a3c3..0000000 --- a/Assets/Scripts/PilleComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Unity.Entities; -using Unity.Mathematics; - -namespace HappyForest.Prototypen -{ - public struct PilleComponent : IComponentData - { - public float3 Velocity; - public float Speed; - } -} diff --git a/Assets/Scripts/PilleComponent.cs.meta b/Assets/Scripts/PilleComponent.cs.meta deleted file mode 100644 index 2c4f515..0000000 --- a/Assets/Scripts/PilleComponent.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a7873b1a55b648b4491f56c9d66ad2e6 \ No newline at end of file diff --git a/Assets/Scripts/PilleDotsBootstrap.cs b/Assets/Scripts/PilleDotsBootstrap.cs deleted file mode 100644 index 68ee8b3..0000000 --- a/Assets/Scripts/PilleDotsBootstrap.cs +++ /dev/null @@ -1,238 +0,0 @@ -using Unity.Collections; -using Unity.Entities; -using Unity.Mathematics; -using Unity.Rendering; -using Unity.Transforms; -using UnityEngine; -using UnityEngine.Rendering; - -namespace HappyForest.Prototypen -{ - // Einziges MonoBehaviour im DOTS-Setup. Aufgabe: Input lesen (Space/M/C/K), - // die Entity-World initialisieren und Pillen-Entities spawnen/löschen. - // Bewegung + Kollision laufen komplett in ISystem/Burst (siehe PilleMovementSystem - // und PilleCollisionSystem) - hier passiert keine Simulationslogik mehr. - public class PilleDotsBootstrap : MonoBehaviour - { - [Header("Rendering (Pflichtfelder)")] - [SerializeField] private Mesh pilleMesh; - [SerializeField] private Material pilleMaterial; - - [Header("Einstellungen")] - [SerializeField] private float speed = 5f; - [SerializeField] private float pilleRadius = 0.5f; - [SerializeField] private float spawnHeight = 1f; - [SerializeField] private string wallTag = "Wall"; - [SerializeField] private float actionDelay = 0.05f; - - private EntityManager entityManager; - private Entity pilleTemplateEntity; - private EntityQuery pilleQuery; - private EntityQuery configQuery; - - private int pilleCount = 0; - private float lastActionTime = 0f; - private bool collisionEnabledCache = false; - - private Texture2D backgroundTexture; - - void Start() - { - if (pilleMesh == null || pilleMaterial == null) - { - Debug.LogError("❌ PilleDotsBootstrap: Mesh oder Material im Inspector zuweisen!"); - enabled = false; - return; - } - - entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; - - SetupSimulationConfig(); - SetupPilleTemplate(); - - pilleQuery = entityManager.CreateEntityQuery(typeof(PilleComponent), typeof(LocalTransform)); - configQuery = entityManager.CreateEntityQuery(typeof(PilleSimulationConfig)); - - backgroundTexture = MakeTex(new Color(0f, 0f, 0f, 0.65f)); - - Debug.Log("✅ PilleDotsBootstrap bereit! Space=Spawn M=Delete C=Spawn100 K=Kollision Toggle"); - } - - void SetupSimulationConfig() - { - var wallBounds = ComputeWallBounds(); - float margin = pilleRadius + 0.1f; - - var configEntity = entityManager.CreateEntity(typeof(PilleSimulationConfig)); - entityManager.SetComponentData(configEntity, new PilleSimulationConfig - { - BoundsMin = new float3(wallBounds.min.x + margin, spawnHeight, wallBounds.min.z + margin), - BoundsMax = new float3(wallBounds.max.x - margin, spawnHeight, wallBounds.max.z - margin), - PlayHeight = spawnHeight, - PilleRadius = pilleRadius, - PilleCollisionEnabled = false - }); - } - - Bounds ComputeWallBounds() - { - var walls = GameObject.FindGameObjectsWithTag(wallTag); - if (walls.Length == 0) - { - Debug.LogWarning($"⚠️ Keine GameObjects mit Tag '{wallTag}' gefunden. Nutze Standard-Grenzen (50x100)."); - return new Bounds(Vector3.zero, new Vector3(50, 10, 100)); - } - - Bounds combined = default; - bool first = true; - - foreach (var wall in walls) - { - var rend = wall.GetComponent(); - if (rend == null) continue; - - if (first) { combined = rend.bounds; first = false; } - else combined.Encapsulate(rend.bounds); - } - - Debug.Log($"✅ Feld-Grenzen aus '{wallTag}'-Objekten berechnet: Min={combined.min} Max={combined.max}"); - return combined; - } - - void SetupPilleTemplate() - { - pilleTemplateEntity = entityManager.CreateEntity( - typeof(PilleComponent), - typeof(LocalTransform), - typeof(LocalToWorld)); - - var desc = new RenderMeshDescription(ShadowCastingMode.On, receiveShadows: true); - var renderMeshArray = new RenderMeshArray(new[] { pilleMaterial }, new[] { pilleMesh }); - - RenderMeshUtility.AddComponents( - pilleTemplateEntity, - entityManager, - desc, - renderMeshArray, - MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0)); - - entityManager.SetComponentData(pilleTemplateEntity, - LocalTransform.FromPosition(new float3(0, spawnHeight, 0))); - - entityManager.SetComponentData(pilleTemplateEntity, new PilleComponent - { - Velocity = new float3(1, 0, 0), - Speed = speed - }); - - // WICHTIG: Prefab-Tag, damit dieses Template NICHT von den Queries/Systemen - // (Bewegung, Kollision, Zählung) erfasst wird. Instantiate() entfernt den Tag - // automatisch bei jeder Kopie. - entityManager.AddComponent(pilleTemplateEntity); - } - - void Update() - { - if (Input.GetKeyDown(KeyCode.K)) - { - ToggleCollision(); - } - - if (Time.time - lastActionTime < actionDelay) - return; - - if (Input.GetKey(KeyCode.Space)) - { - SpawnPille(); - lastActionTime = Time.time; - } - else if (Input.GetKey(KeyCode.M)) - { - DeletePille(); - lastActionTime = Time.time; - } - else if (Input.GetKey(KeyCode.C)) - { - for (int i = 0; i < 100; i++) - SpawnPille(); - lastActionTime = Time.time; - } - } - - void SpawnPille() - { - float3 spawnPos = new float3(0, spawnHeight, 0); - - int existing = pilleQuery.CalculateEntityCount(); - if (existing > 0) - { - var transforms = pilleQuery.ToComponentDataArray(Allocator.Temp); - int idx = UnityEngine.Random.Range(0, transforms.Length); - spawnPos = transforms[idx].Position; - transforms.Dispose(); - } - - Entity newPille = entityManager.Instantiate(pilleTemplateEntity); - - float angle = UnityEngine.Random.Range(0f, math.PI * 2f); - var velocity = new float3(math.cos(angle), 0, math.sin(angle)); - - entityManager.SetComponentData(newPille, LocalTransform.FromPosition(spawnPos)); - entityManager.SetComponentData(newPille, new PilleComponent - { - Velocity = velocity, - Speed = speed - }); - - pilleCount++; - } - - void DeletePille() - { - if (pilleQuery.CalculateEntityCount() == 0) - return; - - var entities = pilleQuery.ToEntityArray(Allocator.Temp); - entityManager.DestroyEntity(entities[0]); - entities.Dispose(); - pilleCount--; - } - - void ToggleCollision() - { - if (configQuery.CalculateEntityCount() == 0) - return; - - var configEntity = configQuery.GetSingletonEntity(); - var cfg = entityManager.GetComponentData(configEntity); - cfg.PilleCollisionEnabled = !cfg.PilleCollisionEnabled; - entityManager.SetComponentData(configEntity, cfg); - collisionEnabledCache = cfg.PilleCollisionEnabled; - } - - Texture2D MakeTex(Color color) - { - var tex = new Texture2D(1, 1); - tex.SetPixel(0, 0, color); - tex.Apply(); - return tex; - } - - void OnGUI() - { - GUI.DrawTexture(new Rect(5, 5, 430, 90), backgroundTexture); - - GUI.color = Color.white; - GUI.skin.label.fontSize = 18; - GUI.Label(new Rect(15, 10, 410, 25), $"Pillen: {pilleCount} | FPS: {(int)(1f / Time.deltaTime)}"); - GUI.Label(new Rect(15, 35, 410, 25), $"Pille-Kollision (K): {(collisionEnabledCache ? "AN" : "AUS")}"); - GUI.Label(new Rect(15, 60, 410, 25), "Space=Spawn M=Delete C=Spawn100 K=Kollision Toggle"); - } - - void OnDestroy() - { - if (backgroundTexture != null) - Destroy(backgroundTexture); - } - } -} diff --git a/Assets/Scripts/PilleDotsBootstrap.cs.meta b/Assets/Scripts/PilleDotsBootstrap.cs.meta deleted file mode 100644 index ce94369..0000000 --- a/Assets/Scripts/PilleDotsBootstrap.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f717f54a39e0fec47bd6fe47f1e643e2 \ No newline at end of file diff --git a/Assets/Scripts/PilleGameObjectMover.cs b/Assets/Scripts/PilleGameObjectMover.cs deleted file mode 100644 index 9ce90bb..0000000 --- a/Assets/Scripts/PilleGameObjectMover.cs +++ /dev/null @@ -1,38 +0,0 @@ -using UnityEngine; - -namespace HappyForest.Prototypen -{ - public class PilleGameObjectMover : MonoBehaviour - { - public Vector3 velocity = Vector3.right; - private float speed = 5f; - private float raycastDistance = 0.8f; - - void FixedUpdate() - { - if (velocity.magnitude == 0) - velocity = Vector3.right; - - // Raycast nur gegen Wände checken - RaycastHit hit; - if (Physics.Raycast(transform.position, velocity.normalized, out hit, raycastDistance)) - { - if (hit.collider.CompareTag("Wall")) - { - velocity = Vector3.Reflect(velocity, hit.normal); - } - } - - // Bewegung - transform.position += velocity.normalized * speed * Time.fixedDeltaTime; - } - - void OnCollisionEnter(Collision collision) - { - if (collision.gameObject.CompareTag("Wall")) - { - velocity = Vector3.Reflect(velocity, collision.relativeVelocity.normalized); - } - } - } -} diff --git a/Assets/Scripts/PilleGameObjectMover.cs.meta b/Assets/Scripts/PilleGameObjectMover.cs.meta deleted file mode 100644 index dd607f2..0000000 --- a/Assets/Scripts/PilleGameObjectMover.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f4aadfba2e345d34a9ec0d542a2149d4 \ No newline at end of file diff --git a/Assets/Scripts/PilleMovementSystem.cs b/Assets/Scripts/PilleMovementSystem.cs deleted file mode 100644 index a9c5d3d..0000000 --- a/Assets/Scripts/PilleMovementSystem.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Unity.Burst; -using Unity.Entities; -using Unity.Mathematics; -using Unity.Transforms; - -namespace HappyForest.Prototypen -{ - // Bewegt alle Pillen und lässt sie an den (aus den "Wall"-Objekten berechneten) - // Feld-Grenzen abprallen. Läuft komplett als Burst-kompilierter Parallel-Job. - [BurstCompile] - public partial struct PilleMovementSystem : ISystem - { - public void OnCreate(ref SystemState state) - { - state.RequireForUpdate(); - } - - [BurstCompile] - public void OnUpdate(ref SystemState state) - { - var config = SystemAPI.GetSingleton(); - float deltaTime = SystemAPI.Time.DeltaTime; - - var job = new MoveAndBounceJob - { - DeltaTime = deltaTime, - BoundsMin = config.BoundsMin, - BoundsMax = config.BoundsMax, - PlayHeight = config.PlayHeight - }; - - state.Dependency = job.ScheduleParallel(state.Dependency); - } - } - - [BurstCompile] - public partial struct MoveAndBounceJob : IJobEntity - { - public float DeltaTime; - public float3 BoundsMin; - public float3 BoundsMax; - public float PlayHeight; - - void Execute(ref LocalTransform transform, ref PilleComponent pille) - { - if (math.lengthsq(pille.Velocity) < 0.0001f) - pille.Velocity = new float3(1f, 0f, 0f); - - float3 pos = transform.Position + pille.Velocity * pille.Speed * DeltaTime; - - if (pos.x < BoundsMin.x || pos.x > BoundsMax.x) - { - pille.Velocity.x *= -1f; - pos.x = math.clamp(pos.x, BoundsMin.x, BoundsMax.x); - } - - if (pos.z < BoundsMin.z || pos.z > BoundsMax.z) - { - pille.Velocity.z *= -1f; - pos.z = math.clamp(pos.z, BoundsMin.z, BoundsMax.z); - } - - pos.y = PlayHeight; - transform.Position = pos; - } - } -} diff --git a/Assets/Scripts/PilleMovementSystem.cs.meta b/Assets/Scripts/PilleMovementSystem.cs.meta deleted file mode 100644 index 2f18385..0000000 --- a/Assets/Scripts/PilleMovementSystem.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 0d9cc3dffbc29c541932aa573ad049ab \ No newline at end of file diff --git a/Assets/Scripts/PilleSimulationConfig.cs b/Assets/Scripts/PilleSimulationConfig.cs deleted file mode 100644 index 176fe1f..0000000 --- a/Assets/Scripts/PilleSimulationConfig.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Unity.Entities; -using Unity.Mathematics; - -namespace HappyForest.Prototypen -{ - // Singleton-Komponente: Wird einmal beim Start von PilleDotsBootstrap erzeugt - // und steuert das Verhalten aller Systeme (Grenzen, Kollisions-Toggle etc.). - public struct PilleSimulationConfig : IComponentData - { - public float3 BoundsMin; - public float3 BoundsMax; - public float PlayHeight; - public float PilleRadius; - public bool PilleCollisionEnabled; - } -} diff --git a/Assets/Scripts/PilleSimulationConfig.cs.meta b/Assets/Scripts/PilleSimulationConfig.cs.meta deleted file mode 100644 index d4490b6..0000000 --- a/Assets/Scripts/PilleSimulationConfig.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 0a8bb1e30bb47a34abb9537815159639 \ No newline at end of file diff --git a/Assets/Scripts/PilleSpawnerWithPrefab.cs b/Assets/Scripts/PilleSpawnerWithPrefab.cs deleted file mode 100644 index ac16b81..0000000 --- a/Assets/Scripts/PilleSpawnerWithPrefab.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Collections; -using Unity.Entities; -using Unity.Mathematics; -using UnityEngine; - -namespace HappyForest.Prototypen -{ - public class PilleSpawnerWithPrefab : MonoBehaviour - { - [SerializeField] private GameObject pillePrefab; - private int pilleCount = 0; - private float lastSpawnTime = 0f; - private float spawnDelay = 0.05f; - private Transform pilleContainer; - - void Start() - { - if (pillePrefab == null) - { - Debug.LogError("❌ Pille Prefab nicht zugewiesen!"); - return; - } - - pilleContainer = new GameObject("PilleInstances").transform; - } - - void Update() - { - if (Time.time - lastSpawnTime < spawnDelay) - return; - - if (Input.GetKey(KeyCode.Space)) - { - SpawnPille(); - lastSpawnTime = Time.time; - } - - if (Input.GetKey(KeyCode.M)) - { - DeleteRandomPille(); - lastSpawnTime = Time.time; - } - - if (Input.GetKey(KeyCode.C)) - { - // Spawn 100 Pillen mit Delay, jede bei einer anderen Pille - StartCoroutine(SpawnMultiplePillen(100)); - lastSpawnTime = Time.time; - } - } - - void SpawnPille() - { - if (pillePrefab == null) - return; - - // Zufällige horizontale Richtung - var random = new Unity.Mathematics.Random((uint)System.Guid.NewGuid().GetHashCode()); - float angle = random.NextFloat() * math.PI * 2; - var velocity = new Vector3(math.cos(angle), 0, math.sin(angle)); - - // Spawn-Position: Bei einer existierenden Pille oder Default - Vector3 spawnPos = new Vector3(0, 5, 0); - - if (pilleContainer.childCount > 0) - { - // Wähle zufällige existierende Pille - int randomIndex = UnityEngine.Random.Range(0, pilleContainer.childCount); - Transform randomPille = pilleContainer.GetChild(randomIndex); - spawnPos = randomPille.position; - } - - // Instantiiere Prefab - var pilleGO = Instantiate(pillePrefab, pilleContainer); - pilleGO.name = $"Pille_{pilleCount}"; - pilleGO.transform.position = spawnPos; - - // Füge MovementScript hinzu - var mover = pilleGO.AddComponent(); - mover.velocity = velocity.normalized * 5f; - - pilleCount++; - } - - void DeleteRandomPille() - { - if (pilleContainer.childCount > 0) - { - Destroy(pilleContainer.GetChild(0).gameObject); - pilleCount--; - } - } - - IEnumerator SpawnMultiplePillen(int count) - { - for (int i = 0; i < count; i++) - { - SpawnPille(); - yield return new WaitForSeconds(0.01f); - } - } - - void OnGUI() - { - GUI.color = Color.yellow; - GUI.skin.label.fontSize = 20; - GUI.Label(new Rect(10, 10, 400, 30), $"Pillen: {pilleCount} | FPS: {(int)(1f / Time.deltaTime)}"); - GUI.Label(new Rect(10, 50, 400, 30), "Space=Spawn | M=Delete | C=Spawn100"); - } - } -} diff --git a/Assets/Scripts/PilleSpawnerWithPrefab.cs.meta b/Assets/Scripts/PilleSpawnerWithPrefab.cs.meta deleted file mode 100644 index 5c087f7..0000000 --- a/Assets/Scripts/PilleSpawnerWithPrefab.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 4d1c6532ed3464c49a3edc67997d2cb8 \ No newline at end of file diff --git a/Docs/CodeDokumentation.html b/Docs/CodeDokumentation.html new file mode 100644 index 0000000..09d3e1f --- /dev/null +++ b/Docs/CodeDokumentation.html @@ -0,0 +1,2353 @@ +Happy Forest — Code-Dokumentation & Editor-Anleitung + + + +
+ + +
+
+

Was hier passiert, Zeile für Zeile

+

Diese Seite erklärt jede Datei im Projekt: erst wofür sie da ist, dann jede Zeile/jeden Block einzeln. Häkchen setzen = verstanden. Der Haken bleibt gespeichert (im Browser dieses Geräts), auch nach Neuladen.

+
+ + +
+
+ Editor-Setup +

Was du in Unity einrichten musst

+
Reihenfolge: erst prüfen, was schon steht (Pille lief ja schon), dann Skirmish aufsetzen.
+
+ +
+ Bereits erledigt — bestätigt durch deine eigenen Screenshots (Pille rendert, 200.000 Stück liefen). Entities Graphics (1.4.21), URP (17.3.0) sind installiert, das URP-Asset ist zugewiesen, Materialien sind konvertiert. Hier nur zur Erinnerung gelistet, falls du sie in einem neuen Projekt nochmal brauchst. +
+ +
    +
  • + +
    + Entities Graphics Paket erledigt +

    Window → Package Manager → Unity Registry → "Entities Graphics" installieren. Ohne dieses Paket können DOTS-Entities gar kein Mesh/Material rendern.

    +
    +
  • +
  • + +
    + URP installiert + zugewiesen erledigt +

    Universal RP Paket, ein URP-Asset erstellt (Assets → Create → Rendering → URP Asset) und in Edit → Project Settings → Graphics (+ Quality-Tab) eingetragen. Entities Graphics funktioniert NUR mit URP oder HDRP, nicht mit Built-in RP — das war der Grund, warum die ersten Pillen unsichtbar waren.

    +
    +
  • +
  • + +
    + Materialien auf URP konvertiert erledigt +

    Window → Rendering → Render Pipeline Converter, "Built-in to URP" → Convert Assets. Ohne das sehen Materialien pink aus (Standard-Shader existiert unter URP nicht).

    +
    +
  • +
  • + +
    + Pille-Setup erledigt +

    Ein leeres GameObject mit PilleDotsBootstrap.cs, Mesh + Material im Inspector zugewiesen, vier Wände in der Szene mit Tag Wall.

    +
    +
  • +
  • + +
    + NEU — Unit-Definitionen für Skirmish anlegen +

    Rechtsklick im Project-Fenster (z.B. in einem neuen Ordner Assets/SkirmishData) → Create → Skirmish → Unit Definition. Mindestens 1, besser 2 Stück (z.B. "Grunt" für Nahkampf, "Archer" für Fernkampf). Im Inspector: Werte eintragen (Leben, Tempo, Schaden, Reichweite, Cooldown), Mesh (z.B. Capsule) und Material zuweisen.

    +
    +
  • +
  • + +
    + NEU — SkirmishBootstrap-GameObject +

    Neues leeres GameObject in derselben Szene (kann neben dem Pille-GameObject existieren) → SkirmishBootstrap.cs draufziehen. Im Inspector das Unit-Typen-Array mit deinen Unit-Definitionen befüllen.

    +
    +
  • +
  • + +
    + Pille/Skirmish-Test durchführen +

    Siehe Tastenbelegung unten. Falls beim ersten Compile ein Fehler kommt: melden, kann ich hier nicht selbst kompilieren/testen.

    +
    +
  • +
  • + +
    + NEU — Base/Placement: Für jedes Team ein Grid +

    Neues leeres GameObject (z.B. "Team0_Base") irgendwo neben der Lane in der Szene positionieren, BaseGrid.cs draufziehen, im Inspector Spalten/Reihen/Zellgröße einstellen. Im Scene-View zeigt ein gelbes Gizmo-Raster, wo das Grid tatsächlich liegt — daran ausrichten. Dasselbe für Team 1 an der anderen Seite der Lane wiederholen (zwei separate GameObjects, zwei separate BaseGrid-Komponenten).

    +
    +
  • +
  • + +
    + NEU — Economy pro Team +

    Auf jedem der beiden Base-GameObjects zusätzlich PlayerEconomy.cs draufziehen (Startgold/Wellen-Einkommen im Inspector einstellbar).

    +
    +
  • +
  • + +
    + NEU — Shop + Platzierung pro Team +

    Je ein weiteres GameObject (z.B. "Team0_Shop") mit ShopUI.cs UND PlacementController.cs. Im Inspector verdrahten: baseGrid → das passende Team-Grid, economy → die passende PlayerEconomy, bei ShopUI zusätzlich placementController → das PlacementController-Component auf demselben Objekt, bei PlacementController zusätzlich shopUI → die ShopUI-Component. Beide brauchen dasselbe shopUnits-Array wie SkirmishBootstrapgleiche Reihenfolge! Der Index in diesem Array ist gleichzeitig die UnitTypeId.

    +
    +
  • +
  • + +
    + NEU — WaveManager +

    Ein GameObject mit WaveManager.cs. Im Inspector: bootstrap → das SkirmishBootstrap-GameObject, team0Grid/team1Grid → die beiden BaseGrids, team0Economy/team1Economy → die beiden PlayerEconomy-Komponenten, playerCount nach Bedarf (bestimmt das Wellen-Intervall zwischen 20–40s).

    +
    +
  • +
  • + +
    + NEU — Base/Placement testen +

    Play drücken, im Shop-Balken unten eine Unit anklicken (wird markiert), dann auf das gelbe Grid im 3D-Fenster klicken → Vorschau-Mesh erscheint, Gold wird abgezogen. Warten, bis der Timer oben ("Nächste Welle in: ...") abläuft → platzierte Units erscheinen automatisch am Lane-Eingang und laufen los.

    +
    +
  • +
+ +
+ + + + + + + + + + + +
TasteSystemWirkung
SpacePille1 Pille spawnen
MPille1 Pille löschen
CPille100 Pillen spawnen
KPillePille-gegen-Pille-Kollision an/aus
1Skirmish1 Unit für Team A spawnen
2Skirmish1 Unit für Team B spawnen
3SkirmishTest-Welle (20 Units je Team, zufällig gemischt)
+
+
+ + +
+

System 1 — Pille (Stresstest-Prototyp)

+

Der erste DOTS-Test: möglichst viele Entities gleichzeitig bewegen, um zu sehen, wo die Grenze liegt (Ergebnis: 200.000 bei 61 FPS). 5 Dateien.

+
+ +
+
+ Komponente (Daten) +

PilleComponent.cs

+
Definiert, welche Daten eine einzelne Pille im Speicher hat — sonst nichts, keine Logik.
+
+
    +
  • + +
    + Z. 1–2 +
    using Unity.Entities;
    +using Unity.Mathematics;
    +

    Unity.Entities bringt das ECS-Grundgerüst mit (IComponentData, Entity, EntityManager ...). Unity.Mathematics bringt float3 — einen Vektor mit x/y/z, der Burst-kompatibel ist (im Gegensatz zu UnityEngine.Vector3, das für den High-Speed-Compiler ungeeignet ist).

    +
    +
  • +
  • + +
    + Z. 4 +
    namespace HappyForest.Prototypen
    +

    Ein "Namensraum" — bündelt allen Pille-Code unter einem Namen, damit z.B. Team bei Skirmish nicht mit etwas Gleichnamigem hier kollidiert.

    +
    +
  • +
  • + +
    + Z. 6 +
    public struct PilleComponent : IComponentData
    +

    Eine ECS-"Komponente": reine Daten, kein Verhalten. struct statt class ist Pflicht — ECS-Komponenten müssen "unmanaged" (blittable) sein, damit Burst sie im rohen Speicher extrem schnell verarbeiten kann. IComponentData ist ein leeres Marker-Interface: es sagt Unity "das hier darf an eine Entity gehängt werden".

    +
    +
  • +
  • + +
    + Z. 8 +
    public float3 Velocity;
    +

    Die aktuelle Bewegungsrichtung als Einheitsvektor (Länge ≈ 1), z.B. (1,0,0) = nach rechts.

    +
    +
  • +
  • + +
    + Z. 9 +
    public float Speed;
    +

    Wie schnell sich die Pille bewegt (Einheiten pro Sekunde) — unabhängig von der Richtung.

    +
    +
  • +
+
+ +
+
+ Komponente (Singleton) +

PilleSimulationConfig.cs

+
Ein globaler Einstellungs-Container. Es gibt genau eine Entity mit dieser Komponente — alle Systeme lesen von hier.
+
+
    +
  • + +
    + Z. 1–2 +
    using Unity.Entities;
    +using Unity.Mathematics;
    +

    Gleich wie bei PilleComponent.cs.

    +
    +
  • +
  • + +
    + Z. 6–7 (Kommentar) +
    // Singleton-Komponente: Wird einmal beim Start von PilleDotsBootstrap erzeugt
    +// und steuert das Verhalten aller Systeme (Grenzen, Kollisions-Toggle etc.).
    +

    "Singleton" heißt: von dieser Komponente existiert immer nur eine Entity in der ganzen Welt — wie eine globale Variable, nur ECS-tauglich.

    +
    +
  • +
  • + +
    + Z. 8 +
    public struct PilleSimulationConfig : IComponentData
    +

    Wie zuvor: struct + IComponentData = ECS-Komponente.

    +
    +
  • +
  • + +
    + Z. 10–11 +
    public float3 BoundsMin;
    +public float3 BoundsMax;
    +

    Die minimale/maximale erlaubte Position im Feld (x, y, z) — berechnet aus den "Wall"-Objekten in der Szene.

    +
    +
  • +
  • + +
    + Z. 12 +
    public float PlayHeight;
    +

    Die feste Höhe (Y), auf der alle Pillen bleiben — sie fallen/fliegen nie.

    +
    +
  • +
  • + +
    + Z. 13 +
    public float PilleRadius;
    +

    Der "Radius" einer Pille — Grundlage für die Kollisionsberechnung.

    +
    +
  • +
  • + +
    + Z. 14 +
    public bool PilleCollisionEnabled;
    +

    Der Ein/Aus-Schalter, den die K-Taste umschaltet.

    +
    +
  • +
+
+ +
+
+ System (Burst) +

PilleMovementSystem.cs

+
Bewegt jeden Frame alle Pillen und lässt sie an den Feld-Grenzen abprallen — komplett Burst-kompiliert, läuft parallel über alle CPU-Kerne.
+
+
    +
  • + +
    + Z. 1–4 +
    using Unity.Burst;
    +using Unity.Entities;
    +using Unity.Mathematics;
    +using Unity.Transforms;
    +

    Unity.Burst bringt [BurstCompile] — Unitys Compiler, der C#-Code in echten Maschinencode übersetzt statt ihn normal zu interpretieren. Das ist der Haupttreiber der 200.000-Pillen-Performance. Unity.Transforms bringt LocalTransform — die ECS-Version von Position/Rotation/Scale (Pendant zu Transform bei normalen GameObjects).

    +
    +
  • +
  • + +
    + Z. 10–11 +
    [BurstCompile]
    +public partial struct PilleMovementSystem : ISystem
    +

    ISystem ist das Interface für ein "System" — der Ort, wo Logik passiert (im Gegensatz zu Komponenten, die nur Daten sind). partial, weil Unity im Hintergrund per Code-Generator zusätzlichen Code in dieselbe Struct einfügt. [BurstCompile] markiert: dieser Code soll von Burst übersetzt werden.

    +
    +
  • +
  • + +
    + Z. 13–16 +
    public void OnCreate(ref SystemState state)
    +{
    +    state.RequireForUpdate<PilleSimulationConfig>();
    +}
    +

    OnCreate läuft einmal, wenn das System erzeugt wird. RequireForUpdate sagt: "Führe OnUpdate nur aus, solange mindestens eine PilleSimulationConfig-Entity existiert" — verhindert Fehler, bevor der Bootstrap sie erzeugt hat.

    +
    +
  • +
  • + +
    + Z. 21–22 +
    var config = SystemAPI.GetSingleton<PilleSimulationConfig>();
    +float deltaTime = SystemAPI.Time.DeltaTime;
    +

    Liest die aktuelle Config (Grenzen, Höhe). DeltaTime ist die Zeit seit dem letzten Frame (z.B. 0.016s bei 60 FPS) — macht Bewegung Framerate-unabhängig.

    +
    +
  • +
  • + +
    + Z. 24–30 +
    var job = new MoveAndBounceJob
    +{
    +    DeltaTime = deltaTime,
    +    BoundsMin = config.BoundsMin,
    +    BoundsMax = config.BoundsMax,
    +    PlayHeight = config.PlayHeight
    +};
    +

    Baut ein "Job"-Objekt: ein Päckchen mit allen Werten, die für jede Pille gleich sind (Zeit, Grenzen, Höhe) — der Job wird gleich für jede Pille einmal ausgeführt.

    +
    +
  • +
  • + +
    + Z. 32 +
    state.Dependency = job.ScheduleParallel(state.Dependency);
    +

    ScheduleParallel reiht den Job in Unitys Job-System ein, das ihn über mehrere CPU-Kerne gleichzeitig abarbeitet. state.Dependency sorgt dafür, dass andere Systeme warten, falls sie dieselben Daten brauchen.

    +
    +
  • +
  • + +
    + Z. 37 +
    public partial struct MoveAndBounceJob : IJobEntity
    +

    IJobEntity ist ein spezieller Job-Typ: die Execute()-Methode wird automatisch für jede Entity aufgerufen, die die dort angeforderten Komponenten besitzt — keine manuelle Schleife nötig.

    +
    +
  • +
  • + +
    + Z. 39–42 +
    public float DeltaTime;
    +public float3 BoundsMin;
    +public float3 BoundsMax;
    +public float PlayHeight;
    +

    Die Kopien der Werte aus OnUpdate — in jedem parallelen Thread verfügbar.

    +
    +
  • +
  • + +
    + Z. 44 +
    void Execute(ref LocalTransform transform, ref PilleComponent pille)
    +

    ref bei beiden Parametern heißt: wir dürfen die Werte lesen und ändern. Diese Methode läuft einmal pro Pille.

    +
    +
  • +
  • + +
    + Z. 46–47 +
    if (math.lengthsq(pille.Velocity) < 0.0001f)
    +    pille.Velocity = new float3(1f, 0f, 0f);
    +

    Falls die Velocity (fast) null ist — z.B. gerade erst gespawnt ohne gesetzte Richtung — gib ihr eine Standardrichtung, sonst würde die Pille stehen bleiben. lengthsq statt length, weil Quadrieren schneller ist als Wurzelziehen und man zum Vergleichen keine echte Länge braucht.

    +
    +
  • +
  • + +
    + Z. 49 +
    float3 pos = transform.Position + pille.Velocity * pille.Speed * DeltaTime;
    +

    Die Standard-Bewegungsformel: neue Position = aktuelle Position + Richtung × Geschwindigkeit × verstrichene Zeit.

    +
    +
  • +
  • + +
    + Z. 51–55 +
    if (pos.x < BoundsMin.x || pos.x > BoundsMax.x)
    +{
    +    pille.Velocity.x *= -1f;
    +    pos.x = math.clamp(pos.x, BoundsMin.x, BoundsMax.x);
    +}
    +

    Liegt die neue X-Position außerhalb der Feld-Grenzen: X-Richtung umkehren (Abprall) und Position an die Grenze "klemmen" (clamp), damit sie nicht durch die Wand rutscht.

    +
    +
  • +
  • + +
    + Z. 57–61 +
    if (pos.z < BoundsMin.z || pos.z > BoundsMax.z)
    +{
    +    pille.Velocity.z *= -1f;
    +    pos.z = math.clamp(pos.z, BoundsMin.z, BoundsMax.z);
    +}
    +

    Dieselbe Prüfung für die Z-Achse (die "Tiefe" des Feldes).

    +
    +
  • +
  • + +
    + Z. 63–64 +
    pos.y = PlayHeight;
    +transform.Position = pos;
    +

    Y wird fix auf PlayHeight gesetzt (keine Höhenbewegung), dann wird die berechnete Position zurückgeschrieben — das macht die Pille sichtbar bewegt.

    +
    +
  • +
+
+ +
+
+ System (Burst, togglebar) +

PilleCollisionSystem.cs

+
Prüft jede Pille gegen jede andere (O(n²)) und lässt sie voneinander abprallen — bewusst simpel, nur aktiv wenn die K-Taste es einschaltet.
+
+
    +
  • + +
    + Z. 1–6 +
    using Unity.Burst;
    +using Unity.Collections;
    +using Unity.Entities;
    +using Unity.Jobs;
    +using Unity.Mathematics;
    +using Unity.Transforms;
    +

    Neu hier: Unity.Collections bringt NativeArray/Allocator — manuell verwalteten Speicher außerhalb des normalen C#-Heaps (Pflicht für Burst/Jobs). Unity.Jobs bringt IJob — ein einfacher Job-Typ ohne automatischen Entity-Bezug ("führe diese eine Methode einmal aus", im Gegensatz zu IJobEntity).

    +
    +
  • +
  • + +
    + Z. 10–13 (Kommentar) +
    // Pille-gegen-Pille Kollision. Bewusst als einfacher O(n^2) Burst-Job gebaut,
    +// damit man per "K"-Taste sauber A/B vergleichen kann, ...
    +

    O(n²) heißt: bei doppelt so vielen Pillen gibt es viermal so viele Prüfungen. Bewusst so simpel gehalten, damit man mit K klar sehen kann, was Kollisionsprüfung an FPS kostet.

    +
    +
  • +
  • + +
    + Z. 14 +
    public partial struct PilleCollisionSystem : ISystem
    +

    Auffällig: kein [BurstCompile] auf der äußeren Struct — bewusst, weil OnUpdate hier "normale" Query-Aufrufe macht. Die eigentliche Rechenarbeit steckt im separaten IJob weiter unten, der sehr wohl [BurstCompile] hat.

    +
    +
  • +
  • + +
    + Z. 23–25 +
    var config = SystemAPI.GetSingleton<PilleSimulationConfig>();
    +if (!config.PilleCollisionEnabled)
    +    return;
    +

    Das ist die K-Taste-Abschaltung: ist Kollision nicht aktiviert, bricht das System sofort ab, ohne irgendetwas zu berechnen.

    +
    +
  • +
  • + +
    + Z. 27–30 +
    var query = SystemAPI.QueryBuilder().WithAll<PilleComponent, LocalTransform>().Build();
    +int count = query.CalculateEntityCount();
    +if (count < 2)
    +    return;
    +

    QueryBuilder baut eine "Anfrage" an die ECS-Welt: "gib mir alle Entities mit PilleComponent UND LocalTransform". Bei weniger als 2 Pillen kann es keine Kollision geben.

    +
    +
  • +
  • + +
    + Z. 32–33 +
    var transforms = query.ToComponentDataArray<LocalTransform>(Allocator.TempJob);
    +var pilles = query.ToComponentDataArray<PilleComponent>(Allocator.TempJob);
    +

    Kopiert Positions- und Bewegungsdaten aller Pillen in zwei einfache Arrays — nötig, weil der Job gleich direkt mit rohem Speicher arbeitet, nicht mit der Query selbst.

    +
    +
  • +
  • + +
    + Z. 35 +
    float radiusSum = config.PilleRadius * 2f;
    +

    Zwei Pillen "berühren" sich, wenn ihr Mittelpunkt-Abstand kleiner ist als die Summe beider Radien — bei gleich großen Pillen ist das der doppelte Radius.

    +
    +
  • +
  • + +
    + Z. 37–42 +
    var job = new ResolvePairCollisionsJob
    +{
    +    Transforms = transforms,
    +    Pilles = pilles,
    +    RadiusSq = radiusSum * radiusSum
    +};
    +

    RadiusSq = Radius im Quadrat, nicht die Wurzel — Wurzelziehen ist teuer, und zum reinen Vergleichen von Abständen braucht man sie gar nicht.

    +
    +
  • +
  • + +
    + Z. 44–45 +
    state.Dependency = job.Schedule(state.Dependency);
    +state.Dependency.Complete();
    +

    .Schedule(...)nicht ScheduleParallel! Der Job verändert gleichzeitig mehrere Pillen im selben Array (siehe unten) — parallel wäre das ein Datenwettlauf. .Complete() wartet, bis der Job wirklich fertig ist, weil wir das Ergebnis sofort brauchen.

    +
    +
  • +
  • + +
    + Z. 47–50 +
    query.CopyFromComponentDataArray(pilles);
    +transforms.Dispose();
    +pilles.Dispose();
    +

    Schreibt die (evtl. veränderten) Pillen-Daten zurück in die echten ECS-Komponenten. Dispose() gibt den manuell reservierten Speicher wieder frei — anders als normale C#-Objekte räumt hier kein Garbage Collector automatisch auf.

    +
    +
  • +
  • + +
    + Z. 54–59 +
    [BurstCompile]
    +public struct ResolvePairCollisionsJob : IJob
    +{
    +    [ReadOnly] public NativeArray<LocalTransform> Transforms;
    +    public NativeArray<PilleComponent> Pilles;
    +    public float RadiusSq;
    +

    [ReadOnly] bei Transforms: wir lesen Positionen nur, ändern sie nie (Sicherheits-Markierung für Burst). Pilles hat keine [ReadOnly]-Markierung, weil wir hier die Velocity bei Kollision ändern dürfen.

    +
    +
  • +
  • + +
    + Z. 63–66 +
    int count = Transforms.Length;
    +for (int i = 0; i < count; i++)
    +{
    +    for (int j = i + 1; j < count; j++)
    +

    Zwei verschachtelte Schleifen: die äußere geht durch jede Pille i, die innere durch jede folgende Pille j (j beginnt bei i+1), damit jedes Paar nur einmal geprüft wird.

    +
    +
  • +
  • + +
    + Z. 68–71 +
    float3 delta = Transforms[i].Position - Transforms[j].Position;
    +float distSq = math.lengthsq(delta);
    +
    +if (distSq < RadiusSq && distSq > 0.0001f)
    +

    Berechnet den (quadrierten) Abstand zwischen Pille i und j. Die zweite Bedingung (distSq > 0.0001f) verhindert eine Division durch 0, falls zwei Pillen exakt an derselben Stelle wären.

    +
    +
  • +
  • + +
    + Z. 73 +
    float3 normal = math.normalize(delta);
    +

    Die "Normale" — die Richtung von Pille j zu Pille i, auf Länge 1 gebracht. Das ist die Achse, an der beide Pillen abprallen.

    +
    +
  • +
  • + +
    + Z. 75–82 +
    var pi = Pilles[i];
    +var pj = Pilles[j];
    +
    +pi.Velocity = math.reflect(pi.Velocity, normal);
    +pj.Velocity = math.reflect(pj.Velocity, -normal);
    +
    +Pilles[i] = pi;
    +Pilles[j] = pj;
    +

    math.reflect spiegelt die Geschwindigkeit an der Kollisions-Normale — physikalisch wie ein Ball, der von einer Wand abprallt, nur dass hier die "Wand" die jeweils andere Pille ist (deshalb bei pj die Normale umgekehrt, -normal). Am Ende werden beide veränderten Werte zurück ins Array geschrieben.

    +
    +
  • +
+
+ +
+
+ MonoBehaviour (Brücke) +

PilleDotsBootstrap.cs

+
Das einzige "normale" Unity-Script im Pille-System. Liest Tasten, baut die Entity-Welt auf, spawnt/löscht Pillen. Bewegung und Kollision passieren NICHT hier, sondern in den beiden Systemen oben.
+
+
    +
  • + +
    + Z. 1–7 +
    using Unity.Collections;
    +using Unity.Entities;
    +using Unity.Mathematics;
    +using Unity.Rendering;
    +using Unity.Transforms;
    +using UnityEngine;
    +using UnityEngine.Rendering;
    +

    Neu: Unity.Rendering bringt RenderMeshUtility/RenderMeshDescription/RenderMeshArray/MaterialMeshInfo — die Werkzeuge, um einer Entity zur Laufzeit ein sichtbares Mesh+Material zu geben (kommt aus dem Entities-Graphics-Paket). UnityEngine bringt die normalen Unity-Klassen (MonoBehaviour, Input, Debug, GUI). UnityEngine.Rendering bringt ShadowCastingMode.

    +
    +
  • +
  • + +
    + Z. 15 +
    public class PilleDotsBootstrap : MonoBehaviour
    +

    class statt struct, weil das hier ein ganz normales Unity-Component ist (kein ECS) — es lebt auf einem GameObject in der Szene, kein Burst.

    +
    +
  • +
  • + +
    + Z. 17–26 +
    [SerializeField] private Mesh pilleMesh;
    +[SerializeField] private Material pilleMaterial;
    +[SerializeField] private float speed = 5f;
    +[SerializeField] private float pilleRadius = 0.5f;
    +[SerializeField] private float spawnHeight = 1f;
    +[SerializeField] private string wallTag = "Wall";
    +[SerializeField] private float actionDelay = 0.05f;
    +

    Alle Werte, die du im Inspector einstellst: Mesh/Material (Pflicht), Bewegungstempo, Kollisionsradius, Spawn-Höhe, welcher Tag als "Wand" zählt, und wie viel Zeit mindestens zwischen zwei Tastendruck-Aktionen liegen muss (verhindert z.B. 60 Spawns pro Sekunde bei gehaltener Leertaste).

    +
    +
  • +
  • + +
    + Z. 28–37 +
    private EntityManager entityManager;
    +private Entity pilleTemplateEntity;
    +private EntityQuery pilleQuery;
    +private EntityQuery configQuery;
    +private int pilleCount = 0;
    +private float lastActionTime = 0f;
    +private bool collisionEnabledCache = false;
    +private Texture2D backgroundTexture;
    +

    Interne Zustände: der Zugang zur ECS-Welt (entityManager), die als Vorlage dienende Template-Entity, zwei "gespeicherte Suchanfragen" (Queries, schneller als sie jedes Mal neu zu bauen), ein Zähler fürs Overlay, der Zeitpunkt der letzten Aktion, ein gecachter Kollisions-Status fürs Overlay, und die Hintergrundtextur für die Lesbarkeit.

    +
    +
  • +
  • + +
    + Z. 41–46 +
    if (pilleMesh == null || pilleMaterial == null)
    +{
    +    Debug.LogError("❌ ...");
    +    enabled = false;
    +    return;
    +}
    +

    Frühzeitiger Abbruch, falls du vergisst, Mesh/Material im Inspector zu setzen — enabled = false schaltet das ganze Script ab, statt später mit unklaren Fehlern abzustürzen.

    +
    +
  • +
  • + +
    + Z. 48 +
    entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    +

    Holt sich den Zugang zur "Standard-ECS-Welt", die Unity beim Spielstart automatisch anlegt. Der EntityManager ist das zentrale Werkzeug, um Entities/Komponenten zu erstellen, zu lesen und zu ändern.

    +
    +
  • +
  • + +
    + Z. 50–58 +
    SetupSimulationConfig();
    +SetupPilleTemplate();
    +pilleQuery = entityManager.CreateEntityQuery(...);
    +configQuery = entityManager.CreateEntityQuery(...);
    +backgroundTexture = MakeTex(...);
    +Debug.Log("✅ ...");
    +

    Ruft die zwei Setup-Methoden auf (Details unten), baut die zwei Queries einmalig, erzeugt die Hintergrundtextur fürs Overlay, und bestätigt in der Console, dass alles bereit ist.

    +
    +
  • +
  • + +
    + Z. 63–74 — SetupSimulationConfig() +
    var wallBounds = ComputeWallBounds();
    +float margin = pilleRadius + 0.1f;
    +var configEntity = entityManager.CreateEntity(typeof(PilleSimulationConfig));
    +entityManager.SetComponentData(configEntity, new PilleSimulationConfig { ... });
    +

    Berechnet die Feld-Grenzen aus den Wänden, zieht einen kleinen Sicherheitsabstand (margin) ab, damit Pillen nicht IN der Wand spawnen/stecken, erstellt dann die eine Singleton-Entity und befüllt sie.

    +
    +
  • +
  • + +
    + Z. 77–100 — ComputeWallBounds() +
    var walls = GameObject.FindGameObjectsWithTag(wallTag);
    +if (walls.Length == 0) { ... return new Bounds(...); }
    +Bounds combined = default;
    +foreach (var wall in walls) { ... combined.Encapsulate(rend.bounds); }
    +

    Sucht alle Szenen-Objekte mit Tag "Wall", liest deren Renderer-Bounds (die räumliche Ausdehnung) und vereinigt sie zu einer Gesamt-Box (Encapsulate) — das ergibt automatisch die Feld-Größe, egal wie die Wände gebaut sind. Gibt es keine Wände, wird eine Standard-Größe (50×10×100) benutzt statt abzustürzen.

    +
    +
  • +
  • + +
    + Z. 104–107 — SetupPilleTemplate() Teil 1 +
    pilleTemplateEntity = entityManager.CreateEntity(
    +    typeof(PilleComponent),
    +    typeof(LocalTransform),
    +    typeof(LocalToWorld));
    +

    Erstellt eine einzelne "Vorlagen-Entity" mit den drei Basis-Komponenten, die jede Pille braucht. LocalToWorld wird zusätzlich zu LocalTransform gebraucht, damit das Rendering-System weiß, wo im Raum die Entity tatsächlich steht.

    +
    +
  • +
  • + +
    + Z. 109–117 — SetupPilleTemplate() Teil 2 +
    var desc = new RenderMeshDescription(ShadowCastingMode.On, receiveShadows: true);
    +var renderMeshArray = new RenderMeshArray(new[] { pilleMaterial }, new[] { pilleMesh });
    +RenderMeshUtility.AddComponents(pilleTemplateEntity, entityManager, desc, renderMeshArray,
    +    MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));
    +

    Das ist der Kern, der DOTS-Entities überhaupt sichtbar macht: RenderMeshUtility.AddComponents hängt der Entity alle nötigen Rendering-Komponenten an (Mesh, Material, Bounds) — ohne diesen Aufruf bleibt jede Entity unsichtbar, egal wie korrekt die Bewegung läuft (das war genau das Problem in der Baker/SubScene-Sackgasse davor).

    +
    +
  • +
  • + +
    + Z. 119–126 — SetupPilleTemplate() Teil 3 +
    entityManager.SetComponentData(pilleTemplateEntity, LocalTransform.FromPosition(...));
    +entityManager.SetComponentData(pilleTemplateEntity, new PilleComponent { Velocity = ..., Speed = speed });
    +

    Setzt die Startwerte der Vorlage: Position (0, spawnHeight, 0) und eine Standard-Velocity/Speed. Jede spätere Kopie startet mit diesen Werten, bevor sie individuell überschrieben werden.

    +
    +
  • +
  • + +
    + Z. 128–131 — SetupPilleTemplate() Teil 4 +
    // WICHTIG: Prefab-Tag, damit dieses Template NICHT von den Queries/Systemen
    +// (Bewegung, Kollision, Zählung) erfasst wird ...
    +entityManager.AddComponent<Prefab>(pilleTemplateEntity);
    +

    Der Prefab-Tag ist ein eingebauter ECS-Mechanismus: Entities mit diesem Tag werden von normalen Queries automatisch ausgeschlossen (die Vorlage selbst bewegt sich also nicht mit). Ruft man Instantiate() auf, entfernt Unity den Tag bei der Kopie automatisch — die Kopie ist dann eine ganz normale, aktive Pille.

    +
    +
  • +
  • + +
    + Z. 134–160 — Update() +
    if (Input.GetKeyDown(KeyCode.K)) ToggleCollision();
    +if (Time.time - lastActionTime < actionDelay) return;
    +if (Input.GetKey(KeyCode.Space)) { SpawnPille(); ... }
    +else if (Input.GetKey(KeyCode.M)) { DeletePille(); ... }
    +else if (Input.GetKey(KeyCode.C)) { for (...) SpawnPille(); ... }
    +

    Läuft jeden Frame. K wird mit GetKeyDown geprüft (reagiert nur auf den Moment des Drückens) und ist von der Cooldown-Sperre ausgenommen. Die anderen drei Tasten nutzen GetKey (reagieren, solange gedrückt gehalten wird) und respektieren actionDelay als Mindestabstand zwischen zwei Aktionen.

    +
    +
  • +
  • + +
    + Z. 162–173 — SpawnPille() Teil 1 +
    float3 spawnPos = new float3(0, spawnHeight, 0);
    +int existing = pilleQuery.CalculateEntityCount();
    +if (existing > 0)
    +{
    +    var transforms = pilleQuery.ToComponentDataArray<LocalTransform>(Allocator.Temp);
    +    int idx = UnityEngine.Random.Range(0, transforms.Length);
    +    spawnPos = transforms[idx].Position;
    +    transforms.Dispose();
    +}
    +

    Bestimmt den Spawn-Ort: Standardmäßig die Mitte, aber falls schon Pillen existieren, wird eine zufällige davon ausgewählt und die neue Pille dort platziert — deshalb "wachsen" die Pillen-Haufen organisch statt alle exakt an einem Punkt zu spawnen.

    +
    +
  • +
  • + +
    + Z. 175–187 — SpawnPille() Teil 2 +
    Entity newPille = entityManager.Instantiate(pilleTemplateEntity);
    +float angle = UnityEngine.Random.Range(0f, math.PI * 2f);
    +var velocity = new float3(math.cos(angle), 0, math.sin(angle));
    +entityManager.SetComponentData(newPille, LocalTransform.FromPosition(spawnPos));
    +entityManager.SetComponentData(newPille, new PilleComponent { Velocity = velocity, Speed = speed });
    +pilleCount++;
    +

    Instantiate erzeugt eine echte Kopie der Vorlage (inkl. Mesh/Material, ohne den Prefab-Tag). Ein zufälliger Winkel (0 bis 360°, in Radiant) ergibt eine zufällige horizontale Startrichtung via Kosinus/Sinus. Dann werden Position und Bewegungsrichtung individuell gesetzt.

    +
    +
  • +
  • + +
    + Z. 190–199 — DeletePille() +
    if (pilleQuery.CalculateEntityCount() == 0) return;
    +var entities = pilleQuery.ToEntityArray(Allocator.Temp);
    +entityManager.DestroyEntity(entities[0]);
    +entities.Dispose();
    +pilleCount--;
    +

    Holt sich alle existierenden Pillen-Entities als Liste und löscht einfach die erste — welche das konkret ist, ist beliebig, es geht nur darum, den Bestand zu verringern.

    +
    +
  • +
  • + +
    + Z. 201–211 — ToggleCollision() +
    var configEntity = configQuery.GetSingletonEntity();
    +var cfg = entityManager.GetComponentData<PilleSimulationConfig>(configEntity);
    +cfg.PilleCollisionEnabled = !cfg.PilleCollisionEnabled;
    +entityManager.SetComponentData(configEntity, cfg);
    +collisionEnabledCache = cfg.PilleCollisionEnabled;
    +

    Structs in C# sind Werttypen — man kann eine Komponente nicht "an Ort und Stelle" ändern, man muss sie lesen, in der Kopie den Wert umdrehen (!cfg...) und dann komplett zurückschreiben. collisionEnabledCache ist eine Kopie nur fürs Overlay, damit OnGUI nicht jeden Frame extra die ECS-Welt abfragen muss.

    +
    +
  • +
  • + +
    + Z. 213–219 — MakeTex() +
    var tex = new Texture2D(1, 1);
    +tex.SetPixel(0, 0, color);
    +tex.Apply();
    +return tex;
    +

    Erzeugt zur Laufzeit eine winzige 1×1-Pixel-Textur in einer Farbe — beim Zeichnen wird sie auf ein großes Rechteck gestreckt, was optisch wie eine einfarbige Fläche aussieht. Günstiger, als eine Bild-Datei mitzuliefern.

    +
    +
  • +
  • + +
    + Z. 221–230 — OnGUI() +
    GUI.DrawTexture(new Rect(5, 5, 430, 90), backgroundTexture);
    +GUI.color = Color.white;
    +GUI.Label(..., $"Pillen: {pilleCount} | FPS: {(int)(1f / Time.deltaTime)}");
    +

    Zeichnet erst die dunkle Hintergrundbox (für Lesbarkeit/Kontrast), dann die Text-Labels in Weiß darüber. Die FPS-Berechnung 1f / Time.deltaTime ist der Kehrwert der Frame-Zeit — bei 0.016s Frame-Zeit ergibt das ≈62 FPS.

    +
    +
  • +
  • + +
    + Z. 232–236 — OnDestroy() +
    if (backgroundTexture != null)
    +    Destroy(backgroundTexture);
    +

    Räumt die manuell erzeugte Textur beim Beenden/Entfernen des Objekts auf — Texturen werden vom Garbage Collector nicht automatisch vollständig freigegeben (sie belegen auch GPU-Speicher).

    +
    +
  • +
+
+ + +
+

System 2 — Skirmish (Kampf-Kern)

+

Phase 1+2 aus dem Implementierungsplan: zwei Teams, Lane-Bewegung, Grid-basiertes Targeting, Kampf, Tod. 9 Dateien. Noch OHNE Base/Placement-UI, Gore/Physik und Upgrades (Phase 3–5).

+
+ +
+
+ ScriptableObject (Editor-Daten) +

Skirmish/Data/UnitDefinition.cs

+
Kein ECS-Code — eine Vorlage, die du im Editor als Asset anlegst und mit Werten befüllst (z.B. "Grunt", "Archer"). Wird beim Spawnen in ECS-Komponenten übersetzt.
+
+
    +
  • + +
    + Z. 3–14 +
    public enum AttackType : byte { Melee = 0, Ranged = 1 }
    +public enum TargetType : byte { Ground = 0 }
    +

    Zwei Aufzählungstypen. : byte heißt, sie werden intern als 1-Byte-Zahl gespeichert statt der C#-Standardgröße (4 Byte, int) — spart Speicher, wenn später tausende Units diese Werte tragen. TargetType hat aktuell nur "Ground", ist aber schon vorbereitet für z.B. "Air" später.

    +
    +
  • +
  • + +
    + Z. 17 +
    [CreateAssetMenu(fileName = "UnitDefinition", menuName = "Skirmish/Unit Definition")]
    +

    Dieses Attribut erzeugt den Rechtsklick-Menüpunkt "Create → Skirmish → Unit Definition" im Project-Fenster — so legst du neue Unit-Typen als Assets an, ohne Code zu schreiben.

    +
    +
  • +
  • + +
    + Z. 18 +
    public class UnitDefinition : ScriptableObject
    +

    ScriptableObject statt MonoBehaviour: lebt nicht auf einem GameObject in der Szene, sondern als eigenständige Asset-Datei im Project-Fenster — genau richtig für "Daten, die mehrfach wiederverwendet werden" (viele Units können dieselbe Definition referenzieren).

    +
    +
  • +
  • + +
    + Z. 20–21 +
    public string unitId = "Unit";
    +public int cost = 10;
    +

    Ein Name zur Wiedererkennung und die Gold-Kosten — cost wird erst ab Phase 3 (Base/Shop) tatsächlich benutzt, ist aber jetzt schon Teil der Definition.

    +
    +
  • +
  • + +
    + Z. 24–30 (Header "Combat") +
    public float maxHealth = 100f;
    +public float moveSpeed = 3f;
    +public float attackDamage = 10f;
    +public float attackRange = 1.5f;
    +public float attackCooldown = 1f;
    +public AttackType attackType = AttackType.Melee;
    +public TargetType targetType = TargetType.Ground;
    +

    Die eigentlichen Kampfwerte, die du im Inspector pro Unit-Typ einstellst: maximales Leben, Lauftempo, Schaden pro Treffer, wie nah ein Ziel sein muss, und wie viele Sekunden zwischen zwei Angriffen liegen.

    +
    +
  • +
  • + +
    + Z. 32–34 (Header "Rendering") +
    public Mesh mesh;
    +public Material material;
    +

    Wie sieht die Unit aus — genau wie pilleMesh/pilleMaterial beim Pille-Bootstrap, nur diesmal pro Unit-Typ statt global.

    +
    +
  • +
+
+ +
+
+ Komponenten (Laufzeit-Daten) +

Skirmish/Components/UnitComponents.cs

+
Die ECS-Komponenten, die eine gespawnte Kampf-Unit tatsächlich trägt — abgeleitet aus UnitDefinition, aber davon losgelöst (wichtig für Upgrades später).
+
+
    +
  • + +
    + Team-Struct +
    public struct Team : IComponentData
    +{
    +    public byte Value;
    +}
    +

    0 = Team A, 1 = Team B. Ein byte reicht locker für zwei Teams (in Zukunft theoretisch bis zu 256).

    +
    +
  • +
  • + +
    + CombatStats-Struct +
    public struct CombatStats : IComponentData
    +{
    +    public float MoveSpeed;
    +    public float AttackDamage;
    +    public float AttackRange;
    +    public float AttackCooldown;
    +    public byte AttackType;
    +}
    +

    Das ist der zentrale Trick aus dem Plan: Systeme lesen nur diese Komponente, nie UnitDefinition direkt. Der Wert wird einmal beim Spawnen aus der Definition (später: × Upgrade-Multiplikator) hineinkopiert. So kann man später Upgrades einbauen, ohne dass irgendein Kampf-System umgeschrieben werden muss.

    +
    +
  • +
  • + +
    + Health-Struct +
    public struct Health : IComponentData
    +{
    +    public float Current;
    +    public float Max;
    +}
    +

    Aktuelles und maximales Leben getrennt gespeichert — Max wird für Phase 3+ gebraucht (z.B. eine Lebensbalken-Anzeige "Current / Max").

    +
    +
  • +
  • + +
    + AttackCooldownTimer-Struct +
    public struct AttackCooldownTimer : IComponentData
    +{
    +    public float Value;
    +}
    +

    Zählt in Sekunden runter, bis die Unit wieder angreifen darf — analog zu einer Abklingzeit bei Fähigkeiten.

    +
    +
  • +
  • + +
    + Target-Struct +
    public struct Target : IComponentData
    +{
    +    public Entity Value;
    +}
    +

    Speichert, WELCHE andere Entity gerade als Kampf-Ziel gilt. Entity.Null heißt "kein Ziel gerade" — dann läuft die Unit stattdessen weiter Richtung Gegner-Base.

    +
    +
  • +
  • + +
    + UnitTypeId-Struct +
    public struct UnitTypeId : IComponentData
    +{
    +    public int Value;
    +}
    +

    Merkt sich, aus welchem Eintrag im unitTypes-Array diese Unit stammt — noch ungenutzt, wird in Phase 4 gebraucht, um beim Tod die richtigen Gore-Teile (Kopf/Arm/Waffe) zuzuordnen.

    +
    +
  • +
  • + +
    + DeadTag-Struct +
    public struct DeadTag : IComponentData
    +{
    +}
    +

    Ein "Tag" ohne jegliche Daten (leerer Struct-Body) — dient nur als Markierung "diese Entity ist tot, wird gleich entfernt". Systeme können mit WithNone<DeadTag> tote Units einfach aus ihren Abfragen ausschließen.

    +
    +
  • +
+
+ +
+
+ Komponente +

Skirmish/Components/MovementComponents.cs

+
Ein einziges Feld: wohin läuft die Unit, wenn sie kein Kampfziel hat.
+
+
    +
  • + +
    + Gesamte Datei +
    public struct LaneMoveTarget : IComponentData
    +{
    +    public float3 Value;
    +}
    +

    Ein fester Zielpunkt — die gegnerische Base. Team A bekommt beim Spawn die Position von Team B eingetragen und umgekehrt. Der Kommentar in der Datei merkt schon an: für eine später geknickte Lane (mehrere Wegpunkte) würde man hier ein BlobArray<float3> + einen Index ergänzen, ohne LaneMovementSystem umbauen zu müssen.

    +
    +
  • +
+
+ +
+
+ System (Burst, parallel) +

Skirmish/Systems/LaneMovementSystem.cs

+
Läuft geradeaus Richtung Gegner-Base. Sobald ein Kampfziel gesetzt ist, bleibt die Unit stehen — CombatSystem übernimmt.
+
+
    +
  • + +
    + OnCreate +
    public void OnCreate(ref SystemState state)
    +{
    +    state.RequireForUpdate<CombatStats>();
    +}
    +

    Wie beim Pille-Movement-System: läuft erst, sobald mindestens eine Unit mit Kampfwerten existiert.

    +
    +
  • +
  • + +
    + OnUpdate +
    var job = new MoveTowardTargetJob { DeltaTime = SystemAPI.Time.DeltaTime };
    +state.Dependency = job.ScheduleParallel(state.Dependency);
    +

    Baut den Job mit der aktuellen Frame-Zeit und schickt ihn parallel über alle Units — exakt dasselbe Muster wie PilleMovementSystem.

    +
    +
  • +
  • + +
    + [WithNone(typeof(DeadTag))] +
    [BurstCompile]
    +[WithNone(typeof(DeadTag))]
    +public partial struct MoveTowardTargetJob : IJobEntity
    +

    Dieses Attribut auf dem Job selbst filtert automatisch tote Units aus der Abfrage heraus — Leichen sollen sich schließlich nicht mehr bewegen.

    +
    +
  • +
  • + +
    + Execute-Signatur +
    void Execute(ref LocalTransform transform, in CombatStats stats, in LaneMoveTarget laneTarget, in Target target)
    +

    ref bei transform (wir ändern die Position), in bei den anderen drei (wir lesen sie nur, ändern sie hier nicht — schneller als ref, weil Burst dann weiß, dass es sich keine Rückschreibe-Gedanken machen muss).

    +
    +
  • +
  • + +
    + Ziel-Check +
    if (target.Value != Entity.Null)
    +    return;
    +

    Hat die Unit gerade ein Kampfziel, wird hier sofort abgebrochen — CombatSystem kümmert sich dann ums Stehenbleiben-und-Angreifen, LaneMovementSystem lässt die Finger davon.

    +
    +
  • +
  • + +
    + Restweg-Berechnung +
    float3 toTarget = laneTarget.Value - transform.Position;
    +float dist = math.length(toTarget);
    +if (dist < 0.05f)
    +    return;
    +

    Vektor und Distanz zum Ziel. Ist die Unit praktisch schon da (unter 5 cm entfernt), wird nichts mehr bewegt — verhindert ein sichtbares "Zittern" durch minimale Überschreitungen.

    +
    +
  • +
  • + +
    + Bewegung mit Schritt-Begrenzung +
    float3 dir = toTarget / dist;
    +transform.Position += dir * math.min(stats.MoveSpeed * DeltaTime, dist);
    +

    Normiert die Richtung (Division durch die eigene Länge = Länge 1), bewegt sich dann Richtung Ziel — math.min(...) sorgt dafür, dass die Unit niemals über das Ziel hinausschießt, auch nicht bei großen Zeitsprüngen (z.B. kurzem Frame-Ruckler).

    +
    +
  • +
+
+ +
+
+ System (Burst, Singleton-Container) +

Skirmish/Systems/BuildLaneGridSystem.cs

+
Baut jeden Frame ein "Bucket-Grid" entlang der Lane, damit TargetingSystem nicht jede Unit gegen jede prüfen muss (das wäre bei 5.000 Units zu langsam).
+
+
    +
  • + +
    + LaneGridSingleton-Struct +
    public struct LaneGridSingleton : IComponentData
    +{
    +    public NativeParallelMultiHashMap<int, Entity> Buckets;
    +    public float BucketSize;
    +}
    +

    Eine "HashMap", die zu einer Zahl (dem Bucket-Index) mehrere Entities speichern kann (deshalb "Multi"). Man kann eine NativeContainer wie diese direkt in einer Komponente speichern, solange sie unmanaged bleibt — hier als Singleton, damit jedes System per SystemAPI.GetSingleton Zugriff bekommt.

    +
    +
  • +
  • + +
    + [UpdateBefore(typeof(TargetingSystem))] +
    [UpdateBefore(typeof(TargetingSystem))]
    +public partial struct BuildLaneGridSystem : ISystem
    +

    Erzwingt die Reihenfolge: dieses System muss VOR TargetingSystem laufen, sonst würde Targeting mit einem leeren oder veralteten Grid arbeiten.

    +
    +
  • +
  • + +
    + OnCreate +
    if (!SystemAPI.HasSingleton<LaneGridSingleton>())
    +{
    +    var entity = state.EntityManager.CreateEntity(typeof(LaneGridSingleton));
    +    state.EntityManager.SetComponentData(entity, new LaneGridSingleton
    +    {
    +        Buckets = new NativeParallelMultiHashMap<int, Entity>(65536, Allocator.Persistent),
    +        BucketSize = 5f
    +    });
    +}
    +

    Erzeugt die Singleton-Entity nur, falls sie noch nicht existiert. Kapazität fest auf 65.536 gesetzt (statt dynamisch wachsend) — das vermeidet, dass ein Reallokieren während ein paralleler Job gerade hineinschreibt, Probleme macht. Allocator.Persistent heißt: bleibt bestehen, bis wir es explizit wieder freigeben (nicht wie Temp/TempJob, die nur kurzlebig sind).

    +
    +
  • +
  • + +
    + OnDestroy +
    if (SystemAPI.HasSingleton<LaneGridSingleton>())
    +{
    +    var grid = SystemAPI.GetSingleton<LaneGridSingleton>();
    +    if (grid.Buckets.IsCreated)
    +        grid.Buckets.Dispose();
    +}
    +

    Gibt den mit Persistent reservierten Speicher beim Beenden frei — das ist Pflicht, sonst merkt sich Unity im Editor ein "Speicherleck" und beschwert sich beim nächsten Play.

    +
    +
  • +
  • + +
    + OnUpdate +
    var gridRW = SystemAPI.GetSingletonRW<LaneGridSingleton>();
    +gridRW.ValueRW.Buckets.Clear();
    +var job = new FillGridJob { BucketSize = ..., Writer = gridRW.ValueRW.Buckets.AsParallelWriter() };
    +state.Dependency = job.ScheduleParallel(state.Dependency);
    +

    GetSingletonRW gibt eine beschreibbare Referenz (statt einer Kopie). Clear() leert das Grid jeden Frame komplett (die alte Speicherreservierung bleibt aber erhalten — nur der Inhalt wird gelöscht). AsParallelWriter() erlaubt mehreren Threads gleichzeitig sicher hineinzuschreiben.

    +
    +
  • +
  • + +
    + FillGridJob-Attribute +
    [BurstCompile]
    +[WithAll(typeof(Team))]
    +[WithNone(typeof(DeadTag))]
    +public partial struct FillGridJob : IJobEntity
    +

    [WithAll(typeof(Team))] ist wichtig: ohne dieses Attribut würde die automatische Query nur nach LocalTransform filtern (dem einzigen Parameter, den Execute direkt nutzt) — das würde auch die Pillen ins Grid mitziehen, falls beide Systeme gleichzeitig in derselben Szene laufen! Team ist eine Komponente, die nur Skirmish-Units haben.

    +
    +
  • +
  • + +
    + Execute +
    void Execute(Entity entity, in LocalTransform transform)
    +{
    +    int bucket = (int)math.floor(transform.Position.z / BucketSize);
    +    Writer.Add(bucket, entity);
    +}
    +

    Berechnet, in welchen "Slot" entlang der Z-Achse (der Lane-Richtung) diese Unit fällt — z.B. bei BucketSize = 5 landen alle Units zwischen Z=10 und Z=15 im Bucket 2. Dann wird die Entity in genau diesen Slot eingetragen. Das ist der ganze Trick: statt 5.000 gegen 5.000 zu prüfen, muss man später nur noch den eigenen Bucket + die zwei Nachbar-Buckets durchsuchen.

    +
    +
  • +
+
+ +
+
+ System (Burst, parallel) +

Skirmish/Systems/TargetingSystem.cs

+
Sucht für jede Unit ein gültiges Kampfziel — aber nur, wenn nötig, nicht jeden Frame neu.
+
+
    +
  • + +
    + OnUpdate +
    var grid = SystemAPI.GetSingleton<LaneGridSingleton>();
    +var job = new AcquireTargetJob { Buckets = grid.Buckets, ..., TeamLookup = SystemAPI.GetComponentLookup<Team>(true), ... };
    +state.Dependency = job.ScheduleParallel(state.Dependency);
    +

    Holt das fertige Grid (von BuildLaneGridSystem) und drei ComponentLookups — das sind "wahlfreie Zugriffe": während der Job über EINE Unit iteriert, kann er trotzdem gezielt Daten von JEDER ANDEREN Entity nachschlagen (Team, Position, Tot-Status). Das true markiert die Lookups als nur-lesend.

    +
    +
  • +
  • + +
    + Execute-Signatur +
    void Execute(Entity entity, ref Target target, in Team team, in CombatStats stats, in LocalTransform transform)
    +

    Läuft für jede lebende Unit (Tote sind per [WithNone(typeof(DeadTag))] ausgeschlossen). entity ist die eigene Entity — gebraucht, um sich selbst später nicht versehentlich als eigenes Ziel zu wählen.

    +
    +
  • +
  • + +
    + Gültigkeits-Check des aktuellen Ziels +
    float rangeSq = stats.AttackRange * stats.AttackRange;
    +if (target.Value != Entity.Null)
    +{
    +    bool stillValid = !DeadLookup.HasComponent(target.Value)
    +        && TransformLookup.HasComponent(target.Value)
    +        && math.distancesq(transform.Position, TransformLookup[target.Value].Position) <= rangeSq;
    +    if (stillValid) return;
    +    target.Value = Entity.Null;
    +}
    +

    Bevor überhaupt neu gesucht wird: ist das aktuelle Ziel noch am Leben, existiert es noch, und ist es noch in Reichweite? Wenn ja, sofort zurück — das spart die teure Grid-Suche in den meisten Frames, weil eine Unit meist mehrere Angriffe hintereinander auf dasselbe Ziel macht.

    +
    +
  • +
  • + +
    + Bucket-Bestimmung +
    int myBucket = (int)math.floor(transform.Position.z / BucketSize);
    +float bestDistSq = float.MaxValue;
    +Entity best = Entity.Null;
    +

    Der eigene Bucket (genau wie in FillGridJob berechnet), plus zwei Variablen, um während der Suche den bisher besten (nächsten) Kandidaten zu merken.

    +
    +
  • +
  • + +
    + Nachbar-Buckets durchsuchen +
    for (int offset = -1; offset <= 1; offset++)
    +{
    +    int bucket = myBucket + offset;
    +    if (!Buckets.TryGetFirstValue(bucket, out Entity candidate, out var it))
    +        continue;
    +    do { ... } while (Buckets.TryGetNextValue(out candidate, ref it));
    +}
    +

    Durchsucht den eigenen Bucket UND je einen links/rechts davon (offset -1, 0, 1) — deckt so auch Fälle ab, wo ein Gegner knapp jenseits der Bucket-Grenze steht. TryGetFirstValue/TryGetNextValue ist das Standard-Muster, um alle Einträge zu einem Schlüssel aus einer NativeParallelMultiHashMap herauszuholen.

    +
    +
  • +
  • + +
    + Kandidaten-Filter +
    if (candidate == entity) continue;
    +if (!TeamLookup.HasComponent(candidate) || TeamLookup[candidate].Value == team.Value) continue;
    +if (!TransformLookup.HasComponent(candidate)) continue;
    +

    Drei Ausschlussgründe: der Kandidat ist man selbst, der Kandidat gehört zum eigenen Team (keine Freundlich-Feuer!), oder er hat gar keine Position (theoretisch nie der Fall, aber sicherheitshalber geprüft).

    +
    +
  • +
  • + +
    + Nächstes-Ziel-Auswahl +
    float distSq = math.distancesq(transform.Position, TransformLookup[candidate].Position);
    +if (distSq <= rangeSq && distSq < bestDistSq)
    +{
    +    bestDistSq = distSq;
    +    best = candidate;
    +}
    +

    Von allen gültigen Kandidaten in Reichweite wird der nächste gewählt — nicht irgendeiner. Nach der Schleife (Zeile ganz unten: target.Value = best;) wird das Ergebnis gespeichert, egal ob ein Ziel gefunden wurde oder Entity.Null übrig blieb.

    +
    +
  • +
+
+ +
+
+ System (Burst, NICHT parallel) +

Skirmish/Systems/CombatSystem.cs

+
Zieht Cooldowns runter, teilt Schaden aus, markiert Tote. Bewusst single-threaded, um Race Conditions auf geteiltes Leben zu vermeiden.
+
+
    +
  • + +
    + Kommentar + [UpdateAfter] +
    // Bewusst NICHT parallelisiert (kein ScheduleParallel): zwei Units koennten
    +// sonst im selben Frame gleichzeitig dasselbe Ziel treffen (Race Condition ...)
    +[UpdateAfter(typeof(TargetingSystem))]
    +

    Läuft garantiert erst, NACHDEM alle Ziele für diesen Frame feststehen. Der Grund für Single-Thread: zwei Units könnten sonst gleichzeitig (auf verschiedenen CPU-Kernen) versuchen, dieselbe fremde Health zu verändern — das wäre ein klassischer Daten-Wettlauf mit unvorhersehbarem Ergebnis.

    +
    +
  • +
  • + +
    + ECB-Setup +
    var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
    +    .CreateCommandBuffer(state.WorldUnmanaged);
    +var healthLookup = SystemAPI.GetComponentLookup<Health>();
    +

    Ein EntityCommandBuffer (ECB) sammelt "Änderungswünsche" (hier: Komponente hinzufügen), die erst später sicher ausgeführt werden — man darf nämlich nicht mitten in einer laufenden Abfrage direkt Komponenten hinzufügen/entfernen. healthLookup hier ohne true — also lesend UND schreibend, weil wir gleich fremdes Leben verändern.

    +
    +
  • +
  • + +
    + Die Haupt-Schleife +
    foreach (var (target, stats, cooldown) in
    +    SystemAPI.Query<RefRO<Target>, RefRO<CombatStats>, RefRW<AttackCooldownTimer>>()
    +        .WithNone<DeadTag>())
    +

    Ein SystemAPI.Query-foreach ist die "von Hand geschriebene", aber trotzdem Burst-schnelle Alternative zu einem separaten IJobEntity — hier bewusst gewählt, weil wir single-threaded bleiben wollen. RefRO = nur lesen, RefRW = lesen und schreiben.

    +
    +
  • +
  • + +
    + Cooldown runterzählen +
    cooldown.ValueRW.Value -= deltaTime;
    +if (target.ValueRO.Value == Entity.Null) continue;
    +if (cooldown.ValueRO.Value > 0f) continue;
    +

    Jede Unit zählt ihren Cooldown IMMER runter (auch ohne Ziel). Dann zwei Abbruchgründe: kein Ziel gesetzt, oder Cooldown noch nicht abgelaufen — continue springt zur nächsten Unit in der Schleife.

    +
    +
  • +
  • + +
    + Schaden anwenden +
    if (!healthLookup.HasComponent(target.ValueRO.Value)) continue;
    +var targetHealth = healthLookup[target.ValueRO.Value];
    +targetHealth.Current -= stats.ValueRO.AttackDamage;
    +healthLookup[target.ValueRO.Value] = targetHealth;
    +cooldown.ValueRW.Value = stats.ValueRO.AttackCooldown;
    +

    Liest das Leben des Ziels über den Lookup (nicht über die eigene Query — das Ziel ist ja eine ANDERE Entity), zieht den Schaden ab, schreibt zurück, und setzt den eigenen Cooldown auf den vollen Wert zurück (nächster Angriff erst wieder nach AttackCooldown Sekunden).

    +
    +
  • +
  • + +
    + Tod markieren +
    if (targetHealth.Current <= 0f)
    +{
    +    ecb.AddComponent<DeadTag>(target.ValueRO.Value);
    +}
    +

    Fällt das Leben auf 0 oder darunter, wird dem Ziel (nicht dem Angreifer!) über den ECB der DeadTag angehängt. Die Entity wird dadurch NICHT sofort gelöscht — das übernimmt erst DeathSystem im nächsten Durchlauf.

    +
    +
  • +
+
+ +
+
+ System (einfach) +

Skirmish/Systems/DeathSystem.cs

+
Räumt tote Units weg. In Phase 4 kommt hier das Gore-Spawning dazu (braucht Unity.Physics, noch nicht installiert).
+
+
    +
  • + +
    + Struct-Deklaration +
    public partial struct DeathSystem : ISystem
    +

    Kein [BurstCompile] hier — bewusst, weil Debug.Log (in der Schleife) keine Burst-kompatible Operation ist. Da hier ohnehin nur wenige Units pro Frame sterben, kostet das kaum Performance.

    +
    +
  • +
  • + +
    + OnCreate +
    state.RequireForUpdate<DeadTag>();
    +

    Läuft nur, solange mindestens eine Entity gerade den DeadTag trägt — in den meisten Frames (niemand stirbt gerade) macht dieses System dadurch komplett gar nichts.

    +
    +
  • +
  • + +
    + OnUpdate +
    var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
    +    .CreateCommandBuffer(state.WorldUnmanaged);
    +foreach (var (deadTag, entity) in SystemAPI.Query<RefRO<DeadTag>>().WithEntityAccess())
    +{
    +    ecb.DestroyEntity(entity);
    +}
    +

    Findet alle als tot markierten Entities und reiht sie zum Löschen ein (wieder über einen ECB statt direkt, aus demselben Grund wie in CombatSystem). WithEntityAccess() gibt zusätzlich zur Komponente auch die Entity-ID selbst mit in die Schleife.

    +
    +
  • +
+
+ +
+
+ MonoBehaviour (Brücke) +

Skirmish/SkirmishBootstrap.cs

+
Pendant zu PilleDotsBootstrap: baut pro Unit-Typ × Team ein Template, liest Debug-Tasten (1/2/3), zeigt das Overlay.
+
+
    +
  • + +
    + Felder (Header "Unit-Typen"/"Lane"/"Debug") +
    [SerializeField] private UnitDefinition[] unitTypes;
    +[SerializeField] private string wallTag = "Wall";
    +[SerializeField] private float spawnHeight = 1f;
    +[SerializeField] private float spawnSpreadX = 8f;
    +[SerializeField] private int debugWaveCountPerTeam = 20;
    +

    Das Array, das du im Inspector mit deinen UnitDefinition-Assets befüllst. spawnSpreadX ist eine seitliche Streuung beim Spawnen, damit nicht alle Units exakt übereinander starten.

    +
    +
  • +
  • + +
    + private Felder +
    private Entity[] templatesTeam0;
    +private Entity[] templatesTeam1;
    +private float3 team0BasePos;
    +private float3 team1BasePos;
    +private EntityQuery aliveQuery;
    +

    Zwei Template-Arrays (ein Template pro Unit-Typ, getrennt nach Team, weil jedes Team ein anderes LaneMoveTarget braucht), die zwei Basis-Positionen an den Lane-Enden, und eine Query, die fürs Overlay zählt, wie viele Units gerade leben.

    +
    +
  • +
  • + +
    + Start() — Absicherung +
    if (unitTypes == null || unitTypes.Length == 0)
    +{
    +    Debug.LogError("❌ ...");
    +    enabled = false;
    +    return;
    +}
    +

    Bricht sauber ab, falls im Inspector kein einziger Unit-Typ eingetragen wurde.

    +
    +
  • +
  • + +
    + Start() — Lane-Basen +
    var laneBounds = ComputeLaneBounds();
    +team0BasePos = new float3(0, spawnHeight, laneBounds.min.z + 2f);
    +team1BasePos = new float3(0, spawnHeight, laneBounds.max.z - 2f);
    +

    Nutzt dieselbe Wand-basierte Grenzenberechnung wie beim Pille-System (nur diesmal als eigene Methode), aber interpretiert die Z-Achse als "Lane-Länge": Team A startet nahe dem Z-Minimum, Team B nahe dem Z-Maximum, jeweils 2 Einheiten von der Wand entfernt.

    +
    +
  • +
  • + +
    + Start() — Templates pro Team bauen +
    templatesTeam0 = new Entity[unitTypes.Length];
    +templatesTeam1 = new Entity[unitTypes.Length];
    +for (int i = 0; i < unitTypes.Length; i++)
    +{
    +    ...
    +    templatesTeam0[i] = CreateUnitTemplate(unitTypes[i], i, 0, team1BasePos);
    +    templatesTeam1[i] = CreateUnitTemplate(unitTypes[i], i, 1, team0BasePos);
    +}
    +

    Für jeden Unit-Typ werden ZWEI Templates gebaut — eines für Team 0 (das Richtung Team-1-Base läuft) und eines für Team 1 (Richtung Team-0-Base). Vorher wird geprüft, ob Definition/Mesh/Material gesetzt sind (Zeilen davor im Original), sonst Abbruch mit klarer Fehlermeldung.

    +
    +
  • +
  • + +
    + ComputeLaneBounds() +
    var walls = GameObject.FindGameObjectsWithTag(wallTag);
    +...
    +foreach (var wall in walls) { ... combined.Encapsulate(rend.bounds); }
    +return combined;
    +

    1:1 dasselbe Muster wie PilleDotsBootstrap.ComputeWallBounds() — bewusst dupliziert statt geteilt, weil beide Systeme unabhängig voneinander bleiben sollen (kein gemeinsamer Code, der beide gleichzeitig beeinflussen könnte).

    +
    +
  • +
  • + +
    + CreateUnitTemplate() — Entity + Rendering +
    var entity = entityManager.CreateEntity(typeof(Team), typeof(CombatStats), typeof(Health), ...);
    +var desc = new RenderMeshDescription(...);
    +var renderMeshArray = new RenderMeshArray(new[] { def.material }, new[] { def.mesh });
    +RenderMeshUtility.AddComponents(entity, entityManager, desc, renderMeshArray, ...);
    +

    Erstellt eine Entity mit ALLEN Komponenten, die eine Kampf-Unit braucht (Team, Kampfwerte, Leben, Cooldown, Ziel, Typ-ID, Laufziel, Transform), dann exakt dasselbe Rendering-Setup wie beim Pille-Template — nur diesmal mit dem Mesh/Material aus der jeweiligen UnitDefinition.

    +
    +
  • +
  • + +
    + CreateUnitTemplate() — Startwerte + Prefab-Tag +
    entityManager.SetComponentData(entity, new Team { Value = team });
    +entityManager.SetComponentData(entity, new CombatStats { MoveSpeed = def.moveSpeed, ... });
    +entityManager.SetComponentData(entity, new Health { Current = def.maxHealth, Max = def.maxHealth });
    +...
    +entityManager.SetComponentData(entity, new LaneMoveTarget { Value = enemyBasePos });
    +entityManager.AddComponent<Prefab>(entity);
    +

    Kopiert die Werte aus der UnitDefinition in die ECS-Komponenten (das ist der "Bake"-Moment aus dem Plan). LaneMoveTarget wird schon HIER korrekt gesetzt (Team 0 bekommt team1BasePos als Ziel) — jede spätere Kopie erbt das automatisch, ohne dass man es beim Spawnen erneut setzen muss. Der Prefab-Tag schließt das Template wieder von allen Queries aus.

    +
    +
  • +
  • + +
    + Update() +
    if (Input.GetKeyDown(KeyCode.Alpha1)) SpawnUnit(0, 0);
    +if (Input.GetKeyDown(KeyCode.Alpha2)) SpawnUnit(0, 1);
    +if (Input.GetKeyDown(KeyCode.Alpha3)) SpawnWave(debugWaveCountPerTeam);
    +

    GetKeyDown (nicht GetKey) — reagiert nur auf den Moment des Drückens, kein Dauerfeuer beim Halten. Taste 1/2 spawnen immer Unit-Typ 0 (den ersten im Array); für gezielte Typ-Auswahl kommt erst mit der Shop-UI in Phase 3 eine echte Steuerung.

    +
    +
  • +
  • + +
    + SpawnUnit() +
    var template = team == 0 ? templatesTeam0[unitTypeIndex] : templatesTeam1[unitTypeIndex];
    +var basePos = team == 0 ? team0BasePos : team1BasePos;
    +Entity newUnit = entityManager.Instantiate(template);
    +float offsetX = UnityEngine.Random.Range(-spawnSpreadX, spawnSpreadX);
    +entityManager.SetComponentData(newUnit, LocalTransform.FromPosition(basePos + new float3(offsetX, 0, 0)));
    +

    Wählt Template und Basis-Position je nach Team, erzeugt die Kopie, gibt ihr eine zufällige seitliche Streuung (X-Achse) und setzt die Start-Position. LaneMoveTarget muss NICHT erneut gesetzt werden — kommt schon korrekt aus dem Template mit.

    +
    +
  • +
  • + +
    + SpawnWave() +
    for (int i = 0; i < countPerTeam; i++)
    +{
    +    int unitTypeIndex = UnityEngine.Random.Range(0, unitTypes.Length);
    +    SpawnUnit(unitTypeIndex, 0);
    +    SpawnUnit(unitTypeIndex, 1);
    +}
    +

    Spawnt countPerTeam Paare — pro Durchlauf einen zufälligen Unit-Typ für beide Teams gleichzeitig, damit die Test-Welle gemischt und für beide Seiten gleich stark ist.

    +
    +
  • +
  • + +
    + OnGUI() +
    int alive = aliveQuery.CalculateEntityCount();
    +GUI.DrawTexture(new Rect(5, 100, 430, 65), backgroundTexture);
    +GUI.Label(..., $"Skirmish - lebende Units: {alive} | FPS: ...");
    +

    Zählt aktuell lebende Units (Query auf Team + Health) und zeichnet das Overlay bei Y=100 — bewusst UNTER dem Pille-Overlay (das bei Y=5–90 liegt), damit beide gleichzeitig lesbar sind, falls beide Systeme parallel in derselben Szene laufen.

    +
    +
  • +
  • + +
    + Update für Phase 3: SpawnUnitInternal() +
    void SpawnUnit(int unitTypeIndex, byte team)
    +{
    +    float offsetX = UnityEngine.Random.Range(-spawnSpreadX, spawnSpreadX);
    +    SpawnUnitInternal(unitTypeIndex, team, offsetX);
    +}
    +
    +public void SpawnUnitAt(int unitTypeIndex, byte team, Vector3 gridWorldPos)
    +{
    +    SpawnUnitInternal(unitTypeIndex, team, gridWorldPos.x);
    +}
    +
    +void SpawnUnitInternal(int unitTypeIndex, byte team, float x)
    +{
    +    ...
    +    float3 spawnPos = new float3(x, basePos.y, basePos.z);
    +    entityManager.SetComponentData(newUnit, LocalTransform.FromPosition(spawnPos));
    +}
    +

    Für Phase 3 wurde die alte SpawnUnit-Methode in drei Teile aufgeteilt: die ursprüngliche Debug-Variante (zufällige X-Streuung, für Tasten 1/2/3) bleibt intern, dazu kommt eine neue öffentliche Methode SpawnUnitAt, die WaveManager aufruft — sie übernimmt die X-Position aus der Base-Grid-Platzierung, aber Y/Z immer vom festen Lane-Eingang des Teams. Ergebnis: welche Spalte im Base-Grid du befüllst, bestimmt, in welcher "Spur" der Lane die Unit lostäuft.

    +
    +
  • +
+
+ + +
+

System 3 — Base / Placement (Phase 3)

+

Vor-Runden-Platzierung: Units kaufen, per Klick aufs eigene Raster stellen. Komplett außerhalb ECS (MonoBehaviour), weil hier nur ein paar Dutzend Einträge pro Spieler verwaltet werden — kein Performance-Grund für DOTS. 5 Dateien.

+
+ +
+
+ MonoBehaviour (Daten) +

Base/PlayerEconomy.cs

+
Ein simples Gold-Konto pro Team.
+
+
    +
  • + +
    + Felder + Property +
    [SerializeField] private int startingGold = 100;
    +[SerializeField] private int incomePerWave = 50;
    +public int Gold { get; private set; }
    +

    { get; private set; } ist eine C#-"Property": von außen kann jeder Gold LESEN, aber nur die Klasse selbst darf es ÄNDERN (über Spend/AddWaveIncome) — verhindert, dass z.B. ShopUI aus Versehen direkt am Kontostand herumschraubt.

    +
    +
  • +
  • + +
    + Awake() +
    void Awake()
    +{
    +    Gold = startingGold;
    +}
    +

    Awake läuft VOR jedem Start im ganzen Spiel — wichtig, weil andere Scripts (z.B. ShopUI) in ihrem eigenen Start schon einen gültigen Gold-Wert lesen können müssen.

    +
    +
  • +
  • + +
    + Spend() +
    public bool Spend(int amount)
    +{
    +    if (amount > Gold) return false;
    +    Gold -= amount;
    +    return true;
    +}
    +

    Gibt true/false zurück, statt einfach den Kontostand negativ werden zu lassen — der Aufrufer (PlacementController) muss das Ergebnis prüfen und die Platzierung abbrechen, falls false.

    +
    +
  • +
  • + +
    + AddWaveIncome() +
    public void AddWaveIncome()
    +{
    +    Gold += incomePerWave;
    +}
    +

    Wird von WaveManager bei jedem Wellenstart aufgerufen — regelmäßiges Einkommen, unabhängig davon, was gekauft wurde.

    +
    +
  • +
+
+ +
+
+ MonoBehaviour (Daten + Gizmo) +

Base/BaseGrid.cs

+
Das Platzierungsraster für ein Team: welche Zelle enthält welchen Unit-Typ, plus einfache Vorschau-Objekte (kein ECS).
+
+
    +
  • + +
    + Felder + Awake() +
    [SerializeField] private int columns = 6;
    +[SerializeField] private int rows = 4;
    +[SerializeField] private float cellSize = 2f;
    +private int[] occupiedUnitTypeIndex;
    +private GameObject[] previewObjects;
    +
    +void Awake()
    +{
    +    int cellCount = columns * rows;
    +    occupiedUnitTypeIndex = new int[cellCount];
    +    previewObjects = new GameObject[cellCount];
    +    for (int i = 0; i < cellCount; i++)
    +        occupiedUnitTypeIndex[i] = -1;
    +}
    +

    Ein flaches 1D-Array statt eines 2D-Arrays — Zelle (Spalte, Reihe) wird über row * columns + col in einen einzigen Index umgerechnet (einfacher zu handhaben als ein echtes 2D-Array in C#). Alle Zellen starten als -1 = leer.

    +
    +
  • +
  • + +
    + TryGetCellFromRay() — Ebene statt Collider +
    var plane = new Plane(Vector3.up, transform.position);
    +if (!plane.Raycast(ray, out float distance))
    +    return false;
    +Vector3 hitPoint = ray.GetPoint(distance);
    +

    Statt einen echten Boden-Collider zu verlangen (mehr Editor-Aufwand für dich), wird eine reine mathematische horizontale Ebene auf Höhe des Grid-GameObjects aufgespannt. Plane.Raycast berechnet, wo ein Strahl (vom Mauszeiger durch die Kamera) diese gedachte Ebene schneidet.

    +
    +
  • +
  • + +
    + TryGetCellFromRay() — Weltposition zu Zelle +
    Vector3 local = hitPoint - transform.position;
    +int col = Mathf.FloorToInt(local.x / cellSize);
    +int row = Mathf.FloorToInt(local.z / cellSize);
    +if (col < 0 || col >= columns || row < 0 || row >= rows)
    +    return false;
    +cellIndex = row * columns + col;
    +

    Rechnet den Treffpunkt relativ zum Grid-Ursprung um, teilt durch die Zellgröße und rundet ab (FloorToInt) — das ergibt Spalte/Reihe. Liegt der Punkt außerhalb der Grid-Abmessungen, wird false zurückgegeben (Klick daneben zählt nicht).

    +
    +
  • +
  • + +
    + IsOccupied() / Place() +
    public bool IsOccupied(int cellIndex) => occupiedUnitTypeIndex[cellIndex] != -1;
    +
    +public void Place(int cellIndex, int unitTypeIndex, UnitDefinition def)
    +{
    +    occupiedUnitTypeIndex[cellIndex] = unitTypeIndex;
    +    ...
    +    var preview = new GameObject($"Preview_{cellIndex}_{def.unitId}");
    +    preview.transform.SetParent(transform);
    +    preview.transform.position = GetCellWorldPos(cellIndex);
    +    var meshFilter = preview.AddComponent<MeshFilter>();
    +    meshFilter.mesh = def.mesh;
    +    var meshRenderer = preview.AddComponent<MeshRenderer>();
    +    meshRenderer.material = def.material;
    +

    IsOccupied nutzt eine "Expression-bodied"-Schreibweise (=> statt { return ...; }) für eine einzeilige Methode. Place merkt sich den Typ in der Zelle UND erzeugt ein sichtbares, aber komplett dummes Vorschau-GameObject (nur Mesh+Material, keine Scripts, keine Collider, keine ECS-Beteiligung) am Zellmittelpunkt.

    +
    +
  • +
  • + +
    + Clear() +
    public void Clear(int cellIndex)
    +{
    +    occupiedUnitTypeIndex[cellIndex] = -1;
    +    if (previewObjects[cellIndex] != null)
    +    {
    +        Destroy(previewObjects[cellIndex]);
    +        previewObjects[cellIndex] = null;
    +    }
    +}
    +

    Macht eine Zelle wieder leer und entfernt das Vorschau-Objekt. Aktuell von keinem anderen Script aufgerufen — vorbereitet für später (z.B. Rechtsklick zum Entfernen).

    +
    +
  • +
  • + +
    + GetCellWorldPos() +
    public Vector3 GetCellWorldPos(int cellIndex)
    +{
    +    int col = cellIndex % columns;
    +    int row = cellIndex / columns;
    +    return transform.position + new Vector3((col + 0.5f) * cellSize, 0f, (row + 0.5f) * cellSize);
    +}
    +

    Die Umkehrung der Index-Berechnung von oben (% = Rest der Division = Spalte, ganzzahlige Division = Reihe), plus +0.5f, damit die Position die MITTE der Zelle trifft statt die Ecke.

    +
    +
  • +
  • + +
    + GetPlacedUnits() +
    public IEnumerable<(int unitTypeIndex, Vector3 worldPos)> GetPlacedUnits()
    +{
    +    for (int i = 0; i < occupiedUnitTypeIndex.Length; i++)
    +    {
    +        if (occupiedUnitTypeIndex[i] != -1)
    +            yield return (occupiedUnitTypeIndex[i], GetCellWorldPos(i));
    +    }
    +}
    +

    yield return macht diese Methode zu einem "Generator": statt eine ganze Liste auf einmal zu bauen, gibt sie ihre Ergebnisse nach und nach heraus, während WaveManager sie mit foreach durchläuft. Das Tupel (int, Vector3) bündelt zwei Werte, ohne dafür eine eigene Klasse schreiben zu müssen.

    +
    +
  • +
  • + +
    + OnDrawGizmos() +
    void OnDrawGizmos()
    +{
    +    Gizmos.color = Color.yellow;
    +    for (int r = 0; r <= rows; r++) { Gizmos.DrawLine(...); }
    +    for (int c = 0; c <= columns; c++) { Gizmos.DrawLine(...); }
    +}
    +

    Zeichnet ein gelbes Liniengitter NUR im Scene-View des Editors (nie im echten Spiel/Build) — rein zur Orientierung, damit du siehst, wo genau dein Grid liegt und wie groß es ist, ohne Play drücken zu müssen.

    +
    +
  • +
+
+ +
+
+ MonoBehaviour (IMGUI) +

Base/ShopUI.cs

+
Der Kaufbalken am unteren Bildschirmrand. Bewusst OnGUI statt uGUI-Canvas — passt zum Rest des Projekts, braucht keine von Hand gebauten Button-Prefabs.
+
+
Abweichung vom ursprünglichen Plan: Der Implementierungsplan sah ein uGUI-Canvas vor. Umgesetzt wurde stattdessen OnGUI (wie bei Pille/Skirmish-Overlays) — weniger Editor-Fummelarbeit (kein Canvas/EventSystem/Button-Prefab nötig), dafür optisch schlichter. Kann später gegen echtes uGUI getauscht werden, ohne die Logik in PlacementController anzufassen.
+
    +
  • + +
    + Felder +
    [SerializeField] private PlayerEconomy economy;
    +[SerializeField] private PlacementController placementController;
    +[SerializeField] private UnitDefinition[] shopUnits;
    +private Rect shopBarScreenRect;
    +private const float BarHeight = 90f;
    +

    shopBarScreenRect wird jeden Frame in OnGUI neu berechnet und hier zwischengespeichert, damit IsPointerOverUI (unten) darauf zugreifen kann, ohne die Berechnung zu duplizieren.

    +
    +
  • +
  • + +
    + OnGUI() — Balken + Gold +
    float barY = Screen.height - BarHeight;
    +shopBarScreenRect = new Rect(0, barY, Screen.width, BarHeight);
    +GUI.Box(shopBarScreenRect, GUIContent.none);
    +GUI.Label(new Rect(10, barY + 4, 200, 22), $"Gold: {(economy != null ? economy.Gold : 0)}");
    +

    GUI-Koordinaten haben Y=0 OBEN — Screen.height - BarHeight platziert den Balken also am unteren Rand. Der ?:-Ausdruck verhindert einen Absturz, falls economy im Inspector vergessen wurde (zeigt dann einfach 0 an).

    +
    +
  • +
  • + +
    + OnGUI() — Buttons pro Unit-Typ +
    float x = 10;
    +for (int i = 0; i < shopUnits.Length; i++)
    +{
    +    var def = shopUnits[i];
    +    bool isSelected = placementController != null && placementController.SelectedUnitIndex == i;
    +    string label = $"{def.unitId}\n{def.cost}g" + (isSelected ? "  ✓" : "");
    +    if (GUI.Button(new Rect(x, barY + 28, ButtonWidth - 8, 54), label) && placementController != null)
    +        placementController.SelectUnit(i);
    +    x += ButtonWidth;
    +}
    +

    GUI.Button(...) gibt true zurück, GENAU in dem Frame, in dem er angeklickt wurde — dann wird SelectUnit(i) auf dem PlacementController aufgerufen. Ein Häkchen (✓) markiert den gerade ausgewählten Button.

    +
    +
  • +
  • + +
    + IsPointerOverUI() +
    public bool IsPointerOverUI(Vector3 mouseScreenPos)
    +{
    +    Vector2 guiPoint = new Vector2(mouseScreenPos.x, Screen.height - mouseScreenPos.y);
    +    return shopBarScreenRect.Contains(guiPoint);
    +}
    +

    Wichtiger Kniff: Input.mousePosition hat Y=0 UNTEN, GUI-Rects haben Y=0 OBEN — zwei verschiedene Koordinatensysteme. Diese Methode rechnet um und prüft, ob der Mausklick über dem Shop-Balken war. PlacementController fragt das vor jedem Platzierungs-Klick ab, damit ein Klick auf einen Shop-Button NICHT gleichzeitig eine Unit auf dem 3D-Grid platziert.

    +
    +
  • +
+
+ +
+
+ MonoBehaviour (Verbindung) +

Base/PlacementController.cs

+
Verbindet Shop-Auswahl → Klick aufs Grid → Gold-Abzug → tatsächliche Platzierung.
+
+
    +
  • + +
    + SelectedUnitIndex + SelectUnit() +
    public int SelectedUnitIndex { get; private set; } = -1;
    +
    +public void SelectUnit(int index)
    +{
    +    SelectedUnitIndex = index;
    +}
    +

    -1 bedeutet "nichts ausgewählt". ShopUI ruft SelectUnit beim Klick auf einen Button auf — das ist die einzige Möglichkeit, wie sich SelectedUnitIndex von außen ändert (dank private set).

    +
    +
  • +
  • + +
    + Update() — frühe Abbrüche +
    if (SelectedUnitIndex < 0) return;
    +if (!Input.GetMouseButtonDown(0)) return;
    +if (shopUI != null && shopUI.IsPointerOverUI(Input.mousePosition)) return;
    +

    Drei Gründe, sofort abzubrechen: keine Unit ausgewählt, kein frischer Linksklick in diesem Frame, oder der Klick war über dem Shop-Balken (siehe ShopUI.IsPointerOverUI).

    +
    +
  • +
  • + +
    + Update() — Raycast + Zell-Ermittlung +
    var cam = targetCamera != null ? targetCamera : Camera.main;
    +if (cam == null || baseGrid == null) return;
    +Ray ray = cam.ScreenPointToRay(Input.mousePosition);
    +if (!baseGrid.TryGetCellFromRay(ray, out int cellIndex)) return;
    +if (baseGrid.IsOccupied(cellIndex)) return;
    +

    targetCamera != null ? targetCamera : Camera.main — nutzt eine im Inspector zugewiesene Kamera, fällt sonst auf die als "MainCamera" getaggte Kamera zurück. ScreenPointToRay wandelt die 2D-Mausposition in einen 3D-Strahl durchs Spielfeld um. Ist die Zelle schon belegt, wird ebenfalls abgebrochen (kein Überschreiben).

    +
    +
  • +
  • + +
    + Update() — Bezahlen + Platzieren +
    var def = shopUnits[SelectedUnitIndex];
    +if (economy != null && !economy.Spend(def.cost))
    +{
    +    Debug.Log("⚠️ Nicht genug Gold!");
    +    return;
    +}
    +baseGrid.Place(cellIndex, SelectedUnitIndex, def);
    +

    Erst wird versucht zu bezahlen (Spend gibt false zurück, wenn zu wenig Gold da ist) — erst wenn das klappt, wird tatsächlich platziert. Bezahlt wird also NUR bei erfolgreicher Platzierung, nie vorher schon.

    +
    +
  • +
  • + +
    + OnGUI() +
    if (SelectedUnitIndex < 0) return;
    +GUI.Label(new Rect(15, Screen.height - 120, 420, 22),
    +    $"Ausgewählt: {shopUnits[SelectedUnitIndex].unitId} — Klick auf das Grid zum Platzieren");
    +

    Ein kleiner Hinweistext, der nur erscheint, solange gerade eine Unit zum Platzieren ausgewählt ist — reines Feedback, keine Interaktion.

    +
    +
  • +
+
+ +
+
+ MonoBehaviour (Brücke, Timer) +

Base/WaveManager.cs

+
Der Wellen-Takt: alle 20–40 Sekunden werden die aktuell platzierten Units beider Teams als ECS-Entities aufs Schlachtfeld gebracht.
+
+
    +
  • + +
    + Start() +
    interval = Mathf.Clamp(40f - 5f * (playerCount - 1), 20f, 40f);
    +timer = interval;
    +

    Die Formel aus dem Implementierungsplan: bei 1 Spieler 40s, pro zusätzlichem Spieler 5s weniger, aber nie unter 20s (Mathf.Clamp deckelt nach unten UND oben).

    +
    +
  • +
  • + +
    + Update() +
    timer -= Time.deltaTime;
    +if (timer <= 0f)
    +{
    +    TriggerWave();
    +    timer = interval;
    +}
    +

    Ein klassischer Countdown-Timer: jeden Frame etwas Zeit abziehen, bei Erreichen von 0 die Welle auslösen und den Timer für die nächste Runde zurücksetzen.

    +
    +
  • +
  • + +
    + TriggerWave() +
    SpawnFromGrid(team0Grid, 0);
    +SpawnFromGrid(team1Grid, 1);
    +if (team0Economy != null) team0Economy.AddWaveIncome();
    +if (team1Economy != null) team1Economy.AddWaveIncome();
    +

    Spawnt für beide Teams (Reihenfolge egal, beide passieren im selben Frame) und zahlt danach beiden Spielern ihr Wellen-Einkommen aus. Wichtig: das Base-Grid wird nicht geleert — dieselbe Platzierung wird bei der nächsten Welle wieder verwendet, ganz wie im Plan vorgesehen.

    +
    +
  • +
  • + +
    + SpawnFromGrid() +
    foreach (var (unitTypeIndex, worldPos) in grid.GetPlacedUnits())
    +{
    +    bootstrap.SpawnUnitAt(unitTypeIndex, team, worldPos);
    +}
    +

    Läuft durch alle belegten Zellen (dank GetPlacedUnits()s yield return ohne vorher eine Liste zu bauen) und ruft für jede eine echte ECS-Entity-Erzeugung über SkirmishBootstrap.SpawnUnitAt auf — das ist die einzige Stelle in diesen 5 Dateien, wo überhaupt mit ECS "in Kontakt" gekommen wird, und selbst hier nur indirekt über die öffentliche Bootstrap-Methode, nie direkt mit Entity/EntityManager.

    +
    +
  • +
  • + +
    + OnGUI() +
    GUI.Label(new Rect(15, 160, 400, 22), $"Nächste Welle in: {Mathf.CeilToInt(timer)}s");
    +

    CeilToInt (aufrunden) statt RoundToInt, damit die Anzeige nicht schon bei z.B. 0.4s Restzeit "0s" zeigt, sondern erst wenn die Welle wirklich ausgelöst wurde.

    +
    +
  • +
+
+ +
+
+
+ + diff --git a/Happy_Forest.slnx b/Happy_Forest.slnx index 311c3c1..d629b17 100644 --- a/Happy_Forest.slnx +++ b/Happy_Forest.slnx @@ -1,4 +1,4 @@  - + diff --git a/Packages/manifest.json b/Packages/manifest.json index 95478c4..eb67bff 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -5,6 +5,7 @@ "com.unity.ide.visualstudio": "2.0.27", "com.unity.inputsystem": "1.20.0", "com.unity.multiplayer.center": "1.0.1", + "com.unity.physics": "1.4.7", "com.unity.render-pipelines.universal": "17.3.0", "com.unity.shadergraph": "17.3.0", "com.unity.modules.accessibility": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index a8da9c2..29964f7 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -102,6 +102,21 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.physics": { + "version": "1.4.7", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.burst": "1.8.29", + "com.unity.entities": "1.4.8", + "com.unity.collections": "2.6.8", + "com.unity.mathematics": "1.3.2", + "com.unity.modules.imgui": "1.0.0", + "com.unity.test-framework": "1.4.6", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, "com.unity.profiling.core": { "version": "1.0.3", "depth": 1,