Initial Combat Implementation

This commit is contained in:
Luis Gonzalez
2026-05-31 21:35:12 -07:00
parent 7fa77ce821
commit 1f647dd5e1
166 changed files with 93337 additions and 91 deletions
+136
View File
@@ -131,6 +131,7 @@ GameObject:
- component: {fileID: 330585545}
- component: {fileID: 330585544}
- component: {fileID: 330585547}
- component: {fileID: 330585548}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@@ -256,6 +257,27 @@ MonoBehaviour:
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
m_Version: 2
--- !u!114 &330585548
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 330585543}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3e5890693b64a429789bf3edfae0a6ff, type: 3}
m_Name:
m_EditorClassIdentifier: ProjectM.Client::ProjectM.Client.PrototypeCameraRig
Pitch: 45
Yaw: 0
Distance: 16
TargetHeight: 1
Orthographic: 0
FieldOfView: 55
OrthoSize: 10
FollowSharpness: 8
FallbackTarget: {x: 3, y: 0, z: 4}
--- !u!1 &410087039
GameObject:
m_ObjectHideFlags: 0
@@ -432,6 +454,119 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &833091043
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 833091047}
- component: {fileID: 833091046}
- component: {fileID: 833091045}
- component: {fileID: 833091044}
m_Layer: 0
m_Name: Ground
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!23 &833091044
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 833091043}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: f5ef5fb55f211414595517e5ed7857b9, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 1
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!64 &833091045
MeshCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 833091043}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 5
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
--- !u!33 &833091046
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 833091043}
m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
--- !u!4 &833091047
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 833091043}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 3, y: 0, z: 4}
m_LocalScale: {x: 3, y: 1, z: 3}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1314640898
GameObject:
m_ObjectHideFlags: 0
@@ -493,3 +628,4 @@ SceneRoots:
- {fileID: 410087041}
- {fileID: 832575519}
- {fileID: 1314640900}
- {fileID: 833091047}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 06af691080cf54372ae1aac941c2589b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:33f3329aa1b6538d7c6ce29cf5ee32240ae9748448063cc3924e79b8cd720b1c
size 167807
@@ -0,0 +1,117 @@
fileFormatVersion: 2
guid: 04f85d1ea79e74f48945ec9f95fb0f34
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,218 @@
{
"version": 1,
"name": "Project M Input",
"maps": [
{
"name": "Gameplay",
"id": "69723709-3120-49dd-bbf0-91ac92ad8582",
"actions": [
{
"name": "Move",
"type": "Value",
"id": "2982f936-538e-4a37-a100-8eb7a2a88e4b",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Aim",
"type": "Value",
"id": "a1b2c3d4-0a1m-4a1m-8a1m-000000000001",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "Fire",
"type": "Button",
"id": "a1b2c3d4-0f1r-4f1r-8f1r-000000000002",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "",
"id": "c3b1e700-eea3-426c-863d-36403d537af3",
"path": "<Gamepad>/leftStick",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "WASD",
"id": "cc5f8773-3f87-4e89-926e-2835221cc71b",
"path": "Dpad",
"interactions": "",
"processors": "",
"groups": "",
"action": "Move",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "cc98fd24-5e37-4b4a-8fdc-9a1c21bc1e09",
"path": "<Keyboard>/w",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "up",
"id": "59f74329-7d63-4bf5-b3bc-3af75462894f",
"path": "<Keyboard>/upArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "624a58a3-f4ec-479f-8e1b-3b54ffaa7d8e",
"path": "<Keyboard>/s",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "2d3501d8-5759-4d58-a909-a5ef3aa51842",
"path": "<Keyboard>/downArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "14dbcdaf-402d-496b-9798-54b770c4a826",
"path": "<Keyboard>/a",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "2f4809ce-93a5-44f7-802d-6ac61f1e03e2",
"path": "<Keyboard>/leftArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "de79a6ae-cf9d-4536-b851-b630e3344d04",
"path": "<Keyboard>/d",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "5bfaeff9-d16a-4fca-b43d-1d6d6b442bc8",
"path": "<Keyboard>/rightArrow",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Move",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "401d3950-45d9-4839-b287-14c2b3902c67",
"path": "<XRController>/{Primary2DAxis}",
"interactions": "",
"processors": "",
"groups": "XR",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "b23bd67c-a1c3-44a5-a73b-c824416534fa",
"path": "<Joystick>/stick",
"interactions": "",
"processors": "",
"groups": "Joystick",
"action": "Move",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "d1e2f3a4-aim0-4001-8001-000000000011",
"path": "<Gamepad>/rightStick",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Aim",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "d1e2f3a4-fir0-4001-8001-000000000021",
"path": "<Gamepad>/rightTrigger",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "d1e2f3a4-fir0-4001-8001-000000000022",
"path": "<Mouse>/leftButton",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "d1e2f3a4-fir0-4001-8001-000000000023",
"path": "<Keyboard>/space",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "Fire",
"isComposite": false,
"isPartOfComposite": false
}
]
}
],
"controlSchemes": []
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 5aa62f6ed584c43b791e76f2fd31820f
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 1
wrapperCodePath: Assets/_Project/Scripts/Client/Input/ProjectMInput.cs
wrapperClassName:
wrapperCodeNamespace:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 130 B

+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 42fbad828f6084cd7961237599a610fd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
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: 84b20ca889a744e888c8c3b3b723ec69, type: 3}
m_Name: Ability_FastLight
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.AbilityDefinition
Id: 2
DisplayName: Fast Light
Damage: 8
ProjectileSpeed: 40
Range: 16
AutoTargetRange: 12
AutoTargetConeDegrees: 35
CooldownTicks: 5
ProjectilePrefab: {fileID: 8857056134628386430, guid: 700fb9c3f7cf94830a0ee78c4e4f1290, type: 3}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b594c2953f8304189b91ca8c96a9d0c4
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
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: 84b20ca889a744e888c8c3b3b723ec69, type: 3}
m_Name: Ability_Primary
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.AbilityDefinition
Id: 1
DisplayName: Primary
Damage: 20
ProjectileSpeed: 25
Range: 20
AutoTargetRange: 12
AutoTargetConeDegrees: 35
CooldownTicks: 12
ProjectilePrefab: {fileID: 8857056134628386430, guid: 700fb9c3f7cf94830a0ee78c4e4f1290, type: 3}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 013954d16c1ff4c1dad82495e38b4657
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
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: 84b20ca889a744e888c8c3b3b723ec69, type: 3}
m_Name: Ability_SlowHeavy
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.AbilityDefinition
Id: 3
DisplayName: Slow Heavy
Damage: 45
ProjectileSpeed: 14
Range: 24
AutoTargetRange: 12
AutoTargetConeDegrees: 35
CooldownTicks: 28
ProjectilePrefab: {fileID: 8857056134628386430, guid: 700fb9c3f7cf94830a0ee78c4e4f1290, type: 3}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 601a090742e5341cc9ee1f25d215136b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c49a4c4126cb24bd984381e2a48e9a99
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
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: c63201f1fd9f24efd9ccae3e20cd2364, type: 3}
m_Name: Character_Default
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.CharacterStatsDefinition
Id: 1
DisplayName: Default
MoveSpeed: 6
TurnRateDegreesPerSec: 720
MaxHealth: 100
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e675b529048144a41a2054c729180bce
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d9c772bb61e104e2e8f26dad62e3d686
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+137
View File
@@ -0,0 +1,137 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-8760768806978604505
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: M_Dummy
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
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.15, b: 0.15, a: 1}
- _Color: {r: 1, g: 0.14999998, b: 0.14999998, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f8df9cd33fb974460a903e35a6fce3c9
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
+137
View File
@@ -0,0 +1,137 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3114125693251561139
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: M_Ground
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
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: 0.16, g: 0.16, b: 0.2, a: 1}
- _Color: {r: 0.15999997, g: 0.15999997, b: 0.19999996, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f5ef5fb55f211414595517e5ed7857b9
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
+137
View File
@@ -0,0 +1,137 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: M_Player
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
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: 0.1, g: 0.55, b: 1, a: 1}
- _Color: {r: 0.09999997, g: 0.54999995, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &6023047813083343898
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
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 14cdbac7bb1304e0ea4716a764261457
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
+137
View File
@@ -0,0 +1,137 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: M_Projectile
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
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.85, b: 0.1, a: 1}
- _Color: {r: 1, g: 0.85, b: 0.09999997, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &8800532420623758028
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
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ba23fa98368bb4a4997bfd08547c83ee
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
+5 -4
View File
@@ -69,7 +69,7 @@ MeshRenderer:
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
- {fileID: 2100000, guid: 14cdbac7bb1304e0ea4716a764261457, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
@@ -105,8 +105,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 766c44362be2b4fcaa872e6fb44fc42f, type: 3}
m_Name:
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.PlayerAuthoring
MoveSpeed: 6
TurnRateDegreesPerSec: 720
Character: {fileID: 11400000, guid: e675b529048144a41a2054c729180bce, type: 2}
PrimaryAbility: {fileID: 11400000, guid: 013954d16c1ff4c1dad82495e38b4657, type: 2}
FallbackMaxHealth: 100
HitRadius: 0.6
--- !u!114 &304484164735584996
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -144,5 +146,4 @@ MonoBehaviour:
OptimizationMode: 0
Importance: 1
MaxSendRate: 0
SingleWorldHostInterpolationSmoothing: 1
prefabId:
+148
View File
@@ -0,0 +1,148 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &8857056134628386430
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8708023721942265622}
- component: {fileID: 892549877506778438}
- component: {fileID: 2964537246306930844}
- component: {fileID: 866206118258644914}
- component: {fileID: 398845119511860568}
- component: {fileID: 4884824795720831881}
m_Layer: 0
m_Name: Projectile
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8708023721942265622
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8857056134628386430}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.35, y: 0.35, z: 0.35}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &892549877506778438
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8857056134628386430}
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &2964537246306930844
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8857056134628386430}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: ba23fa98368bb4a4997bfd08547c83ee, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 1
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!114 &866206118258644914
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8857056134628386430}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1429bc3b3a1da44e4a11065be0733a8f, type: 3}
m_Name:
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ProjectileAuthoring
Speed: 25
Damage: 20
Range: 20
--- !u!114 &398845119511860568
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8857056134628386430}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
--- !u!114 &4884824795720831881
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8857056134628386430}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
HasOwner: 1
SupportAutoCommandTarget: 0
TrackInterpolationDelay: 0
GhostGroup: 0
UsePreSerialization: 0
UseSingleBaseline: 0
RollbackPredictedSpawnedGhostState: 0
RollbackPredictionOnStructuralChanges: 1
DefaultGhostMode: 2
SupportedGhostModes: 3
OptimizationMode: 0
Importance: 1
MaxSendRate: 0
prefabId:
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 700fb9c3f7cf94830a0ee78c4e4f1290
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,147 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6681740481886397972
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3040602402374479956}
- component: {fileID: 9111625462373738953}
- component: {fileID: 6113974662432286332}
- component: {fileID: 5080357267889081991}
- component: {fileID: 3549493811897787778}
- component: {fileID: 1198698002335808414}
m_Layer: 0
m_Name: TrainingDummy
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3040602402374479956
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
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: 2, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &9111625462373738953
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &6113974662432286332
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: f8df9cd33fb974460a903e35a6fce3c9, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 1
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!114 &5080357267889081991
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c32aebeb7bfbb464898dfee8e6e87e6c, type: 3}
m_Name:
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.TrainingDummyAuthoring
MaxHealth: 60
HitRadius: 0.8
--- !u!114 &3549493811897787778
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
--- !u!114 &1198698002335808414
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6681740481886397972}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
HasOwner: 0
SupportAutoCommandTarget: 0
TrackInterpolationDelay: 0
GhostGroup: 0
UsePreSerialization: 0
UseSingleBaseline: 0
RollbackPredictedSpawnedGhostState: 0
RollbackPredictionOnStructuralChanges: 1
DefaultGhostMode: 0
SupportedGhostModes: 3
OptimizationMode: 0
Importance: 1
MaxSendRate: 0
prefabId:
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9ee6032b77c444d07a1dc9dfbf5abd68
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,149 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &3885353946372160549
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3572766465862231365}
- component: {fileID: 3909651526955663392}
- component: {fileID: 3320445911748035220}
- component: {fileID: 6410754845414010269}
- component: {fileID: 9053853372340598254}
- component: {fileID: 6834786618115927220}
m_Layer: 0
m_Name: UpgradePickup
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3572766465862231365
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.8, y: 0.8, z: 0.8}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &3909651526955663392
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &3320445911748035220
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 1
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!114 &6410754845414010269
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 07e6d40378fcb43c5be706ef96cb4bb2, type: 3}
m_Name:
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.UpgradePickupAuthoring
Target: 0
Op: 0
Value: 10
HitRadius: 1.2
--- !u!114 &9053853372340598254
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
--- !u!114 &6834786618115927220
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
HasOwner: 0
SupportAutoCommandTarget: 1
TrackInterpolationDelay: 0
GhostGroup: 0
UsePreSerialization: 0
UseSingleBaseline: 0
RollbackPredictedSpawnedGhostState: 0
RollbackPredictionOnStructuralChanges: 1
DefaultGhostMode: 0
SupportedGhostModes: 3
OptimizationMode: 0
Importance: 1
MaxSendRate: 0
prefabId:
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d7dd0078c7b1f4cf995ca3a4b1155569
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7329bc1c607064c54a087b67292b8ff5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,92 @@
using System.Collections.Generic;
using ProjectM.Simulation;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Bakes the designer-authored ability + character definitions into a single AbilityDatabase blob
/// singleton (immutable, shared, Burst-fast) plus a companion AbilityPrefabElement buffer holding the
/// per-ability projectile ghost prefab entity refs (entity refs cannot live in a blob). Place ONE of
/// these in the gameplay subscene; it streams identically into the client and server worlds (config,
/// not replicated). DependsOn each definition so a value change in an SO re-bakes the blob.
/// </summary>
public class AbilityDatabaseAuthoring : MonoBehaviour
{
[Tooltip("All ability definitions available in the game. Indexed at runtime by AbilityId.")]
public List<AbilityDefinition> Abilities = new List<AbilityDefinition>();
[Tooltip("All character-stats definitions. Indexed at runtime by CharacterId.")]
public List<CharacterStatsDefinition> Characters = new List<CharacterStatsDefinition>();
private class DatabaseBaker : Baker<AbilityDatabaseAuthoring>
{
public override void Bake(AbilityDatabaseAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.None);
int abilityCount = authoring.Abilities != null ? authoring.Abilities.Count : 0;
int charCount = authoring.Characters != null ? authoring.Characters.Count : 0;
var builder = new BlobBuilder(Allocator.Temp);
ref var root = ref builder.ConstructRoot<AbilityDatabaseBlob>();
var abilityArray = builder.Allocate(ref root.Abilities, abilityCount);
for (int i = 0; i < abilityCount; i++)
{
var def = authoring.Abilities[i];
if (def == null) { abilityArray[i] = default; continue; }
DependsOn(def);
abilityArray[i] = new AbilityDefBlob
{
Id = (byte)def.Id,
Damage = def.Damage,
ProjectileSpeed = def.ProjectileSpeed,
Range = def.Range,
AutoTargetRange = def.AutoTargetRange,
AutoTargetConeRadians = math.radians(def.AutoTargetConeDegrees),
CooldownTicks = def.CooldownTicks,
Name = def.DisplayName,
};
}
var charArray = builder.Allocate(ref root.Characters, charCount);
for (int i = 0; i < charCount; i++)
{
var def = authoring.Characters[i];
if (def == null) { charArray[i] = default; continue; }
DependsOn(def);
charArray[i] = new CharacterStatsBlob
{
Id = (byte)def.Id,
MoveSpeed = def.MoveSpeed,
TurnRateRadiansPerSec = math.radians(def.TurnRateDegreesPerSec),
MaxHealth = def.MaxHealth,
Name = def.DisplayName,
};
}
var blob = builder.CreateBlobAssetReference<AbilityDatabaseBlob>(Allocator.Persistent);
builder.Dispose();
AddBlobAsset(ref blob, out _);
AddComponent(entity, new AbilityDatabase { Value = blob });
// Companion entity-ref buffer: per-ability projectile ghost prefab (resolved via GetEntity).
var prefabBuffer = AddBuffer<AbilityPrefabElement>(entity);
for (int i = 0; i < abilityCount; i++)
{
var def = authoring.Abilities[i];
if (def == null || def.ProjectilePrefab == null) continue;
prefabBuffer.Add(new AbilityPrefabElement
{
Id = (byte)def.Id,
Prefab = GetEntity(def.ProjectilePrefab, TransformUsageFlags.Dynamic),
});
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4f60b41d8ec8d4c24b4d4f54af919080
@@ -0,0 +1,33 @@
using ProjectM.Simulation;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Designer-facing definition of one ability. Numeric fields are baked into the AbilityDatabase blob
/// (immutable, Burst-fast runtime config); the projectile prefab is baked into the companion
/// AbilityPrefabElement buffer (entity refs cannot live inside a blob). UI fields (icon/description)
/// are deliberately deferred to a later managed lookup keyed by id.
/// </summary>
[CreateAssetMenu(menuName = "Project M/Ability Definition", fileName = "Ability_")]
public class AbilityDefinition : ScriptableObject
{
public AbilityId Id = AbilityId.Primary;
public string DisplayName = "Ability";
[Header("Combat")]
[Min(0f)] public float Damage = 20f;
[Min(0f)] public float ProjectileSpeed = 25f;
[Min(0f)] public float Range = 20f;
[Header("Auto-target assist")]
[Min(0f)] public float AutoTargetRange = 12f;
[Min(0f)] public float AutoTargetConeDegrees = 35f;
[Header("Timing")]
[Min(1)] public int CooldownTicks = 12;
[Header("Prefab (baked into the prefab buffer, not the blob)")]
public GameObject ProjectilePrefab;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 84b20ca889a744e888c8c3b3b723ec69
@@ -0,0 +1,21 @@
using ProjectM.Simulation;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Designer-facing definition of a character's base stats (movement + survivability), baked into the
/// AbilityDatabase blob and looked up at runtime by CharacterStatsRef. The single source of these
/// values - PlayerAuthoring also seeds the player's starting Health from MaxHealth.
/// </summary>
[CreateAssetMenu(menuName = "Project M/Character Stats Definition", fileName = "Character_")]
public class CharacterStatsDefinition : ScriptableObject
{
public CharacterId Id = CharacterId.Default;
public string DisplayName = "Character";
[Min(0f)] public float MoveSpeed = 6f;
[Min(0f)] public float TurnRateDegreesPerSec = 720f;
[Min(0f)] public float MaxHealth = 100f;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c63201f1fd9f24efd9ccae3e20cd2364
@@ -0,0 +1,38 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the projectile ghost prefab fired by the player's primary ability. Bakes the
/// baked-once tunables (<see cref="Projectile.Speed"/>, <see cref="Projectile.Damage"/>,
/// <see cref="Projectile.Range"/>) onto the entity; the replicated <c>Direction</c>/<c>SpawnId</c>
/// and the integrated <c>DistanceTravelled</c> are left at their default 0 and written at spawn
/// time by AbilityFireSystem. Ghost replication and <c>GhostOwner</c> are supplied by the
/// GhostAuthoringComponent added on the same prefab GameObject (not added here, nor is Health).
/// <c>GetEntity(TransformUsageFlags.Dynamic)</c> ensures a runtime-mutable LocalTransform exists.
/// </summary>
public class ProjectileAuthoring : MonoBehaviour
{
[Min(0f)] public float Speed = 25f;
[Min(0f)] public float Damage = 20f;
[Min(0f)] public float Range = 20f;
private class ProjectileBaker : Baker<ProjectileAuthoring>
{
public override void Bake(ProjectileAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
// Direction / SpawnId / DistanceTravelled default to 0 — set at spawn by AbilityFireSystem.
AddComponent(entity, new Projectile
{
Speed = authoring.Speed,
Damage = authoring.Damage,
Range = authoring.Range
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1429bc3b3a1da44e4a11065be0733a8f
@@ -0,0 +1,34 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring placed once in the gameplay subscene. Bakes a <see cref="ProjectileSpawner"/>
/// singleton holding the projectile ghost prefab entity, which the predicted AbilityFireSystem
/// instantiates whenever a player fires. The spawner itself carries no transform (it is a pure
/// data singleton) so it is baked with <c>TransformUsageFlags.None</c>, while the referenced
/// prefab is baked with <c>TransformUsageFlags.Dynamic</c> so the spawned projectile has a
/// runtime-mutable LocalTransform.
/// </summary>
public class ProjectileSpawnerAuthoring : MonoBehaviour
{
[Tooltip("The projectile ghost prefab spawned when a player fires.")]
public GameObject ProjectilePrefab;
private class ProjectileSpawnerBaker : Baker<ProjectileSpawnerAuthoring>
{
public override void Bake(ProjectileSpawnerAuthoring authoring)
{
// The spawner itself needs no transform; it is a data singleton.
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new ProjectileSpawner
{
Prefab = GetEntity(authoring.ProjectilePrefab, TransformUsageFlags.Dynamic)
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aa1b7c054d5a043f2801cddf90567acf
@@ -0,0 +1,41 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the training-dummy enemy prefab. Bakes a stationary, damageable auto-target
/// candidate: <see cref="TrainingDummyTag"/> marks it for the ability auto-target cone,
/// <see cref="Health"/> and <see cref="HitRadius"/> make it a valid projectile hit target, and a
/// <see cref="DamageEvent"/> buffer receives server-authoritative hits. Dummies are NOT ghosts and
/// carry no <c>GhostOwner</c>, so projectiles never treat them as the firing owner.
/// <c>GetEntity(TransformUsageFlags.Dynamic)</c> ensures a runtime LocalTransform for hit tests
/// and spawn placement.
/// </summary>
public class TrainingDummyAuthoring : MonoBehaviour
{
[Min(0f), Tooltip("Starting and maximum health for the dummy.")]
public float MaxHealth = 60f;
[Min(0f), Tooltip("World-unit radius used by the projectile hit test.")]
public float HitRadius = 0.8f;
private class TrainingDummyBaker : Baker<TrainingDummyAuthoring>
{
public override void Bake(TrainingDummyAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
AddComponent<TrainingDummyTag>(entity);
AddComponent(entity, new Health
{
Current = authoring.MaxHealth,
Max = authoring.MaxHealth
});
AddComponent(entity, new HitRadius { Value = authoring.HitRadius });
AddBuffer<DamageEvent>(entity);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c32aebeb7bfbb464898dfee8e6e87e6c
@@ -0,0 +1,48 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the baked <see cref="TrainingDummySpawner"/> singleton. Place this on a single
/// GameObject in the gameplay subscene; at runtime the server-only
/// <c>TrainingDummySpawnSystem</c> reads the singleton, instantiates <see cref="Count"/> dummies
/// laid out along +X from <see cref="Origin"/> at <see cref="Spacing"/> intervals, then destroys
/// the singleton so it fires exactly once. The entity itself carries no transform
/// (<c>TransformUsageFlags.None</c>); only the referenced <see cref="DummyPrefab"/> needs a
/// runtime-mutable LocalTransform (<c>TransformUsageFlags.Dynamic</c>).
/// </summary>
public class TrainingDummySpawnerAuthoring : MonoBehaviour
{
[Tooltip("Training dummy prefab to instantiate. Must carry TrainingDummyAuthoring.")]
public GameObject DummyPrefab;
[Min(0)]
[Tooltip("How many dummies to spawn.")]
public int Count = 3;
[Min(0f)]
[Tooltip("World-unit spacing between consecutive dummies along +X.")]
public float Spacing = 3f;
[Tooltip("World-space position of the first dummy.")]
public Vector3 Origin = new Vector3(0, 0, 8);
private class TrainingDummySpawnerBaker : Baker<TrainingDummySpawnerAuthoring>
{
public override void Bake(TrainingDummySpawnerAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new TrainingDummySpawner
{
Prefab = GetEntity(authoring.DummyPrefab, TransformUsageFlags.Dynamic),
Count = authoring.Count,
Spacing = authoring.Spacing,
Origin = authoring.Origin
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 51cc543c146d84239ba6dc219221df18
@@ -0,0 +1,43 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for an upgrade pickup ghost prefab: a world object that grants one stat modifier to the
/// first player that overlaps it (server-authoritative, applied by <c>UpgradePickupSystem</c>) and
/// then despawns. Bake the prefab as an interpolated ghost (add a GhostAuthoringComponent) so clients
/// see it appear and despawn. <c>GetEntity(TransformUsageFlags.Dynamic)</c> gives it a world transform.
/// </summary>
public class UpgradePickupAuthoring : MonoBehaviour
{
[Tooltip("Which stat the granted modifier targets.")]
public StatTarget Target = StatTarget.Damage;
[Tooltip("How the granted modifier combines.")]
public ModOp Op = ModOp.Flat;
[Tooltip("Modifier magnitude: flat amount, or fractional percent (0.1 = +10%).")]
public float Value = 10f;
[Tooltip("Overlap radius (world units) for the player pickup test.")]
[Min(0f)] public float HitRadius = 1f;
private class UpgradePickupBaker : Baker<UpgradePickupAuthoring>
{
public override void Bake(UpgradePickupAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
AddComponent(entity, new UpgradePickup
{
Target = (byte)authoring.Target,
Op = (byte)authoring.Op,
Value = authoring.Value,
SourceId = 0u,
});
AddComponent(entity, new HitRadius { Value = authoring.HitRadius });
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 07e6d40378fcb43c5be706ef96cb4bb2
@@ -0,0 +1,48 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the baked <see cref="UpgradePickupSpawner"/> singleton (mirrors
/// TrainingDummySpawnerAuthoring). Place on a single GameObject in the gameplay subscene; the
/// server-only <c>UpgradePickupSpawnSystem</c> reads it, spawns <see cref="Count"/> pickups along +X
/// from <see cref="Origin"/> at <see cref="Spacing"/> intervals, then destroys the singleton so it
/// fires exactly once. The entity carries no transform; only the prefab needs a runtime transform.
/// </summary>
public class UpgradePickupSpawnerAuthoring : MonoBehaviour
{
[Tooltip("Upgrade pickup prefab to instantiate. Must carry UpgradePickupAuthoring.")]
public GameObject PickupPrefab;
[Min(0)]
[Tooltip("How many pickups to spawn.")]
public int Count = 2;
[Min(0f)]
[Tooltip("World-unit spacing between consecutive pickups along +X.")]
public float Spacing = 3f;
[Tooltip("World-space position of the first pickup.")]
public Vector3 Origin = new Vector3(-4f, 0f, 6f);
private class UpgradePickupSpawnerBaker : Baker<UpgradePickupSpawnerAuthoring>
{
public override void Bake(UpgradePickupSpawnerAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new UpgradePickupSpawner
{
Prefab = authoring.PickupPrefab != null
? GetEntity(authoring.PickupPrefab, TransformUsageFlags.Dynamic)
: Entity.Null,
Count = authoring.Count,
Spacing = authoring.Spacing,
Origin = authoring.Origin,
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e367eb55e2c2248f18be10d6c3c9ad67
@@ -1,21 +1,32 @@
using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the player ghost prefab. Bakes the gameplay components onto the entity and
/// exposes movement tunables for designers. Ghost replication, <c>GhostOwner</c> and
/// AutoCommandTarget are supplied by the GhostAuthoringComponent added on the same prefab
/// GameObject. <c>GetEntity(TransformUsageFlags.Dynamic)</c> ensures a runtime-mutable
/// LocalTransform exists.
/// Authoring for the player ghost prefab. As of M3 the numeric tunables live in data
/// (<see cref="CharacterStatsDefinition"/> / <see cref="AbilityDefinition"/> ScriptableObjects);
/// this authoring only selects which definitions the player uses and bakes the light id refs, the
/// (empty) replicated modifier buffer, and the zeroed effective-stat components that
/// StatRecomputeSystem fills each predicted tick. Health is seeded from the character definition's
/// MaxHealth (single source). Ghost replication, <c>GhostOwner</c> and AutoCommandTarget come from
/// the GhostAuthoringComponent on the same prefab GameObject; <c>GetEntity(TransformUsageFlags.Dynamic)</c>
/// ensures a runtime-mutable LocalTransform exists.
/// </summary>
public class PlayerAuthoring : MonoBehaviour
{
[Min(0f)] public float MoveSpeed = 6f;
[Min(0f)] public float TurnRateDegreesPerSec = 720f;
[Tooltip("Character-stats definition (move speed, turn rate, max health). Single source of those values.")]
public CharacterStatsDefinition Character;
[Tooltip("Ability definition occupying the player's primary slot.")]
public AbilityDefinition PrimaryAbility;
[Header("Fallbacks (used only if a definition above is unassigned)")]
[Min(0f)] public float FallbackMaxHealth = 100f;
/// <summary>Projectile hit-test radius for the player as a damageable target, in world units.</summary>
[Min(0f)] public float HitRadius = 0.6f;
private class PlayerBaker : Baker<PlayerAuthoring>
{
@@ -23,14 +34,38 @@ namespace ProjectM.Authoring
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
// Re-bake when a referenced definition's serialized values change.
if (authoring.Character != null) DependsOn(authoring.Character);
if (authoring.PrimaryAbility != null) DependsOn(authoring.PrimaryAbility);
byte characterId = authoring.Character != null
? (byte)authoring.Character.Id : (byte)CharacterId.Default;
byte abilityId = authoring.PrimaryAbility != null
? (byte)authoring.PrimaryAbility.Id : (byte)AbilityId.Primary;
float maxHealth = authoring.Character != null
? authoring.Character.MaxHealth : authoring.FallbackMaxHealth;
AddComponent<PlayerTag>(entity);
AddComponent(entity, new PlayerMoveStats
{
MoveSpeed = authoring.MoveSpeed,
TurnRateRadiansPerSec = math.radians(authoring.TurnRateDegreesPerSec)
});
AddComponent<PlayerFacing>(entity);
AddComponent<PlayerInput>(entity);
// Data-driven stat refs (replace M2's inlined PlayerMoveStats / AbilityStats values).
AddComponent(entity, new CharacterStatsRef { Id = characterId });
AddComponent(entity, new AbilityRef { Id = abilityId });
// Effective stats: zeroed at bake, recomputed every predicted tick by StatRecomputeSystem.
AddComponent(entity, new EffectiveAbilityStats());
AddComponent(entity, new EffectiveCharacterStats());
// Empty replicated modifier stack (grown by upgrades/pickups/debug hook, server-authoritative).
AddBuffer<StatModifier>(entity);
// Combat: server-authoritative health (Current replicated for display), the player's
// damageable hit radius, predicted cooldown state, and the per-tick damage inbox.
AddComponent(entity, new Health { Current = maxHealth, Max = maxHealth });
AddComponent(entity, new HitRadius { Value = authoring.HitRadius });
AddComponent<AbilityCooldown>(entity);
AddBuffer<DamageEvent>(entity);
}
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 626396bc0d4204603abfd345e6e4f21d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,185 @@
using ProjectM.Simulation;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using Unity.NetCode.LowLevel;
namespace ProjectM.Client
{
/// <summary>
/// Client-only predicted-spawn classifier for projectiles. When a predicted client fires, it
/// locally spawns a predicted projectile ghost; later the server's authoritative spawn arrives in
/// the <see cref="GhostSpawnQueue"/>. This system pairs the incoming server ghost with the matching
/// locally predicted entity so netcode reconciles them instead of double-spawning. The match key is
/// <see cref="Projectile.SpawnId"/> — a deterministic <c>(ownerNetId << 16) | absoluteFireCount</c>
/// value computed identically on client and server, replicated as a <c>[GhostField]</c> so it is
/// present in snapshot history and readable here via <see cref="SnapshotDataBufferComponentLookup"/>.
/// Mirrors the official Netcode HelloNetcode 02_PredictedSpawning GrenadeClassificationSystem,
/// with GrenadeData→<see cref="Projectile"/> and GrenadeSpawner→<see cref="ProjectileSpawner"/>.
/// Runs after the built-in <see cref="GhostSpawnClassificationSystem"/> (so any owner-predicted
/// default classification has already had a pass) and before the OrderLast
/// <c>DefaultGhostSpawnClassificationSystem</c> (so entries this system does NOT match still fall
/// through to the spawn-tick-window fallback).
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(GhostSpawnClassificationSystemGroup))]
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
[CreateAfter(typeof(GhostCollectionSystem))]
[CreateAfter(typeof(GhostReceiveSystem))]
// NOTE: intentionally NOT [BurstCompile]d. The cross-assembly generic
// SnapshotDataBufferComponentLookup.TryGetComponentDataFromSnapshotHistory&lt;T&gt;() trips a Burst
// internal compiler error (type-hash resolution) on Netcode 1.13.2. Classification only runs when
// ghost spawns are received (a cold path, not the prediction loop), so a managed job is fine here.
public partial struct ProjectileClassificationSystem : ISystem
{
SnapshotDataLookupHelper m_SnapshotDataLookupHelper;
BufferLookup<PredictedGhostSpawn> m_PredictedGhostSpawnLookup;
ComponentLookup<Projectile> m_ProjectileLookup;
/// <summary>
/// Resolved once in <see cref="OnUpdate"/>: the ghost-collection index of our projectile prefab.
/// -1 until the <see cref="GhostCollectionPrefab"/> buffer has been populated and scanned.
/// </summary>
int m_GhostType;
public void OnCreate(ref SystemState state)
{
// Match the built-in GhostSpawnClassificationSystem / DefaultGhostSpawnClassificationSystem:
// in a single-world host (a world flagged GameClient AND GameServer) there is no real client
// snapshot history to classify against, so the package's classification systems disable
// themselves. We do the same so we never run alone in that scenario after the system we
// UpdateAfter has switched itself off. (For Project M's standard separate ClientWorld +
// ServerWorld over IPC, IsHost() is false and this guard is a no-op.)
if (state.WorldUnmanaged.IsHost())
{
state.Enabled = false;
return;
}
// Build the snapshot lookup helper from the two collection singletons. CreateAfter on
// GhostCollectionSystem + GhostReceiveSystem guarantees both singletons exist by now.
m_SnapshotDataLookupHelper = new SnapshotDataLookupHelper(
ref state,
SystemAPI.GetSingletonEntity<GhostCollection>(),
SystemAPI.GetSingletonEntity<SpawnedGhostEntityMap>());
m_PredictedGhostSpawnLookup = state.GetBufferLookup<PredictedGhostSpawn>(true);
m_ProjectileLookup = state.GetComponentLookup<Projectile>(true);
state.RequireForUpdate<GhostSpawnQueue>();
state.RequireForUpdate<PredictedGhostSpawnList>();
state.RequireForUpdate<NetworkId>();
state.RequireForUpdate<ProjectileSpawner>();
m_GhostType = -1;
}
public void OnUpdate(ref SystemState state)
{
// Resolve our projectile ghost-type index once by scanning the ghost-collection prefab
// buffer for the spawner's prefab entity. The collection is populated only after the ghost
// prefabs have loaded, so retry each tick until found.
if (m_GhostType == -1)
{
var projectilePrefab = SystemAPI.GetSingleton<ProjectileSpawner>().Prefab;
var ghostCollection = SystemAPI.GetSingletonEntity<GhostCollection>();
var prefabs = SystemAPI.GetBuffer<GhostCollectionPrefab>(ghostCollection);
for (int i = 0; i < prefabs.Length; ++i)
{
if (prefabs[i].GhostPrefab == projectilePrefab)
{
m_GhostType = i;
break;
}
}
if (m_GhostType == -1)
return;
}
m_SnapshotDataLookupHelper.Update(ref state);
m_PredictedGhostSpawnLookup.Update(ref state);
m_ProjectileLookup.Update(ref state);
// SystemAPI is a system-context-only facade and cannot be used inside the IJobEntity; resolve
// the predicted-spawn-list singleton entity here and pass it into the job (this mirrors the
// built-in DefaultGhostSpawnClassificationJob.spawnListEntity pattern).
var classificationJob = new ProjectileClassificationJob
{
GhostType = m_GhostType,
SnapshotDataLookup = m_SnapshotDataLookupHelper.CreateSnapshotBufferLookup(),
PredictedSpawnListEntity = SystemAPI.GetSingletonEntity<PredictedGhostSpawnList>(),
PredictedGhostSpawnLookup = m_PredictedGhostSpawnLookup,
ProjectileLookup = m_ProjectileLookup,
};
state.Dependency = classificationJob.Schedule(state.Dependency);
}
/// <summary>
/// For each newly received server spawn in a <see cref="GhostSpawnQueue"/>, attempts to find a
/// locally predicted projectile with the same <see cref="Projectile.SpawnId"/> read out of
/// snapshot history. On a match it points the queued spawn at the predicted entity (so netcode
/// adopts it instead of instantiating a duplicate), marks the entry classified, and removes that
/// predicted entry from the list. Entries this system does NOT match are left untouched so the
/// OrderLast <c>DefaultGhostSpawnClassificationSystem</c> fallback can still try the spawn-tick
/// window match.
/// </summary>
[WithAll(typeof(GhostSpawnQueue))]
partial struct ProjectileClassificationJob : IJobEntity
{
public int GhostType;
public SnapshotDataBufferComponentLookup SnapshotDataLookup;
// Resolved in OnUpdate (SystemAPI is unavailable inside a job). A single Entity field, so it
// is NOT marked [ReadOnly].
public Entity PredictedSpawnListEntity;
[ReadOnly] public BufferLookup<PredictedGhostSpawn> PredictedGhostSpawnLookup;
[ReadOnly] public ComponentLookup<Projectile> ProjectileLookup;
// 'data' is taken by value (NOT 'in') because TryGetComponentDataFromSnapshotHistory needs a
// mutable 'ref DynamicBuffer<SnapshotDataBuffer>'. The built-in GhostSpawnClassification uses
// 'in' only because it never calls that ref overload — do not copy that here.
public void Execute(DynamicBuffer<GhostSpawnBuffer> newSpawns, DynamicBuffer<SnapshotDataBuffer> data)
{
var predictedSpawnList = PredictedGhostSpawnLookup[PredictedSpawnListEntity];
for (int i = 0; i < newSpawns.Length; ++i)
{
ref var newSpawn = ref newSpawns.ElementAt(i);
// Only classify our own ghost type, and only predicted spawns that have not already
// been matched/claimed (PredictedSpawnEntity == Null && !HasClassifiedPredictedSpawn)
// — leave everything else to the defaults.
if (newSpawn.GhostType != GhostType)
continue;
if (newSpawn.SpawnType != GhostSpawnBuffer.Type.Predicted ||
newSpawn.HasClassifiedPredictedSpawn ||
newSpawn.PredictedSpawnEntity != Entity.Null)
continue;
if (!SnapshotDataLookup.TryGetComponentDataFromSnapshotHistory(
newSpawn.GhostType, data, out Projectile incoming, i))
continue;
for (int j = 0; j < predictedSpawnList.Length; ++j)
{
if (predictedSpawnList[j].ghostType != GhostType)
continue;
var predictedEntity = predictedSpawnList[j].entity;
if (incoming.SpawnId == ProjectileLookup[predictedEntity].SpawnId)
{
// Claim the decision ONLY on a real match, so non-matches still fall through
// to the OrderLast default classifier (matches the official sample).
newSpawn.PredictedSpawnEntity = predictedEntity;
newSpawn.HasClassifiedPredictedSpawn = true;
predictedSpawnList.RemoveAtSwapBack(j);
break;
}
}
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 97e6942342fcf4de5b0ede9f2a33ed5b
@@ -0,0 +1,85 @@
#if UNITY_EDITOR
using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
namespace ProjectM.Client
{
/// <summary>
/// EDITOR-ONLY validation hook for driving the local player's <see cref="PlayerInput"/> without a
/// real input device or a focused Game view. The Unity Input System ignores injected/real device
/// input while the Game view is unfocused, which makes headless (MCP <c>execute_code</c>) or
/// automated fire/move validation impossible through <see cref="PlayerInputGatherSystem"/> alone.
/// <para>
/// This system runs in <see cref="GhostInputSystemGroup"/> immediately AFTER the real gather and,
/// when <see cref="Active"/> is set, overwrites the locally-owned player's input from static fields
/// you can poke from a debugger / <c>execute_code</c> / an editor button. Because it writes the same
/// <see cref="PlayerInput"/> the gather does, it drives the authentic command → prediction →
/// AbilityFireSystem pipeline (not a shortcut), so it validates the real fire/move/auto-target path.
/// </para>
/// <para>
/// Entirely wrapped in <c>#if UNITY_EDITOR</c>: it does not exist in player builds. Pair with
/// <c>Application.runInBackground = true</c> so the unfocused editor keeps ticking. Usage from
/// <c>execute_code</c>: <c>ProjectM.Client.DebugInputInjectionSystem.Fire();</c> (one shot),
/// <c>...SetMove(0f, 1f);</c> (hold a move heading), <c>...SetAim(1f, 0f);</c>, <c>...Stop();</c>.
/// </para>
/// </summary>
[UpdateInGroup(typeof(GhostInputSystemGroup))]
[UpdateAfter(typeof(PlayerInputGatherSystem))]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial class DebugInputInjectionSystem : SystemBase
{
/// <summary>While true, this system overrides the local player's gathered input each frame.</summary>
public static bool Active;
/// <summary>Movement heading applied to PlayerInput.Move while <see cref="Active"/>.</summary>
public static float2 Move;
/// <summary>Aim vector applied to PlayerInput.Aim while <see cref="Active"/> (zero = face movement).</summary>
public static float2 Aim;
/// <summary>Frames remaining to hold the Fire event. Each held frame raises Fire.Set(); holding
/// across several frames spans multiple network ticks so the one-shot event reliably reaches the
/// command buffer (a single-frame pulse can be lost across the frame→tick boundary). 0 = idle.</summary>
public static int FireFrames;
/// <summary>Convenience: hold Fire for the next <paramref name="frames"/> frames (also enables
/// override). The ability cooldown still gates how many shots actually result.</summary>
public static void Fire(int frames = 10) { Active = true; FireFrames = math.max(FireFrames, frames); }
/// <summary>Convenience: hold a planar move heading (also enables override).</summary>
public static void SetMove(float x, float z) { Active = true; Move = new float2(x, z); }
/// <summary>Convenience: hold an aim direction (also enables override).</summary>
public static void SetAim(float x, float z) { Active = true; Aim = new float2(x, z); }
/// <summary>Convenience: stop overriding and clear all injected input.</summary>
public static void Stop() { Active = false; Move = default; Aim = default; FireFrames = 0; }
protected override void OnCreate()
{
RequireForUpdate<PlayerInput>();
}
protected override void OnUpdate()
{
if (!Active)
return;
bool fire = FireFrames > 0;
if (FireFrames > 0) FireFrames--;
float2 move = Move;
float2 aim = Aim;
foreach (var input in SystemAPI.Query<RefRW<PlayerInput>>().WithAll<GhostOwnerIsLocal>())
{
input.ValueRW.Move = move;
input.ValueRW.Aim = aim;
if (fire)
input.ValueRW.Fire.Set();
}
}
}
}
#endif
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 51213eca1bfe84a16837d5755c334101
@@ -6,62 +6,73 @@ using Unity.NetCode;
namespace ProjectM.Client
{
/// <summary>
/// Client-only twin-stick input gather. Samples the Input System once per frame (WASD /
/// left-stick -&gt; Move, right-stick -&gt; Aim) and writes <see cref="PlayerInput"/> on the
/// locally-owned player ghost (filtered to <see cref="GhostOwnerIsLocal"/>). Runs in
/// <see cref="GhostInputSystemGroup"/> — NOT the prediction loop — so devices are read once per
/// frame, never re-read during rollback. Implemented as a non-Burst <see cref="ISystem"/>
/// because it reads the managed Input System.
/// Client-only twin-stick input gather. Samples the new Input System action map (the generated
/// <c>ProjectMInput</c> wrapper over <c>Assets/Settings/Project M Input.inputactions</c>) once per
/// frame and writes <see cref="PlayerInput"/> on the locally-owned player ghost (filtered to
/// <see cref="GhostOwnerIsLocal"/>). Runs in <see cref="GhostInputSystemGroup"/> — NOT the
/// prediction loop — so devices are read once per frame, never re-read during rollback.
/// <para>
/// NOTE: the Input System device types are fully qualified rather than imported via
/// <c>using UnityEngine.InputSystem;</c> on purpose — that namespace also defines a
/// <c>PlayerInput</c> type which would collide with <see cref="ProjectM.Simulation.PlayerInput"/>
/// and make the Entities source generator bind <c>RefRW&lt;PlayerInput&gt;</c> to the managed
/// class (a spurious CS8377 "must be unmanaged").
/// Implemented as a managed <see cref="SystemBase"/> (not a Burst <c>ISystem</c>) because it holds
/// and reads the managed Input System wrapper. Fire is an <see cref="InputEvent"/>: the event field
/// is reset each frame and raised via <c>Set()</c> on the press edge, so a single click fires
/// exactly once; netcode accumulates the absolute <c>Count</c> into the command buffer across the
/// frame→tick boundary (read back in <c>AbilityFireSystem</c> as the predicted-spawn key).
/// </para>
/// <para>
/// NOTE: Input System types are fully qualified (e.g. <c>UnityEngine.Vector2</c>) and
/// <c>using UnityEngine.InputSystem;</c> is intentionally omitted — that namespace defines a
/// <c>PlayerInput</c> type that collides with <see cref="ProjectM.Simulation.PlayerInput"/> and
/// makes the Entities generator bind <c>RefRW&lt;PlayerInput&gt;</c> to the managed class (a
/// spurious CS8377). The generated <c>ProjectMInput</c> wrapper lives in this assembly.
/// </para>
/// </summary>
[UpdateInGroup(typeof(GhostInputSystemGroup))]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial struct PlayerInputGatherSystem : ISystem
public partial class PlayerInputGatherSystem : SystemBase
{
public void OnCreate(ref SystemState state)
private ProjectMInput _controls;
protected override void OnCreate()
{
state.RequireForUpdate<PlayerInput>();
RequireForUpdate<PlayerInput>();
_controls = new ProjectMInput();
_controls.Gameplay.Enable();
}
public void OnUpdate(ref SystemState state)
protected override void OnDestroy()
{
float2 move = float2.zero;
float2 aim = float2.zero;
var keyboard = UnityEngine.InputSystem.Keyboard.current;
if (keyboard != null)
if (_controls != null)
{
if (keyboard.wKey.isPressed) move.y += 1f;
if (keyboard.sKey.isPressed) move.y -= 1f;
if (keyboard.dKey.isPressed) move.x += 1f;
if (keyboard.aKey.isPressed) move.x -= 1f;
_controls.Gameplay.Disable();
_controls.Dispose();
_controls = null;
}
}
var gamepad = UnityEngine.InputSystem.Gamepad.current;
if (gamepad != null)
{
float2 leftStick = gamepad.leftStick.ReadValue();
if (math.lengthsq(leftStick) > math.lengthsq(move))
move = leftStick;
protected override void OnUpdate()
{
var gameplay = _controls.Gameplay;
aim = gamepad.rightStick.ReadValue();
}
float2 move = (float2)gameplay.Move.ReadValue<UnityEngine.Vector2>();
float2 aim = (float2)gameplay.Aim.ReadValue<UnityEngine.Vector2>();
// Right-stick deadzone: a resting stick yields zero Aim so PlayerAimSystem falls back to
// the movement heading (controller-first directional aim).
// Right-stick deadzone: a resting stick yields zero Aim so PlayerAimSystem falls back to the
// movement heading (controller-first directional aim).
if (math.lengthsq(aim) < 0.04f)
aim = float2.zero;
bool firePressed = gameplay.Fire.WasPressedThisFrame();
foreach (var input in SystemAPI.Query<RefRW<PlayerInput>>().WithAll<GhostOwnerIsLocal>())
{
input.ValueRW.Move = move;
input.ValueRW.Aim = aim;
// Reset the per-frame event, then raise it on the press edge. Netcode latches the
// absolute Count into the command buffer; AbilityFireSystem reads it as the SpawnId key.
input.ValueRW.Fire = default;
if (firePressed)
input.ValueRW.Fire.Set();
}
}
}
@@ -0,0 +1,533 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was auto-generated by com.unity.inputsystem:InputActionCodeGenerator
// version 1.19.0
// from Assets/Settings/Project M Input.inputactions
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Utilities;
/// <summary>
/// Provides programmatic access to <see cref="InputActionAsset" />, <see cref="InputActionMap" />, <see cref="InputAction" /> and <see cref="InputControlScheme" /> instances defined in asset "Assets/Settings/Project M Input.inputactions".
/// </summary>
/// <remarks>
/// This class is source generated and any manual edits will be discarded if the associated asset is reimported or modified.
/// </remarks>
/// <example>
/// <code>
/// using namespace UnityEngine;
/// using UnityEngine.InputSystem;
///
/// // Example of using an InputActionMap named "Player" from a UnityEngine.MonoBehaviour implementing callback interface.
/// public class Example : MonoBehaviour, MyActions.IPlayerActions
/// {
/// private MyActions_Actions m_Actions; // Source code representation of asset.
/// private MyActions_Actions.PlayerActions m_Player; // Source code representation of action map.
///
/// void Awake()
/// {
/// m_Actions = new MyActions_Actions(); // Create asset object.
/// m_Player = m_Actions.Player; // Extract action map object.
/// m_Player.AddCallbacks(this); // Register callback interface IPlayerActions.
/// }
///
/// void OnDestroy()
/// {
/// m_Actions.Dispose(); // Destroy asset object.
/// }
///
/// void OnEnable()
/// {
/// m_Player.Enable(); // Enable all actions within map.
/// }
///
/// void OnDisable()
/// {
/// m_Player.Disable(); // Disable all actions within map.
/// }
///
/// #region Interface implementation of MyActions.IPlayerActions
///
/// // Invoked when "Move" action is either started, performed or canceled.
/// public void OnMove(InputAction.CallbackContext context)
/// {
/// Debug.Log($"OnMove: {context.ReadValue&lt;Vector2&gt;()}");
/// }
///
/// // Invoked when "Attack" action is either started, performed or canceled.
/// public void OnAttack(InputAction.CallbackContext context)
/// {
/// Debug.Log($"OnAttack: {context.ReadValue&lt;float&gt;()}");
/// }
///
/// #endregion
/// }
/// </code>
/// </example>
public partial class @ProjectMInput: IInputActionCollection2, IDisposable
{
/// <summary>
/// Provides access to the underlying asset instance.
/// </summary>
public InputActionAsset asset { get; }
/// <summary>
/// Constructs a new instance.
/// </summary>
public @ProjectMInput()
{
asset = InputActionAsset.FromJson(@"{
""version"": 1,
""name"": ""Project M Input"",
""maps"": [
{
""name"": ""Gameplay"",
""id"": ""69723709-3120-49dd-bbf0-91ac92ad8582"",
""actions"": [
{
""name"": ""Move"",
""type"": ""Value"",
""id"": ""2982f936-538e-4a37-a100-8eb7a2a88e4b"",
""expectedControlType"": ""Vector2"",
""processors"": """",
""interactions"": """",
""initialStateCheck"": true
},
{
""name"": ""Aim"",
""type"": ""Value"",
""id"": ""a1b2c3d4-0a1m-4a1m-8a1m-000000000001"",
""expectedControlType"": ""Vector2"",
""processors"": """",
""interactions"": """",
""initialStateCheck"": true
},
{
""name"": ""Fire"",
""type"": ""Button"",
""id"": ""a1b2c3d4-0f1r-4f1r-8f1r-000000000002"",
""expectedControlType"": ""Button"",
""processors"": """",
""interactions"": """",
""initialStateCheck"": false
}
],
""bindings"": [
{
""name"": """",
""id"": ""c3b1e700-eea3-426c-863d-36403d537af3"",
""path"": ""<Gamepad>/leftStick"",
""interactions"": """",
""processors"": """",
""groups"": "";Gamepad"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": ""WASD"",
""id"": ""cc5f8773-3f87-4e89-926e-2835221cc71b"",
""path"": ""Dpad"",
""interactions"": """",
""processors"": """",
""groups"": """",
""action"": ""Move"",
""isComposite"": true,
""isPartOfComposite"": false
},
{
""name"": ""up"",
""id"": ""cc98fd24-5e37-4b4a-8fdc-9a1c21bc1e09"",
""path"": ""<Keyboard>/w"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""up"",
""id"": ""59f74329-7d63-4bf5-b3bc-3af75462894f"",
""path"": ""<Keyboard>/upArrow"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""down"",
""id"": ""624a58a3-f4ec-479f-8e1b-3b54ffaa7d8e"",
""path"": ""<Keyboard>/s"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""down"",
""id"": ""2d3501d8-5759-4d58-a909-a5ef3aa51842"",
""path"": ""<Keyboard>/downArrow"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""left"",
""id"": ""14dbcdaf-402d-496b-9798-54b770c4a826"",
""path"": ""<Keyboard>/a"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""left"",
""id"": ""2f4809ce-93a5-44f7-802d-6ac61f1e03e2"",
""path"": ""<Keyboard>/leftArrow"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""right"",
""id"": ""de79a6ae-cf9d-4536-b851-b630e3344d04"",
""path"": ""<Keyboard>/d"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": ""right"",
""id"": ""5bfaeff9-d16a-4fca-b43d-1d6d6b442bc8"",
""path"": ""<Keyboard>/rightArrow"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": true
},
{
""name"": """",
""id"": ""401d3950-45d9-4839-b287-14c2b3902c67"",
""path"": ""<XRController>/{Primary2DAxis}"",
""interactions"": """",
""processors"": """",
""groups"": ""XR"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""b23bd67c-a1c3-44a5-a73b-c824416534fa"",
""path"": ""<Joystick>/stick"",
""interactions"": """",
""processors"": """",
""groups"": ""Joystick"",
""action"": ""Move"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""d1e2f3a4-aim0-4001-8001-000000000011"",
""path"": ""<Gamepad>/rightStick"",
""interactions"": """",
""processors"": """",
""groups"": "";Gamepad"",
""action"": ""Aim"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""d1e2f3a4-fir0-4001-8001-000000000021"",
""path"": ""<Gamepad>/rightTrigger"",
""interactions"": """",
""processors"": """",
""groups"": "";Gamepad"",
""action"": ""Fire"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""d1e2f3a4-fir0-4001-8001-000000000022"",
""path"": ""<Mouse>/leftButton"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Fire"",
""isComposite"": false,
""isPartOfComposite"": false
},
{
""name"": """",
""id"": ""d1e2f3a4-fir0-4001-8001-000000000023"",
""path"": ""<Keyboard>/space"",
""interactions"": """",
""processors"": """",
""groups"": "";Keyboard&Mouse"",
""action"": ""Fire"",
""isComposite"": false,
""isPartOfComposite"": false
}
]
}
],
""controlSchemes"": []
}");
// Gameplay
m_Gameplay = asset.FindActionMap("Gameplay", throwIfNotFound: true);
m_Gameplay_Move = m_Gameplay.FindAction("Move", throwIfNotFound: true);
m_Gameplay_Aim = m_Gameplay.FindAction("Aim", throwIfNotFound: true);
m_Gameplay_Fire = m_Gameplay.FindAction("Fire", throwIfNotFound: true);
}
~@ProjectMInput()
{
UnityEngine.Debug.Assert(!m_Gameplay.enabled, "This will cause a leak and performance issues, ProjectMInput.Gameplay.Disable() has not been called.");
}
/// <summary>
/// Destroys this asset and all associated <see cref="InputAction"/> instances.
/// </summary>
public void Dispose()
{
UnityEngine.Object.Destroy(asset);
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.bindingMask" />
public InputBinding? bindingMask
{
get => asset.bindingMask;
set => asset.bindingMask = value;
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.devices" />
public ReadOnlyArray<InputDevice>? devices
{
get => asset.devices;
set => asset.devices = value;
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.controlSchemes" />
public ReadOnlyArray<InputControlScheme> controlSchemes => asset.controlSchemes;
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.Contains(InputAction)" />
public bool Contains(InputAction action)
{
return asset.Contains(action);
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.GetEnumerator()" />
public IEnumerator<InputAction> GetEnumerator()
{
return asset.GetEnumerator();
}
/// <inheritdoc cref="IEnumerable.GetEnumerator()" />
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.Enable()" />
public void Enable()
{
asset.Enable();
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.Disable()" />
public void Disable()
{
asset.Disable();
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.bindings" />
public IEnumerable<InputBinding> bindings => asset.bindings;
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.FindAction(string, bool)" />
public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)
{
return asset.FindAction(actionNameOrId, throwIfNotFound);
}
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionAsset.FindBinding(InputBinding, out InputAction)" />
public int FindBinding(InputBinding bindingMask, out InputAction action)
{
return asset.FindBinding(bindingMask, out action);
}
// Gameplay
private readonly InputActionMap m_Gameplay;
private List<IGameplayActions> m_GameplayActionsCallbackInterfaces = new List<IGameplayActions>();
private readonly InputAction m_Gameplay_Move;
private readonly InputAction m_Gameplay_Aim;
private readonly InputAction m_Gameplay_Fire;
/// <summary>
/// Provides access to input actions defined in input action map "Gameplay".
/// </summary>
public struct GameplayActions
{
private @ProjectMInput m_Wrapper;
/// <summary>
/// Construct a new instance of the input action map wrapper class.
/// </summary>
public GameplayActions(@ProjectMInput wrapper) { m_Wrapper = wrapper; }
/// <summary>
/// Provides access to the underlying input action "Gameplay/Move".
/// </summary>
public InputAction @Move => m_Wrapper.m_Gameplay_Move;
/// <summary>
/// Provides access to the underlying input action "Gameplay/Aim".
/// </summary>
public InputAction @Aim => m_Wrapper.m_Gameplay_Aim;
/// <summary>
/// Provides access to the underlying input action "Gameplay/Fire".
/// </summary>
public InputAction @Fire => m_Wrapper.m_Gameplay_Fire;
/// <summary>
/// Provides access to the underlying input action map instance.
/// </summary>
public InputActionMap Get() { return m_Wrapper.m_Gameplay; }
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionMap.Enable()" />
public void Enable() { Get().Enable(); }
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionMap.Disable()" />
public void Disable() { Get().Disable(); }
/// <inheritdoc cref="UnityEngine.InputSystem.InputActionMap.enabled" />
public bool enabled => Get().enabled;
/// <summary>
/// Implicitly converts an <see ref="GameplayActions" /> to an <see ref="InputActionMap" /> instance.
/// </summary>
public static implicit operator InputActionMap(GameplayActions set) { return set.Get(); }
/// <summary>
/// Adds <see cref="InputAction.started"/>, <see cref="InputAction.performed"/> and <see cref="InputAction.canceled"/> callbacks provided via <param cref="instance" /> on all input actions contained in this map.
/// </summary>
/// <param name="instance">Callback instance.</param>
/// <remarks>
/// If <paramref name="instance" /> is <c>null</c> or <paramref name="instance"/> have already been added this method does nothing.
/// </remarks>
/// <seealso cref="GameplayActions" />
public void AddCallbacks(IGameplayActions instance)
{
if (instance == null || m_Wrapper.m_GameplayActionsCallbackInterfaces.Contains(instance)) return;
m_Wrapper.m_GameplayActionsCallbackInterfaces.Add(instance);
@Move.started += instance.OnMove;
@Move.performed += instance.OnMove;
@Move.canceled += instance.OnMove;
@Aim.started += instance.OnAim;
@Aim.performed += instance.OnAim;
@Aim.canceled += instance.OnAim;
@Fire.started += instance.OnFire;
@Fire.performed += instance.OnFire;
@Fire.canceled += instance.OnFire;
}
/// <summary>
/// Removes <see cref="InputAction.started"/>, <see cref="InputAction.performed"/> and <see cref="InputAction.canceled"/> callbacks provided via <param cref="instance" /> on all input actions contained in this map.
/// </summary>
/// <remarks>
/// Calling this method when <paramref name="instance" /> have not previously been registered has no side-effects.
/// </remarks>
/// <seealso cref="GameplayActions" />
private void UnregisterCallbacks(IGameplayActions instance)
{
@Move.started -= instance.OnMove;
@Move.performed -= instance.OnMove;
@Move.canceled -= instance.OnMove;
@Aim.started -= instance.OnAim;
@Aim.performed -= instance.OnAim;
@Aim.canceled -= instance.OnAim;
@Fire.started -= instance.OnFire;
@Fire.performed -= instance.OnFire;
@Fire.canceled -= instance.OnFire;
}
/// <summary>
/// Unregisters <param cref="instance" /> and unregisters all input action callbacks via <see cref="GameplayActions.UnregisterCallbacks(IGameplayActions)" />.
/// </summary>
/// <seealso cref="GameplayActions.UnregisterCallbacks(IGameplayActions)" />
public void RemoveCallbacks(IGameplayActions instance)
{
if (m_Wrapper.m_GameplayActionsCallbackInterfaces.Remove(instance))
UnregisterCallbacks(instance);
}
/// <summary>
/// Replaces all existing callback instances and previously registered input action callbacks associated with them with callbacks provided via <param cref="instance" />.
/// </summary>
/// <remarks>
/// If <paramref name="instance" /> is <c>null</c>, calling this method will only unregister all existing callbacks but not register any new callbacks.
/// </remarks>
/// <seealso cref="GameplayActions.AddCallbacks(IGameplayActions)" />
/// <seealso cref="GameplayActions.RemoveCallbacks(IGameplayActions)" />
/// <seealso cref="GameplayActions.UnregisterCallbacks(IGameplayActions)" />
public void SetCallbacks(IGameplayActions instance)
{
foreach (var item in m_Wrapper.m_GameplayActionsCallbackInterfaces)
UnregisterCallbacks(item);
m_Wrapper.m_GameplayActionsCallbackInterfaces.Clear();
AddCallbacks(instance);
}
}
/// <summary>
/// Provides a new <see cref="GameplayActions" /> instance referencing this action map.
/// </summary>
public GameplayActions @Gameplay => new GameplayActions(this);
/// <summary>
/// Interface to implement callback methods for all input action callbacks associated with input actions defined by "Gameplay" which allows adding and removing callbacks.
/// </summary>
/// <seealso cref="GameplayActions.AddCallbacks(IGameplayActions)" />
/// <seealso cref="GameplayActions.RemoveCallbacks(IGameplayActions)" />
public interface IGameplayActions
{
/// <summary>
/// Method invoked when associated input action "Move" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
/// </summary>
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnMove(InputAction.CallbackContext context);
/// <summary>
/// Method invoked when associated input action "Aim" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
/// </summary>
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnAim(InputAction.CallbackContext context);
/// <summary>
/// Method invoked when associated input action "Fire" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
/// </summary>
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
void OnFire(InputAction.CallbackContext context);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9d0226495dbc844da9ddfb554d46aa02
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2d9e7bf5afc824f079f0a869c9948ad9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,98 @@
using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
using UnityEngine;
namespace ProjectM.Client
{
/// <summary>
/// Prototype ARPG follow camera. Attach to the Main Camera (a plain MonoBehaviour): each LateUpdate
/// it frames the local player at a fixed pitch/yaw and distance — the classic top-down/isometric ARPG
/// framing (V Rising / Diablo / PoE2). Every lever is exposed so the feel can be dialled in live in
/// Play Mode. The player's world position is published by <see cref="PrototypeCameraTargetSystem"/>
/// (an ECS system that observes job dependencies safely); the MonoBehaviour never touches the
/// EntityManager directly — doing so from LateUpdate raced the subscene async-load job and threw a
/// job-safety exception at startup. Before a player spawns it frames <see cref="FallbackTarget"/>.
/// <para>
/// Presentation only — uses wall-clock <c>Time.deltaTime</c> for framerate-independent smoothing,
/// which is correct here (not deterministic simulation). Default: mid 3/4 ~45°, perspective.
/// </para>
/// </summary>
[RequireComponent(typeof(Camera))]
[DisallowMultipleComponent]
public class PrototypeCameraRig : MonoBehaviour
{
/// <summary>Local player world position, published each client tick by <see cref="PrototypeCameraTargetSystem"/>.</summary>
public static float3 TargetWorldPos;
/// <summary>True while a locally-owned player exists to follow.</summary>
public static bool HasTarget;
[Header("Angle (degrees)")]
[Range(10f, 89f)] public float Pitch = 45f;
[Range(-180f, 180f)] public float Yaw = 0f;
[Header("Framing")]
[Min(1f)] public float Distance = 16f;
[Tooltip("Raise the look-at point off the ground toward the character's centre of mass.")]
public float TargetHeight = 1f;
[Header("Lens")]
public bool Orthographic = false;
[Range(20f, 90f)] public float FieldOfView = 55f;
[Min(1f)] public float OrthoSize = 10f;
[Header("Follow")]
[Tooltip("Higher = snappier follow. 0 = instant. Framerate-independent.")]
[Min(0f)] public float FollowSharpness = 8f;
[Tooltip("What to frame before a local player exists (edit mode / pre-spawn).")]
public Vector3 FallbackTarget = new Vector3(3f, 0f, 4f);
Camera _cam;
void Awake() => _cam = GetComponent<Camera>();
void LateUpdate()
{
if (_cam == null) _cam = GetComponent<Camera>();
_cam.orthographic = Orthographic;
_cam.fieldOfView = FieldOfView;
_cam.orthographicSize = OrthoSize;
Vector3 target = HasTarget ? (Vector3)TargetWorldPos : FallbackTarget;
target.y += TargetHeight;
var rot = Quaternion.Euler(Pitch, Yaw, 0f);
Vector3 desired = target - (rot * Vector3.forward) * Distance;
float k = FollowSharpness <= 0f ? 1f : 1f - Mathf.Exp(-FollowSharpness * Time.deltaTime);
transform.SetPositionAndRotation(Vector3.Lerp(transform.position, desired, k), rot);
}
}
/// <summary>
/// Publishes the locally-owned player's world position to <see cref="PrototypeCameraRig"/> statics
/// each client tick. Lives in ECS (not the camera MonoBehaviour) so reading <see cref="LocalTransform"/>
/// respects job dependencies — avoiding the subscene-load job-safety exception that direct
/// EntityManager access from LateUpdate caused.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
public partial class PrototypeCameraTargetSystem : SystemBase
{
protected override void OnUpdate()
{
bool found = false;
foreach (var transform in SystemAPI.Query<RefRO<LocalTransform>>()
.WithAll<GhostOwnerIsLocal, PlayerTag>())
{
PrototypeCameraRig.TargetWorldPos = transform.ValueRO.Position;
found = true;
break;
}
PrototypeCameraRig.HasTarget = found;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3e5890693b64a429789bf3edfae0a6ff
@@ -4,6 +4,7 @@
"references": [
"ProjectM.Simulation",
"Unity.Entities",
"Unity.Transforms",
"Unity.Collections",
"Unity.Mathematics",
"Unity.Burst",
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a8dcd28017b9e4abead6e2dc32ef9383
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,68 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
namespace ProjectM.Server
{
/// <summary>
/// Server-authoritative damage application. Drains each damageable entity's
/// <see cref="DamageEvent"/> buffer (appended by <see cref="ProjectileDamageSystem"/> earlier
/// this tick), subtracts the summed amount from <see cref="Health"/>, then clears the buffer so
/// each hit is applied exactly once. Entities that carry character stats (players) clamp to their
/// data-driven <see cref="EffectiveCharacterStats.MaxHealth"/> ceiling; others (training dummies)
/// clamp at zero. A dead <see cref="TrainingDummyTag"/> is destroyed; player death is deferred.
/// Health.Current is a <c>[GhostField]</c>, so the new value replicates to clients for display.
///
/// Runs server-only (<see cref="WorldSystemFilterFlags.ServerSimulation"/>) inside the prediction
/// group so it shares tick timing with movement/damage, where it executes once per tick. The
/// single structural change (destroying a dead dummy) is batched through a frame-allocator
/// <see cref="EntityCommandBuffer"/> that is played back immediately to the entity manager — so a
/// plain-world EditMode test needs no separate ECB system.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[UpdateAfter(typeof(ProjectileDamageSystem))]
[BurstCompile]
public partial struct HealthApplyDamageSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (health, dmg, entity) in
SystemAPI.Query<RefRW<Health>, DynamicBuffer<DamageEvent>>()
.WithEntityAccess())
{
if (dmg.Length == 0)
continue;
float total = 0f;
for (int i = 0; i < dmg.Length; i++)
total += dmg[i].Amount;
dmg.Clear();
float newHp = health.ValueRO.Current - total;
// Effective max health (base + modifiers) is the runtime ceiling for entities that carry
// character stats (players); others just clamp at zero. No auto-heal on a max increase.
if (SystemAPI.HasComponent<EffectiveCharacterStats>(entity))
newHp = math.clamp(newHp, 0f, SystemAPI.GetComponent<EffectiveCharacterStats>(entity).MaxHealth);
else
newHp = math.max(0f, newHp);
health.ValueRW.Current = newHp;
// Server-authoritative death: training dummies despawn; player death is deferred (clamp only).
if (health.ValueRO.Current <= 0f && SystemAPI.HasComponent<TrainingDummyTag>(entity))
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7c1729291515a4966b83ef050554a772
@@ -0,0 +1,145 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-authoritative projectile resolution: applies hits to damageable entities and expires
/// projectiles past their range. Runs in the server world only
/// (<see cref="WorldSystemFilterFlags.ServerSimulation"/>) inside the
/// <see cref="PredictedSimulationSystemGroup"/>, ordered <see cref="ProjectileMoveSystem"/> so the
/// projectile's <see cref="LocalTransform"/> is the post-move position for this tick.
///
/// Hit detection is a <b>swept</b> planar (XZ) test: rather than checking the projectile's point
/// position (which tunnels straight through a target when the per-tick step exceeds the target's
/// radius — e.g. a fast projectile, or any projectile while the server is tick-batching under load),
/// it reconstructs the segment the projectile traversed this tick
/// (<c>[curPos - dir*speed*dt, curPos]</c>) and tests each target's hit radius against the closest
/// point on that segment. The target hit earliest along the path (smallest segment parameter) wins.
/// A target whose <see cref="GhostOwner"/> matches the projectile's owner is skipped (no self-hits);
/// dummies carry no <see cref="GhostOwner"/> and are therefore always valid targets.
///
/// On a hit the system appends a <see cref="DamageEvent"/> to the target (consumed by
/// <c>HealthApplyDamageSystem</c>) and destroys the projectile. Deferring damage to a buffer lets a
/// single tick stack hits from multiple projectiles. All structural changes go through an
/// <see cref="EntityCommandBuffer"/> that plays back immediately to the
/// <see cref="EntityManager"/> (Temp allocator) — keeping this server-only, once-per-tick system
/// self-contained and plain-world testable without a separate ECB system.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[UpdateAfter(typeof(ProjectileMoveSystem))]
public partial struct ProjectileDamageSystem : ISystem
{
/// <summary>Lookup used to read a target's owner so a projectile never hits its own caster.</summary>
ComponentLookup<GhostOwner> m_GhostOwnerLookup;
/// <summary>Extra forgiveness added to a target's hit radius to approximate the projectile's own size.</summary>
const float k_ProjectileRadius = 0.2f;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
m_GhostOwnerLookup = state.GetComponentLookup<GhostOwner>(isReadOnly: true);
// No projectiles → nothing to expire or hit-test; skip the tick (and its allocations) entirely.
state.RequireForUpdate<Projectile>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
m_GhostOwnerLookup.Update(ref state);
float dt = SystemAPI.Time.DeltaTime;
var ecb = new EntityCommandBuffer(Allocator.Temp);
// Snapshot all damageable targets once for this tick. Stable iteration order (query order).
var targetEntities = new NativeList<Entity>(Allocator.Temp);
var targetPositions = new NativeList<float3>(Allocator.Temp);
var targetRadii = new NativeList<float>(Allocator.Temp);
foreach (var (xform, hitRadius, targetEntity) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>>()
.WithAll<Health>()
.WithEntityAccess())
{
targetEntities.Add(targetEntity);
targetPositions.Add(xform.ValueRO.Position);
targetRadii.Add(hitRadius.ValueRO.Value);
}
foreach (var (xform, proj, owner, projectileEntity) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Projectile>, RefRO<GhostOwner>>()
.WithEntityAccess())
{
int projOwnerId = owner.ValueRO.NetworkId;
// This tick's planar travel segment: [segStart -> segEnd]. Sweeping the segment (rather
// than testing only segEnd) is what prevents fast projectiles from tunnelling targets.
float3 cur = xform.ValueRO.Position;
float2 segEnd = new float2(cur.x, cur.z);
float2 segStart = segEnd - proj.ValueRO.Direction * (proj.ValueRO.Speed * dt);
float2 seg = segEnd - segStart;
float segLenSq = math.lengthsq(seg);
int bestIdx = -1;
float bestT = float.MaxValue;
for (int i = 0; i < targetEntities.Length; i++)
{
var target = targetEntities[i];
// Skip the caster: a target whose GhostOwner matches the projectile owner is the
// shooter (or another ghost they own). Dummies have no GhostOwner, so never skipped.
if (m_GhostOwnerLookup.HasComponent(target) &&
m_GhostOwnerLookup[target].NetworkId == projOwnerId)
continue;
float2 tp = new float2(targetPositions[i].x, targetPositions[i].z);
// Closest point on the travel segment to the target centre.
float t = segLenSq > 1e-8f
? math.saturate(math.dot(tp - segStart, seg) / segLenSq)
: 0f;
float2 closest = segStart + t * seg;
float hitDist = targetRadii[i] + k_ProjectileRadius;
if (math.distancesq(tp, closest) <= hitDist * hitDist && t < bestT)
{
bestT = t;
bestIdx = i;
}
}
if (bestIdx >= 0)
{
// Earliest target along the travel path: deal damage and consume the projectile.
ecb.AppendToBuffer(targetEntities[bestIdx], new DamageEvent
{
Amount = proj.ValueRO.Damage,
SourceNetworkId = projOwnerId,
});
ecb.DestroyEntity(projectileEntity);
continue;
}
// Nothing hit this tick: expire the projectile once it has travelled its full range.
if (proj.ValueRO.DistanceTravelled >= proj.ValueRO.Range)
ecb.DestroyEntity(projectileEntity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
targetEntities.Dispose();
targetPositions.Dispose();
targetRadii.Dispose();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f19fa77609fa04c2ca4e293f19c052a4
@@ -0,0 +1,52 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-only, one-shot dummy spawner. On its first update it reads the baked
/// <see cref="TrainingDummySpawner"/> singleton and instantiates <c>Count</c> training-dummy
/// ghosts in a row, spaced <c>Spacing</c> world-units apart along +X starting at <c>Origin</c>.
/// It then destroys the spawner singleton entity so <c>RequireForUpdate<TrainingDummySpawner></c>
/// is no longer satisfied and the system stops running (idempotent: dummies are spawned exactly once).
/// Runs in the default <see cref="SimulationSystemGroup"/> (NOT the prediction loop) since spawning is
/// a non-predicted, server-authoritative event; the dummies replicate to clients as interpolated ghosts.
/// All structural changes are batched through an <see cref="EntityCommandBuffer"/>.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct TrainingDummySpawnSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<TrainingDummySpawner>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Grab both the singleton entity (to destroy when done) and its baked config.
var spawnerEntity = SystemAPI.GetSingletonEntity<TrainingDummySpawner>();
var spawner = SystemAPI.GetComponent<TrainingDummySpawner>(spawnerEntity);
var ecb = new EntityCommandBuffer(Allocator.Temp);
for (int i = 0; i < spawner.Count; i++)
{
var dummy = ecb.Instantiate(spawner.Prefab);
var position = spawner.Origin + new float3(i * spawner.Spacing, 0f, 0f);
ecb.SetComponent(dummy, LocalTransform.FromPosition(position));
}
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
ecb.DestroyEntity(spawnerEntity);
ecb.Playback(state.EntityManager);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7673acbc74f7a4bddbca0679122511ea
@@ -0,0 +1,52 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-only, one-shot upgrade-pickup spawner (mirrors TrainingDummySpawnSystem). On its first
/// update it reads the baked <see cref="UpgradePickupSpawner"/> singleton and instantiates
/// <c>Count</c> pickup ghosts in a row, spaced <c>Spacing</c> world-units apart along +X starting at
/// <c>Origin</c>, then destroys the singleton so the system idles (spawned exactly once). Runs in the
/// default <see cref="SimulationSystemGroup"/> (NOT the prediction loop); pickups replicate to clients
/// as interpolated ghosts. Structural changes are batched through an <see cref="EntityCommandBuffer"/>.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct UpgradePickupSpawnSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<UpgradePickupSpawner>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var spawnerEntity = SystemAPI.GetSingletonEntity<UpgradePickupSpawner>();
var spawner = SystemAPI.GetComponent<UpgradePickupSpawner>(spawnerEntity);
var ecb = new EntityCommandBuffer(Allocator.Temp);
if (spawner.Prefab != Entity.Null)
{
for (int i = 0; i < spawner.Count; i++)
{
var pickup = ecb.Instantiate(spawner.Prefab);
var position = spawner.Origin + new float3(i * spawner.Spacing, 0f, 0f);
ecb.SetComponent(pickup, LocalTransform.FromPosition(position));
}
}
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
ecb.DestroyEntity(spawnerEntity);
ecb.Playback(state.EntityManager);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 602544c80ca5649258a9d6ca9ad74f79
@@ -0,0 +1,78 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-authoritative upgrade pickup grant. When a player overlaps an <see cref="UpgradePickup"/>
/// (planar XZ distance within the pickup's <see cref="HitRadius"/>), appends the pickup's modifier to
/// the player's replicated <see cref="StatModifier"/> buffer (which replicates to the predicting
/// owner, so StatRecomputeSystem folds identical effective stats on both worlds) and destroys the
/// pickup. Runs in the default <see cref="SimulationSystemGroup"/> (NOT the prediction loop) since the
/// grant is a non-predicted server event. The buffer append + pickup destroy are batched through an
/// <see cref="EntityCommandBuffer"/> played back immediately — so a plain-world EditMode test needs no
/// separate ECB system.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct UpgradePickupSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<UpgradePickup>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Snapshot modifiable players (carrying the modifier buffer + a transform) once this tick.
var playerEntities = new NativeList<Entity>(Allocator.Temp);
var playerPositions = new NativeList<float3>(Allocator.Temp);
foreach (var (xform, e) in
SystemAPI.Query<RefRO<LocalTransform>>()
.WithAll<PlayerTag, StatModifier>()
.WithEntityAccess())
{
playerEntities.Add(e);
playerPositions.Add(xform.ValueRO.Position);
}
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (xform, radius, pickup, pickupEntity) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>, RefRO<UpgradePickup>>()
.WithEntityAccess())
{
float2 pp = new float2(xform.ValueRO.Position.x, xform.ValueRO.Position.z);
float r = radius.ValueRO.Value;
for (int i = 0; i < playerEntities.Length; i++)
{
float2 cp = new float2(playerPositions[i].x, playerPositions[i].z);
if (math.distancesq(pp, cp) > r * r)
continue;
ecb.AppendToBuffer(playerEntities[i], new StatModifier
{
Target = pickup.ValueRO.Target,
Op = pickup.ValueRO.Op,
Value = pickup.ValueRO.Value,
SourceId = pickup.ValueRO.SourceId,
});
ecb.DestroyEntity(pickupEntity);
break; // granted to the first overlapping player, then despawns
}
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
playerEntities.Dispose();
playerPositions.Dispose();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c3d9ef25fbc464e52aa342b531d6f35e
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 94ea01b14c1384c9f96d8f7bd6ac1a14
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
#if UNITY_EDITOR
using System.Collections.Generic;
using ProjectM.Simulation;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Server
{
/// <summary>
/// Editor-only debug hook (mirrors ProjectM.Client.DebugInputInjectionSystem's static-poke pattern)
/// for driving the server-authoritative modifier stack from MCP execute_code. Because modifiers are
/// server-authoritative, a client-side append would be stomped by the next snapshot, so this runs in
/// the SERVER world: the change flows back through the snapshot and is prediction-correct on the
/// client. In-editor single-process only (client + server worlds in one process). Poke from execute_code:
/// DebugModifierInjectionSystem.AddModifier((byte)StatTarget.Damage, (byte)ModOp.Flat, 50f);
/// DebugModifierInjectionSystem.AddModifier((byte)StatTarget.MoveSpeed, (byte)ModOp.PercentAdd, 0.5f);
/// DebugModifierInjectionSystem.CycleAbility(); // Primary -> FastLight -> SlowHeavy -> Primary
/// DebugModifierInjectionSystem.ClearModifiers();
/// All applied to the first player on the next server tick.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial class DebugModifierInjectionSystem : SystemBase
{
struct PendingModifier { public byte Target; public byte Op; public float Value; }
static readonly List<PendingModifier> s_Pending = new List<PendingModifier>();
static bool s_Clear;
static bool s_Cycle;
/// <summary>Queue a modifier to append to the first player on the next server tick.</summary>
public static void AddModifier(byte target, byte op, float value)
{
s_Pending.Add(new PendingModifier { Target = target, Op = op, Value = value });
}
/// <summary>Clear the first player's whole modifier stack on the next server tick.</summary>
public static void ClearModifiers() => s_Clear = true;
/// <summary>Cycle the first player's primary ability id on the next server tick.</summary>
public static void CycleAbility() => s_Cycle = true;
protected override void OnUpdate()
{
if (s_Pending.Count == 0 && !s_Clear && !s_Cycle)
return;
Entity player = Entity.Null;
foreach (var (abilityRef, e) in
SystemAPI.Query<RefRO<AbilityRef>>().WithAll<PlayerTag, StatModifier>().WithEntityAccess())
{
player = e;
break;
}
if (player == Entity.Null)
return;
if (s_Clear)
{
EntityManager.GetBuffer<StatModifier>(player).Clear();
s_Clear = false;
}
if (s_Pending.Count > 0)
{
var buffer = EntityManager.GetBuffer<StatModifier>(player);
for (int i = 0; i < s_Pending.Count; i++)
{
var m = s_Pending[i];
buffer.Add(new StatModifier { Target = m.Target, Op = m.Op, Value = m.Value, SourceId = 0u });
}
s_Pending.Clear();
}
if (s_Cycle)
{
var abilityRef = EntityManager.GetComponentData<AbilityRef>(player);
abilityRef.Id = abilityRef.Id switch
{
(byte)AbilityId.Primary => (byte)AbilityId.FastLight,
(byte)AbilityId.FastLight => (byte)AbilityId.SlowHeavy,
_ => (byte)AbilityId.Primary,
};
EntityManager.SetComponentData(player, abilityRef);
s_Cycle = false;
}
}
}
}
#endif
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1f8906df777734bbcb3f21100d6e664e
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3a7ea79ccc9c74c62a94c777999859c6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Predicted per-player ability cooldown gate. Holds the earliest server tick at which the
/// owning player may fire again, so <see cref="AbilityFireSystem"/> can throttle shots
/// deterministically across client prediction and server simulation.
/// <para>
/// Replicated as a <see cref="GhostField"/> so the cooldown survives the frame→tick→rollback
/// boundary: when the client re-predicts ticks after a snapshot, it sees the same authoritative
/// gate the server applied and converges without double-firing. Stored as a raw <c>uint</c>
/// rather than a <see cref="NetworkTick"/> for simple, quantization-free serialization; compare
/// by wrapping it back into a <see cref="NetworkTick"/> and using
/// <see cref="NetworkTick.IsNewerThan"/> (raw subtraction is unsafe across tick wraparound).
/// </para>
/// </summary>
public struct AbilityCooldown : IComponentData
{
/// <summary>
/// Raw tick value of the earliest tick the player may fire again. <c>0</c> = ready (no
/// cooldown pending). Set by <see cref="AbilityFireSystem"/> to
/// <c>serverTick + max(1, CooldownTicks)</c> on fire; treat as "still cooling down" only
/// while a valid <see cref="NetworkTick"/> built from it is newer than the current
/// <c>ServerTick</c>.
/// </summary>
[GhostField] public uint NextFireTick;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b7a2b67b22b2a4abaa8efd84759445c0
@@ -0,0 +1,14 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Singleton handle to the baked ability/character definition database (config, not replicated -
/// baked identically into both worlds from the gameplay subscene). The companion AbilityPrefabElement
/// buffer on the same entity carries the per-ability projectile prefab entity refs.
/// </summary>
public struct AbilityDatabase : IComponentData
{
public BlobAssetReference<AbilityDatabaseBlob> Value;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6b297f7d451084342af25012ccb3a3e8
@@ -0,0 +1,74 @@
using Unity.Collections;
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>One authored ability definition, baked immutable into the AbilityDatabase blob.</summary>
public struct AbilityDefBlob
{
public byte Id; // AbilityId
public float Damage;
public float ProjectileSpeed;
public float Range;
public float AutoTargetRange;
public float AutoTargetConeRadians;
public int CooldownTicks;
public FixedString64Bytes Name;
}
/// <summary>One authored character-stats definition, baked immutable into the AbilityDatabase blob.</summary>
public struct CharacterStatsBlob
{
public byte Id; // CharacterId
public float MoveSpeed;
public float TurnRateRadiansPerSec;
public float MaxHealth;
public FixedString64Bytes Name;
}
/// <summary>
/// Immutable designer-authored definition database, baked from ScriptableObjects to a blob asset and
/// shared by every entity (Burst-fast, zero per-instance cost). Looked up by stable id. Entity/prefab
/// references are NOT stored here (blobs don't remap entity refs) - see AbilityPrefabElement.
///
/// NOTE: the lookups are intentionally NOT 'readonly' methods. A readonly struct method forces a
/// defensive copy of a field when calling a non-readonly member on it; copying a BlobArray breaks its
/// relative-offset pointer, so the array would read as empty. Plain (non-readonly) methods access the
/// BlobArray in place. Always reach these through 'ref blob.Value'.
/// </summary>
public struct AbilityDatabaseBlob
{
public BlobArray<AbilityDefBlob> Abilities;
public BlobArray<CharacterStatsBlob> Characters;
/// <summary>Linear lookup by ability id (the array is tiny). Returns false if not present.</summary>
public bool TryGetAbility(byte id, out AbilityDefBlob def)
{
for (int i = 0; i < Abilities.Length; i++)
{
if (Abilities[i].Id == id)
{
def = Abilities[i];
return true;
}
}
def = default;
return false;
}
/// <summary>Linear lookup by character id (the array is tiny). Returns false if not present.</summary>
public bool TryGetCharacter(byte id, out CharacterStatsBlob def)
{
for (int i = 0; i < Characters.Length; i++)
{
if (Characters[i].Id == id)
{
def = Characters[i];
return true;
}
}
def = default;
return false;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 974be1ee95bef486ea49a4d42ecc9796
@@ -0,0 +1,173 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Simulation
{
/// <summary>
/// Predicted "fire" ability: on the single fully-predicting pass of each tick, spawns a
/// Projectile ghost for every player whose PlayerInput.Fire event is set this tick and whose
/// AbilityCooldown has elapsed. Runs in both worlds: the owning client predict-spawns the
/// projectile (classified into the authoritative ghost by ProjectileClassificationSystem via the
/// Projectile.SpawnId key), and the server spawns the replicated truth.
///
/// M3 data-driven: ability stats are read from the per-entity EffectiveAbilityStats (authored base
/// from the AbilityDatabase blob keyed by AbilityRef, folded with the replicated StatModifier buffer
/// by StatRecomputeSystem earlier this tick). The projectile ghost prefab is resolved per ability via
/// the AbilityPrefabElement buffer on the AbilityDatabase singleton. Effective Speed/Damage/Range are
/// snapshotted into the spawned Projectile, so the downstream move/damage systems are unchanged and
/// predicted + server projectiles match (both folded the same replicated modifiers).
///
/// Determinism / idempotency: the prediction loop re-runs this system on rollback, so all
/// non-idempotent effects (spawning, cooldown advance) are gated behind
/// NetworkTime.IsFirstTimeFullyPredictingTick so they happen exactly once per tick. The absolute
/// fire count comes from the replicated input command buffer at NetworkTime.ServerTick (not a
/// local counter) so the SpawnId matches on client and server. No wall-clock, no System.Random,
/// no UnityEngine.Time.
///
/// Auto-target is intentionally server-only: the client fires along raw aim, and the server's
/// authoritative Projectile.Direction GhostField reconciles the predicted projectile to the
/// assisted heading.
/// </summary>
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[UpdateAfter(typeof(PlayerAimSystem))]
[BurstCompile]
public partial struct AbilityFireSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<AbilityDatabase>();
state.RequireForUpdate<NetworkTime>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Spawning is a one-off effect: only run on the unique fully-predicting pass of this tick
// so a rollback re-simulation does not double-spawn.
var networkTime = SystemAPI.GetSingleton<NetworkTime>();
if (!networkTime.IsFirstTimeFullyPredictingTick)
return;
var serverTick = networkTime.ServerTick;
if (!serverTick.IsValid)
return;
// Per-ability projectile ghost prefabs live on the AbilityDatabase singleton's companion buffer.
var dbEntity = SystemAPI.GetSingletonEntity<AbilityDatabase>();
var abilityPrefabs = SystemAPI.GetBuffer<AbilityPrefabElement>(dbEntity);
bool isServer = state.WorldUnmanaged.IsServer();
// Server-only auto-target candidate set: training-dummy world XZ positions, collected once.
var candidatePositions = new NativeList<float3>(Allocator.Temp);
if (isServer)
{
foreach (var dummyTransform in
SystemAPI.Query<RefRO<LocalTransform>>().WithAll<TrainingDummyTag>())
{
candidatePositions.Add(dummyTransform.ValueRO.Position);
}
}
var candidates = candidatePositions.AsArray();
var ecb = new EntityCommandBuffer(state.WorldUpdateAllocator);
foreach (var (input, facing, xform, eff, abilityRef, cd, owner, entity) in
SystemAPI.Query<RefRO<PlayerInput>, RefRO<PlayerFacing>, RefRO<LocalTransform>,
RefRO<EffectiveAbilityStats>, RefRO<AbilityRef>, RefRW<AbilityCooldown>,
RefRO<GhostOwner>>()
.WithAll<Simulate>()
.WithEntityAccess())
{
// The InputEvent on the component carries the per-tick delta: set => fired this tick.
if (!input.ValueRO.Fire.IsSet)
continue;
// Cooldown gate. NextFireTick == 0 means "ready". Otherwise the player may fire only
// once serverTick is at-or-newer than the stored tick (i.e. the stored tick is not
// strictly newer than now).
uint nextFireRaw = cd.ValueRO.NextFireTick;
if (nextFireRaw != 0)
{
var nextTick = new NetworkTick(nextFireRaw);
if (nextTick.IsValid && nextTick.IsNewerThan(serverTick))
continue; // still cooling down
}
// Resolve the projectile ghost prefab for this player's selected ability id.
Entity prefab = Entity.Null;
for (int i = 0; i < abilityPrefabs.Length; i++)
{
if (abilityPrefabs[i].Id == abilityRef.ValueRO.Id)
{
prefab = abilityPrefabs[i].Prefab;
break;
}
}
if (prefab == Entity.Null)
continue; // ability has no projectile prefab wired
// Absolute (monotonic) fire count from the replicated command buffer at this tick.
// This is the classification key shared by client prediction and server truth.
var inputBuffer = SystemAPI.GetBuffer<InputBufferData<PlayerInput>>(entity);
if (!inputBuffer.GetDataAtTick(serverTick, out var applied))
continue;
uint absoluteFireCount = applied.InternalInput.Fire.Count;
float2 rawAim = facing.ValueRO.Direction;
if (math.lengthsq(rawAim) < 1e-6f)
rawAim = new float2(0f, 1f);
else
rawAim = math.normalize(rawAim);
// Client fires along raw aim; only the server applies the auto-target assist cone.
float2 dir = rawAim;
if (isServer && eff.ValueRO.AutoTargetRange > 0f)
{
dir = AutoTarget.Resolve(
xform.ValueRO.Position,
rawAim,
eff.ValueRO.AutoTargetRange,
eff.ValueRO.AutoTargetConeRadians,
candidates);
}
uint spawnId = (uint)owner.ValueRO.NetworkId << 16 | absoluteFireCount;
var projectile = ecb.Instantiate(prefab);
float3 planarDir = new float3(dir.x, 0f, dir.y);
float3 spawnPos = xform.ValueRO.Position + planarDir * 0.6f;
spawnPos.y = xform.ValueRO.Position.y;
quaternion rot = quaternion.LookRotationSafe(planarDir, math.up());
ecb.SetComponent(projectile, LocalTransform.FromPositionRotation(spawnPos, rot));
ecb.SetComponent(projectile, new GhostOwner { NetworkId = owner.ValueRO.NetworkId });
// Snapshot the effective ability stats into the projectile (base + modifiers, computed
// identically on both worlds), so the move/damage systems need no modifier lookup.
ecb.SetComponent(projectile, new Projectile
{
Direction = math.normalize(dir),
SpawnId = spawnId,
Speed = eff.ValueRO.ProjectileSpeed,
Damage = eff.ValueRO.Damage,
Range = eff.ValueRO.Range,
DistanceTravelled = 0f,
});
// Earliest raw tick the player may fire again. Clamp cooldown to >= 1 tick.
uint cooldownTicks = (uint)math.max(1, eff.ValueRO.CooldownTicks);
cd.ValueRW.NextFireTick = serverTick.TickIndexForValidTick + cooldownTicks;
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
candidatePositions.Dispose();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 473b7521bce4d4d1abc794bcd4e8e6fe
@@ -0,0 +1,15 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Companion buffer on the AbilityDatabase singleton mapping an ability id to its projectile ghost
/// prefab entity. Prefab/entity references are kept OUT of the blob (blob assets don't remap entity
/// references); they are baked here via GetEntity, which the entity serializer patches correctly.
/// </summary>
public struct AbilityPrefabElement : IBufferElementData
{
public byte Id; // AbilityId
public Entity Prefab;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 91844bb3b4d7843318fc0cdbfe68d43e
@@ -0,0 +1,15 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Which authored ability definition occupies this entity's primary slot - a light replicated key
/// into the AbilityDatabase blob, replacing M2's inlined AbilityStats values. Replicated so an
/// ability swap is server-authoritative and prediction-correct. <c>Id</c> stores an <see cref="AbilityId"/>.
/// </summary>
public struct AbilityRef : IComponentData
{
[GhostField] public byte Id;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d6ea08a11ef3d4afdb722b735ca3ed03
@@ -0,0 +1,92 @@
using Unity.Collections;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Pure, deterministic auto-target assist helper for player abilities (M2 combat). Given a shooter
/// position, a raw aim direction, and a set of candidate target positions, picks the best target
/// inside an assist cone and snaps the shot toward it; otherwise returns the raw aim unchanged.
/// <para>
/// Authored as a <see langword="static"/> class (no state) so it is Burst-safe and allocation-free,
/// callable from inside predicted/jobified systems. It is intended to run server-side only (see
/// <c>AbilityFireSystem</c>) — the server's authoritative <c>Projectile.Direction</c> GhostField then
/// reconciles the client's raw-aim predicted projectile. Determinism: no wall-clock, no randomness,
/// pure math; ties are broken by smallest candidate index so identical inputs always yield identical
/// output across the prediction loop and across machines.
/// </para>
/// </summary>
public static class AutoTarget
{
/// <summary>
/// Resolves the planar (XZ) direction a shot should take. Returns the normalized direction toward
/// the nearest candidate within <paramref name="autoTargetRange"/> whose bearing from
/// <paramref name="from"/> lies within <paramref name="coneHalfAngleRadians"/> of
/// <paramref name="rawAimDir"/>; if no candidate qualifies, returns <paramref name="rawAimDir"/>
/// unchanged.
/// </summary>
/// <param name="from">Shooter world position; only the XZ plane is considered.</param>
/// <param name="rawAimDir">
/// Caller-normalized planar aim direction (world XZ mapped to <c>float2(x, y)</c>). If it is
/// effectively zero-length, it is returned unchanged (no valid heading to assist).
/// </param>
/// <param name="autoTargetRange">Max planar distance to consider a candidate; <c>0</c> (or less) disables assist.</param>
/// <param name="coneHalfAngleRadians">Half-angle of the assist cone, measured from <paramref name="rawAimDir"/>.</param>
/// <param name="candidatePositions">Candidate target world positions (XZ used). Read-only; not modified.</param>
/// <returns>
/// The normalized direction toward the chosen candidate, or <paramref name="rawAimDir"/> when no
/// candidate qualifies. Ties on distance are broken by the smallest candidate index for determinism.
/// </returns>
public static float2 Resolve(float3 from, float2 rawAimDir, float autoTargetRange, float coneHalfAngleRadians,
in NativeArray<float3> candidatePositions)
{
// No valid heading to assist along — caller guarantees normalization, but guard zero-length.
if (math.lengthsq(rawAimDir) < 1e-6f)
return rawAimDir;
// Disabled / nothing to consider.
if (autoTargetRange <= 0f || candidatePositions.Length == 0)
return rawAimDir;
float rangeSq = autoTargetRange * autoTargetRange;
float cosCone = math.cos(coneHalfAngleRadians);
int bestIndex = -1;
float bestDistSq = float.MaxValue;
float2 bestDir = rawAimDir;
for (int i = 0; i < candidatePositions.Length; i++)
{
// Planar (XZ) offset from shooter to candidate.
float3 offset = candidatePositions[i] - from;
float2 planar = new float2(offset.x, offset.z);
float distSq = math.lengthsq(planar);
// Skip self / coincident candidates (effectively zero distance → undefined bearing).
if (distSq < 1e-6f)
continue;
// Out of range.
if (distSq > rangeSq)
continue;
// Bearing test: dot of unit bearing with the (unit) raw aim vs cos(half-angle).
float2 dir = planar * math.rsqrt(distSq); // normalized planar bearing
float dot = math.dot(dir, rawAimDir);
if (dot < cosCone)
continue; // outside the assist cone
// Nearest wins; strict less-than keeps the first (smallest-index) candidate on ties.
if (distSq < bestDistSq)
{
bestDistSq = distSq;
bestIndex = i;
bestDir = dir;
}
}
return bestIndex >= 0 ? bestDir : rawAimDir;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ac02264d177dd426e8f6972c7c3ceaae
@@ -0,0 +1,20 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// One pending hit against a damageable entity, queued as a per-entity buffer element. The server
/// appends a DamageEvent when a projectile hits (ProjectileDamageSystem), then HealthApplyDamageSystem
/// drains the buffer once per tick to subtract from Health. Buffering decouples hit detection from
/// health resolution and lets multiple simultaneous hits accumulate before being applied. Not
/// replicated — only Health.Current is a GhostField; the buffer is server-side and cleared each tick.
/// </summary>
public struct DamageEvent : IBufferElementData
{
/// <summary>Damage to subtract from the target's Health.Current (world health units).</summary>
public float Amount;
/// <summary>NetworkId of the firing player that caused this hit (attribution / self-hit filtering upstream).</summary>
public int SourceNetworkId;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 498738282d585418893b23454a4b88a0
@@ -0,0 +1,20 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Per-entity effective ability stats: the authored base (from the AbilityDatabase blob keyed by
/// AbilityRef) folded with the entity's StatModifier buffer by StatRecomputeSystem each predicted
/// tick. Derived/local, NOT replicated - both worlds recompute it deterministically from the
/// replicated modifier buffer, so it matches under prediction without being in the snapshot.
/// </summary>
public struct EffectiveAbilityStats : IComponentData
{
public float Damage;
public float ProjectileSpeed;
public float Range;
public float AutoTargetRange;
public float AutoTargetConeRadians;
public int CooldownTicks;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a8bb3a5c343e74e7fb249e96c0c55fdc
@@ -0,0 +1,16 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Per-entity effective character stats (movement + survivability): authored base (from the
/// CharacterStats blob keyed by CharacterStatsRef) folded with the StatModifier buffer by
/// StatRecomputeSystem each predicted tick. Derived/local, NOT replicated (see EffectiveAbilityStats).
/// </summary>
public struct EffectiveCharacterStats : IComponentData
{
public float MoveSpeed;
public float TurnRateRadiansPerSec;
public float MaxHealth;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1d8413eebbeda4761b33430128e7a437

Some files were not shown because too many files have changed in this diff Show More