Characters
This commit is contained in:
8
Game.meta
Normal file
8
Game.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3aa05ef999c0db8c9a37646187699217
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
32
Game/AGENTS.md
Normal file
32
Game/AGENTS.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
- Unity assets live under `Assets/Game/`; `Hero/`, `Enemy/`, and `Scenes/` store prefabs and scene content.
|
||||||
|
- Gameplay code is in `Assets/Game/Scripts/`, grouped by domain: `ThirdPerson*` for locomotion, `Combat/` for health & damage, and `WeaponSystem/` for weapons, projectiles, and data assets.
|
||||||
|
- Keep shared multiplayer logic in `Scripts/` so both host and clients load identical behaviours; place editor-only utilities under an `Editor/` folder to avoid runtime inclusion.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
- Open the project with the Unity Hub or run `unity -projectPath ./` from the repo root to launch the editor.
|
||||||
|
- Generate a standalone build with `unity -projectPath ./ -buildTarget StandaloneWindows64 -executeMethod BuildScripts.BuildClient` (create the `BuildClient` method in an editor script if missing).
|
||||||
|
- Use the Unity Test Runner (`Window > General > Test Runner`) and run both Edit Mode and Play Mode suites before merging gameplay changes.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
- Follow standard C# conventions: PascalCase for classes, methods, and public members; camelCase for private fields; prefix serialized private fields with `[SerializeField]` and keep them private.
|
||||||
|
- Organize namespaces under `MegaKoop.Game.<Feature>` to mirror the folder structure.
|
||||||
|
- Prefer composition-friendly MonoBehaviours with explicit `SerializeField` dependencies; avoid singletons in gameplay code unless wrapped for network synchronisation.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
- Use Unity’s built-in NUnit framework for component tests; name files `<Feature>Tests.cs` and mirror the folder of the code under test.
|
||||||
|
- Favour deterministic Play Mode tests that exercise Steamworks stubs or mock networking flows; document any non-deterministic behaviour in the test summary.
|
||||||
|
- Aim for tests around new systems that affect combat sync, projectiles, or hero abilities so regressions are caught pre-merge.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
- Commit messages should be imperative and scoped (e.g., `Add projectile lifetime clamps`).
|
||||||
|
- PRs must describe gameplay impact, list affected scenes/prefabs, and include replication steps for multiplayer behaviour.
|
||||||
|
- Link to tracking tasks and attach editor or in-game screenshots/GIFs when modifying hero abilities, UI, or VFX.
|
||||||
|
|
||||||
|
## Multiplayer & Online Rules
|
||||||
|
- Treat every feature as networked-first: verify authority flows, replication, and prediction before adding offline shortcuts.
|
||||||
|
- Integrate Steamworks.NET for session management; keep wrappers in a dedicated networking layer so gameplay systems call high-level abstractions.
|
||||||
|
- When adding hero abilities, ensure weapon firing, damage, and state changes trigger RPCs/events that work for host migration and client late-join scenarios.
|
||||||
|
- Document any temp offline fallbacks and add TODOs to replace them with Steamworks-backed implementations.
|
||||||
7
Game/AGENTS.md.meta
Normal file
7
Game/AGENTS.md.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 26880776f1200689dbc1be5c197a03c0
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Game/Enemy.meta
Normal file
8
Game/Enemy.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f250f7f11c39195b2a38e7f052154182
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Game/Enemy/Golem.meta
Normal file
8
Game/Enemy/Golem.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a9df8db4a9428a557a3f6f46b6652d79
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
3220
Game/Enemy/Golem/Golem.prefab
Normal file
3220
Game/Enemy/Golem/Golem.prefab
Normal file
File diff suppressed because it is too large
Load Diff
9
Game/Enemy/Golem/Golem.prefab.meta
Normal file
9
Game/Enemy/Golem/Golem.prefab.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b5051c49d05768c73a8c42e1967fe4b2
|
||||||
|
timeCreated: 1526423972
|
||||||
|
licenseType: Store
|
||||||
|
NativeFormatImporter:
|
||||||
|
mainObjectFileID: 100100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Game/Hero.meta
Normal file
8
Game/Hero.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: de06818812108c54f8cc6429c1370c05
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Game/Hero/Weapons.meta
Normal file
8
Game/Hero/Weapons.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cf250eac73dbba38d947df7a05910bc5
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Game/Hero/Weapons/Staff.meta
Normal file
8
Game/Hero/Weapons/Staff.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2f55c0b12239df279b90e7e62c02f093
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
4847
Game/Hero/Weapons/Staff/Projectile_Fireball.prefab
Normal file
4847
Game/Hero/Weapons/Staff/Projectile_Fireball.prefab
Normal file
File diff suppressed because it is too large
Load Diff
9
Game/Hero/Weapons/Staff/Projectile_Fireball.prefab.meta
Normal file
9
Game/Hero/Weapons/Staff/Projectile_Fireball.prefab.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6703b124cb13a577c8aae6a4851d0274
|
||||||
|
timeCreated: 1526434856
|
||||||
|
licenseType: Store
|
||||||
|
NativeFormatImporter:
|
||||||
|
mainObjectFileID: 100100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
27
Game/Hero/Weapons/Staff/Staff.asset
Normal file
27
Game/Hero/Weapons/Staff/Staff.asset
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
%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: 2fd5ff95d6e2be5d09b6c054570cef0a, type: 3}
|
||||||
|
m_Name: Staff
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.WeaponSystem.WeaponDefinition
|
||||||
|
displayName: Weapon
|
||||||
|
viewPrefab: {fileID: 6595277065068154610, guid: 7eba33411273ad195adc8a8253711e93, type: 3}
|
||||||
|
projectilePrefab: {fileID: -6920969466594260193, guid: 6703b124cb13a577c8aae6a4851d0274, type: 3}
|
||||||
|
projectileSpeed: 10
|
||||||
|
projectileLifetime: 5
|
||||||
|
shotsPerSecond: 1
|
||||||
|
baseDamage: 10
|
||||||
|
range: 25
|
||||||
|
projectilesPerShot: 1
|
||||||
|
spreadAngle: 0
|
||||||
|
hitMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 256
|
||||||
8
Game/Hero/Weapons/Staff/Staff.asset.meta
Normal file
8
Game/Hero/Weapons/Staff/Staff.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 912de7c15ecf9d38d9be364bc0ac0f70
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
138
Game/Hero/Weapons/Staff/Staff.prefab
Normal file
138
Game/Hero/Weapons/Staff/Staff.prefab
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1 &1515352688969276
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 4447648817959868}
|
||||||
|
- component: {fileID: 33814000495682932}
|
||||||
|
- component: {fileID: 23588202079409966}
|
||||||
|
- component: {fileID: 6595277065068154610}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Staff
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &4447648817959868
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1515352688969276}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
|
||||||
|
m_LocalPosition: {x: -0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children:
|
||||||
|
- {fileID: 2459495563451568810}
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!33 &33814000495682932
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1515352688969276}
|
||||||
|
m_Mesh: {fileID: 4300000, guid: b5a701b2b15d1b8449e77534e99ea3ed, type: 3}
|
||||||
|
--- !u!23 &23588202079409966
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1515352688969276}
|
||||||
|
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: caf01dd5152f3934d8079e6160aa3b1e, 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: 0
|
||||||
|
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_AdditionalVertexStreams: {fileID: 0}
|
||||||
|
--- !u!114 &6595277065068154610
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1515352688969276}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 5590d430717d95f878b7929af7bf28ff, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.WeaponSystem.WeaponView
|
||||||
|
muzzles:
|
||||||
|
- {fileID: 2459495563451568810}
|
||||||
|
--- !u!1 &4268583630942894705
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 2459495563451568810}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Muzzle
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &2459495563451568810
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 4268583630942894705}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: -0, y: 0, z: -0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 1.004, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 4447648817959868}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
9
Game/Hero/Weapons/Staff/Staff.prefab.meta
Normal file
9
Game/Hero/Weapons/Staff/Staff.prefab.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7eba33411273ad195adc8a8253711e93
|
||||||
|
timeCreated: 1526437063
|
||||||
|
licenseType: Store
|
||||||
|
NativeFormatImporter:
|
||||||
|
mainObjectFileID: 100100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
2449
Game/Hero/Wizard.prefab
Normal file
2449
Game/Hero/Wizard.prefab
Normal file
File diff suppressed because it is too large
Load Diff
7
Game/Hero/Wizard.prefab.meta
Normal file
7
Game/Hero/Wizard.prefab.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fe75fe22781f92b369675fdfc9657f7d
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Game/Scripts.meta
Normal file
8
Game/Scripts.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e1468aaa4e1f2d7d08d7d6e9acb26be7
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Game/Scripts/Combat.meta
Normal file
8
Game/Scripts/Combat.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 43cbd7a1d6f0054838d3503a2cc2b68f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
27
Game/Scripts/Combat/DamagePayload.cs
Normal file
27
Game/Scripts/Combat/DamagePayload.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Combat
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bundles damage information so different systems can respond consistently.
|
||||||
|
/// </summary>
|
||||||
|
public struct DamagePayload
|
||||||
|
{
|
||||||
|
public float Amount;
|
||||||
|
public Vector3 HitPoint;
|
||||||
|
public Vector3 HitNormal;
|
||||||
|
public GameObject Source;
|
||||||
|
public Team SourceTeam;
|
||||||
|
public GameObject Target;
|
||||||
|
|
||||||
|
public DamagePayload(float amount, Vector3 hitPoint, Vector3 hitNormal, GameObject source, Team sourceTeam, GameObject target)
|
||||||
|
{
|
||||||
|
Amount = amount;
|
||||||
|
HitPoint = hitPoint;
|
||||||
|
HitNormal = hitNormal;
|
||||||
|
Source = source;
|
||||||
|
SourceTeam = sourceTeam;
|
||||||
|
Target = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Combat/DamagePayload.cs.meta
Normal file
2
Game/Scripts/Combat/DamagePayload.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7e3b00f0d0eff2b0e813e41895012e02
|
||||||
97
Game/Scripts/Combat/Health.cs
Normal file
97
Game/Scripts/Combat/Health.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Combat
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class Health : MonoBehaviour, IDamageable
|
||||||
|
{
|
||||||
|
[SerializeField] private float maxHealth = 100f;
|
||||||
|
[SerializeField] private Team team = Team.Neutral;
|
||||||
|
[SerializeField] private bool ignoreFriendlyFire = true;
|
||||||
|
[SerializeField] private bool destroyOnDeath = false;
|
||||||
|
[SerializeField] private UnityEvent<float> onHealthChanged;
|
||||||
|
[SerializeField] private UnityEvent onDeath;
|
||||||
|
|
||||||
|
public float MaxHealth => maxHealth;
|
||||||
|
public float CurrentHealth { get; private set; }
|
||||||
|
public Team Team => team;
|
||||||
|
public bool IsAlive => CurrentHealth > 0f;
|
||||||
|
|
||||||
|
public event Action<float> NormalizedHealthChanged;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
maxHealth = Mathf.Max(1f, maxHealth);
|
||||||
|
CurrentHealth = maxHealth;
|
||||||
|
onHealthChanged?.Invoke(CurrentHealth / MaxHealth);
|
||||||
|
NormalizedHealthChanged?.Invoke(CurrentHealth / MaxHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyDamage(DamagePayload payload)
|
||||||
|
{
|
||||||
|
if (!IsAlive || payload.Amount <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreFriendlyFire && team != Team.Neutral && payload.SourceTeam == team)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentHealth = Mathf.Max(0f, CurrentHealth - payload.Amount);
|
||||||
|
float normalized = CurrentHealth / MaxHealth;
|
||||||
|
onHealthChanged?.Invoke(normalized);
|
||||||
|
NormalizedHealthChanged?.Invoke(normalized);
|
||||||
|
|
||||||
|
if (CurrentHealth <= 0f)
|
||||||
|
{
|
||||||
|
HandleDeath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Heal(float amount)
|
||||||
|
{
|
||||||
|
if (amount <= 0f || !IsAlive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentHealth = Mathf.Min(MaxHealth, CurrentHealth + amount);
|
||||||
|
float normalized = CurrentHealth / MaxHealth;
|
||||||
|
onHealthChanged?.Invoke(normalized);
|
||||||
|
NormalizedHealthChanged?.Invoke(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDeath()
|
||||||
|
{
|
||||||
|
onDeath?.Invoke();
|
||||||
|
|
||||||
|
if (destroyOnDeath)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
maxHealth = Mathf.Max(1f, maxHealth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ForceSetNormalizedHealth(float normalized)
|
||||||
|
{
|
||||||
|
normalized = Mathf.Clamp01(normalized);
|
||||||
|
bool wasAlive = IsAlive;
|
||||||
|
CurrentHealth = normalized * MaxHealth;
|
||||||
|
onHealthChanged?.Invoke(normalized);
|
||||||
|
NormalizedHealthChanged?.Invoke(normalized);
|
||||||
|
|
||||||
|
if (CurrentHealth <= 0f && wasAlive)
|
||||||
|
{
|
||||||
|
HandleDeath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Combat/Health.cs.meta
Normal file
2
Game/Scripts/Combat/Health.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 920077a2cfc4cde58a4ceeea4b96ebe9
|
||||||
9
Game/Scripts/Combat/IDamageable.cs
Normal file
9
Game/Scripts/Combat/IDamageable.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace MegaKoop.Game.Combat
|
||||||
|
{
|
||||||
|
public interface IDamageable
|
||||||
|
{
|
||||||
|
Team Team { get; }
|
||||||
|
bool IsAlive { get; }
|
||||||
|
void ApplyDamage(DamagePayload payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Combat/IDamageable.cs.meta
Normal file
2
Game/Scripts/Combat/IDamageable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2044a5c09d25d50beb90c486ace32d50
|
||||||
9
Game/Scripts/Combat/Team.cs
Normal file
9
Game/Scripts/Combat/Team.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace MegaKoop.Game.Combat
|
||||||
|
{
|
||||||
|
public enum Team
|
||||||
|
{
|
||||||
|
Neutral = 0,
|
||||||
|
Heroes = 1,
|
||||||
|
Enemies = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Combat/Team.cs.meta
Normal file
2
Game/Scripts/Combat/Team.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 620167bdb0a57e180b2bee989e9ee5d1
|
||||||
8
Game/Scripts/Networking.meta
Normal file
8
Game/Scripts/Networking.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: caf731eed00799f15a7315a669658012
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
10
Game/Scripts/Networking/ICharacterInputSource.cs
Normal file
10
Game/Scripts/Networking/ICharacterInputSource.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
public interface ICharacterInputSource
|
||||||
|
{
|
||||||
|
Vector2 MoveInput { get; }
|
||||||
|
bool JumpPressed { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/ICharacterInputSource.cs.meta
Normal file
2
Game/Scripts/Networking/ICharacterInputSource.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 403a3945388ae5082bf455368a91a800
|
||||||
34
Game/Scripts/Networking/NetworkCharacterInputProxy.cs
Normal file
34
Game/Scripts/Networking/NetworkCharacterInputProxy.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
public class NetworkCharacterInputProxy : MonoBehaviour, ICharacterInputSource
|
||||||
|
{
|
||||||
|
public Vector2 MoveInput { get; private set; }
|
||||||
|
public bool JumpPressed { get; private set; }
|
||||||
|
|
||||||
|
private bool jumpConsumed;
|
||||||
|
|
||||||
|
public void SetInput(Vector2 move, bool jump)
|
||||||
|
{
|
||||||
|
MoveInput = move;
|
||||||
|
if (jump)
|
||||||
|
{
|
||||||
|
JumpPressed = true;
|
||||||
|
jumpConsumed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LateUpdate()
|
||||||
|
{
|
||||||
|
if (JumpPressed && !jumpConsumed)
|
||||||
|
{
|
||||||
|
jumpConsumed = true;
|
||||||
|
}
|
||||||
|
else if (jumpConsumed)
|
||||||
|
{
|
||||||
|
JumpPressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e3118c9c432a7acbd824645749251552
|
||||||
53
Game/Scripts/Networking/NetworkIdentity.cs
Normal file
53
Game/Scripts/Networking/NetworkIdentity.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class NetworkIdentity : MonoBehaviour
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<int, NetworkIdentity> registry = new();
|
||||||
|
private static int nextId = 1;
|
||||||
|
|
||||||
|
[SerializeField] private int networkId;
|
||||||
|
[SerializeField] private bool assignOnAwake = true;
|
||||||
|
|
||||||
|
public int NetworkId => networkId;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (assignOnAwake && networkId == 0)
|
||||||
|
{
|
||||||
|
networkId = nextId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Register();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (registry.TryGetValue(networkId, out NetworkIdentity existing) && existing == this)
|
||||||
|
{
|
||||||
|
registry.Remove(networkId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Register()
|
||||||
|
{
|
||||||
|
if (networkId == 0)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[NetworkIdentity] {name} has no network id and won't be tracked.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registry.TryGetValue(networkId, out NetworkIdentity existing) && existing != this)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[NetworkIdentity] Duplicate network id {networkId} detected. Overwriting reference.");
|
||||||
|
}
|
||||||
|
|
||||||
|
registry[networkId] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGet(int id, out NetworkIdentity identity) => registry.TryGetValue(id, out identity);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/NetworkIdentity.cs.meta
Normal file
2
Game/Scripts/Networking/NetworkIdentity.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1e6f99ca4475ead269829ba672213d5c
|
||||||
222
Game/Scripts/Networking/NetworkMessages.cs
Normal file
222
Game/Scripts/Networking/NetworkMessages.cs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
public enum NetworkMessageType : byte
|
||||||
|
{
|
||||||
|
Heartbeat = 0,
|
||||||
|
LobbyState = 1,
|
||||||
|
PlayerInput = 2,
|
||||||
|
CharacterTransform = 3,
|
||||||
|
WeaponFire = 4,
|
||||||
|
HealthSync = 5,
|
||||||
|
ProjectileSpawn = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct NetworkMessage
|
||||||
|
{
|
||||||
|
public readonly NetworkMessageType Type;
|
||||||
|
public readonly byte[] Payload;
|
||||||
|
public readonly ulong Sender;
|
||||||
|
|
||||||
|
public NetworkMessage(NetworkMessageType type, byte[] payload, ulong sender)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Payload = payload;
|
||||||
|
Sender = sender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct PlayerInputMessage
|
||||||
|
{
|
||||||
|
public readonly int NetworkId;
|
||||||
|
public readonly Vector2 MoveInput;
|
||||||
|
public readonly bool JumpPressed;
|
||||||
|
public readonly Vector2 LookDelta;
|
||||||
|
|
||||||
|
public PlayerInputMessage(int networkId, Vector2 moveInput, bool jumpPressed, Vector2 lookDelta)
|
||||||
|
{
|
||||||
|
NetworkId = networkId;
|
||||||
|
MoveInput = moveInput;
|
||||||
|
JumpPressed = jumpPressed;
|
||||||
|
LookDelta = lookDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(PlayerInputMessage message)
|
||||||
|
{
|
||||||
|
using var writer = new NetworkWriter();
|
||||||
|
writer.Write(message.NetworkId);
|
||||||
|
writer.Write(message.MoveInput.x);
|
||||||
|
writer.Write(message.MoveInput.y);
|
||||||
|
writer.Write(message.JumpPressed);
|
||||||
|
writer.Write(message.LookDelta.x);
|
||||||
|
writer.Write(message.LookDelta.y);
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerInputMessage Deserialize(byte[] buffer)
|
||||||
|
{
|
||||||
|
using var reader = new NetworkReader(buffer);
|
||||||
|
int id = reader.ReadInt();
|
||||||
|
float moveX = reader.ReadFloat();
|
||||||
|
float moveY = reader.ReadFloat();
|
||||||
|
bool jump = reader.ReadBool();
|
||||||
|
float lookX = reader.ReadFloat();
|
||||||
|
float lookY = reader.ReadFloat();
|
||||||
|
return new PlayerInputMessage(id, new Vector2(moveX, moveY), jump, new Vector2(lookX, lookY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct CharacterTransformMessage
|
||||||
|
{
|
||||||
|
public readonly int NetworkId;
|
||||||
|
public readonly Vector3 Position;
|
||||||
|
public readonly Quaternion Rotation;
|
||||||
|
public readonly Vector3 Velocity;
|
||||||
|
|
||||||
|
public CharacterTransformMessage(int networkId, Vector3 position, Quaternion rotation, Vector3 velocity)
|
||||||
|
{
|
||||||
|
NetworkId = networkId;
|
||||||
|
Position = position;
|
||||||
|
Rotation = rotation;
|
||||||
|
Velocity = velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(CharacterTransformMessage message)
|
||||||
|
{
|
||||||
|
using var writer = new NetworkWriter();
|
||||||
|
writer.Write(message.NetworkId);
|
||||||
|
writer.Write(message.Position);
|
||||||
|
writer.Write(message.Rotation);
|
||||||
|
writer.Write(message.Velocity);
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CharacterTransformMessage Deserialize(byte[] buffer)
|
||||||
|
{
|
||||||
|
using var reader = new NetworkReader(buffer);
|
||||||
|
int id = reader.ReadInt();
|
||||||
|
Vector3 position = reader.ReadVector3();
|
||||||
|
Quaternion rotation = reader.ReadQuaternion();
|
||||||
|
Vector3 velocity = reader.ReadVector3();
|
||||||
|
return new CharacterTransformMessage(id, position, rotation, velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct WeaponFireMessage
|
||||||
|
{
|
||||||
|
public readonly int NetworkId;
|
||||||
|
public readonly int WeaponIndex;
|
||||||
|
public readonly Vector3 MuzzlePosition;
|
||||||
|
public readonly Vector3 Direction;
|
||||||
|
public readonly float Timestamp;
|
||||||
|
|
||||||
|
public WeaponFireMessage(int networkId, int weaponIndex, Vector3 muzzlePosition, Vector3 direction, float timestamp)
|
||||||
|
{
|
||||||
|
NetworkId = networkId;
|
||||||
|
WeaponIndex = weaponIndex;
|
||||||
|
MuzzlePosition = muzzlePosition;
|
||||||
|
Direction = direction;
|
||||||
|
Timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(WeaponFireMessage message)
|
||||||
|
{
|
||||||
|
using var writer = new NetworkWriter();
|
||||||
|
writer.Write(message.NetworkId);
|
||||||
|
writer.Write(message.WeaponIndex);
|
||||||
|
writer.Write(message.MuzzlePosition);
|
||||||
|
writer.Write(message.Direction);
|
||||||
|
writer.Write(message.Timestamp);
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WeaponFireMessage Deserialize(byte[] buffer)
|
||||||
|
{
|
||||||
|
using var reader = new NetworkReader(buffer);
|
||||||
|
int networkId = reader.ReadInt();
|
||||||
|
int index = reader.ReadInt();
|
||||||
|
Vector3 muzzle = reader.ReadVector3();
|
||||||
|
Vector3 direction = reader.ReadVector3();
|
||||||
|
float time = reader.ReadFloat();
|
||||||
|
return new WeaponFireMessage(networkId, index, muzzle, direction, time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct HealthSyncMessage
|
||||||
|
{
|
||||||
|
public readonly int NetworkId;
|
||||||
|
public readonly float NormalizedHealth;
|
||||||
|
|
||||||
|
public HealthSyncMessage(int networkId, float normalizedHealth)
|
||||||
|
{
|
||||||
|
NetworkId = networkId;
|
||||||
|
NormalizedHealth = normalizedHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(HealthSyncMessage message)
|
||||||
|
{
|
||||||
|
using var writer = new NetworkWriter();
|
||||||
|
writer.Write(message.NetworkId);
|
||||||
|
writer.Write(message.NormalizedHealth);
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HealthSyncMessage Deserialize(byte[] buffer)
|
||||||
|
{
|
||||||
|
using var reader = new NetworkReader(buffer);
|
||||||
|
int id = reader.ReadInt();
|
||||||
|
float normalized = reader.ReadFloat();
|
||||||
|
return new HealthSyncMessage(id, normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ProjectileSpawnMessage
|
||||||
|
{
|
||||||
|
public readonly int NetworkId;
|
||||||
|
public readonly int WeaponIndex;
|
||||||
|
public readonly Vector3 Position;
|
||||||
|
public readonly Vector3 Direction;
|
||||||
|
public readonly float Speed;
|
||||||
|
public readonly float Life;
|
||||||
|
public readonly float Damage;
|
||||||
|
|
||||||
|
public ProjectileSpawnMessage(int networkId, int weaponIndex, Vector3 position, Vector3 direction, float speed, float life, float damage)
|
||||||
|
{
|
||||||
|
NetworkId = networkId;
|
||||||
|
WeaponIndex = weaponIndex;
|
||||||
|
Position = position;
|
||||||
|
Direction = direction;
|
||||||
|
Speed = speed;
|
||||||
|
Life = life;
|
||||||
|
Damage = damage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(ProjectileSpawnMessage message)
|
||||||
|
{
|
||||||
|
using var writer = new NetworkWriter();
|
||||||
|
writer.Write(message.NetworkId);
|
||||||
|
writer.Write(message.WeaponIndex);
|
||||||
|
writer.Write(message.Position);
|
||||||
|
writer.Write(message.Direction);
|
||||||
|
writer.Write(message.Speed);
|
||||||
|
writer.Write(message.Life);
|
||||||
|
writer.Write(message.Damage);
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProjectileSpawnMessage Deserialize(byte[] buffer)
|
||||||
|
{
|
||||||
|
using var reader = new NetworkReader(buffer);
|
||||||
|
int networkId = reader.ReadInt();
|
||||||
|
int index = reader.ReadInt();
|
||||||
|
Vector3 position = reader.ReadVector3();
|
||||||
|
Vector3 direction = reader.ReadVector3();
|
||||||
|
float speed = reader.ReadFloat();
|
||||||
|
float life = reader.ReadFloat();
|
||||||
|
float damage = reader.ReadFloat();
|
||||||
|
return new ProjectileSpawnMessage(networkId, index, position, direction, speed, life, damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/NetworkMessages.cs.meta
Normal file
2
Game/Scripts/Networking/NetworkMessages.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 28ade96a26da5d9f693df8a5a80b0970
|
||||||
116
Game/Scripts/Networking/NetworkSerialization.cs
Normal file
116
Game/Scripts/Networking/NetworkSerialization.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
public sealed class NetworkWriter : IDisposable
|
||||||
|
{
|
||||||
|
private readonly MemoryStream stream;
|
||||||
|
private readonly BinaryWriter writer;
|
||||||
|
|
||||||
|
public NetworkWriter(int capacity = 128)
|
||||||
|
{
|
||||||
|
stream = new MemoryStream(capacity);
|
||||||
|
writer = new BinaryWriter(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(byte value) => writer.Write(value);
|
||||||
|
public void Write(int value) => writer.Write(value);
|
||||||
|
public void Write(uint value) => writer.Write(value);
|
||||||
|
public void Write(short value) => writer.Write(value);
|
||||||
|
public void Write(ushort value) => writer.Write(value);
|
||||||
|
public void Write(long value) => writer.Write(value);
|
||||||
|
public void Write(ulong value) => writer.Write(value);
|
||||||
|
public void Write(float value) => writer.Write(value);
|
||||||
|
public void Write(bool value) => writer.Write(value);
|
||||||
|
|
||||||
|
public void Write(Vector3 value)
|
||||||
|
{
|
||||||
|
writer.Write(value.x);
|
||||||
|
writer.Write(value.y);
|
||||||
|
writer.Write(value.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(Quaternion value)
|
||||||
|
{
|
||||||
|
writer.Write(value.x);
|
||||||
|
writer.Write(value.y);
|
||||||
|
writer.Write(value.z);
|
||||||
|
writer.Write(value.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(string value) => writer.Write(value ?? string.Empty);
|
||||||
|
|
||||||
|
public byte[] ToArray()
|
||||||
|
{
|
||||||
|
writer.Flush();
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
writer?.Dispose();
|
||||||
|
stream?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NetworkReader : IDisposable
|
||||||
|
{
|
||||||
|
private readonly MemoryStream stream;
|
||||||
|
private readonly BinaryReader reader;
|
||||||
|
|
||||||
|
public NetworkReader(byte[] buffer)
|
||||||
|
{
|
||||||
|
stream = new MemoryStream(buffer ?? Array.Empty<byte>());
|
||||||
|
reader = new BinaryReader(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte ReadByte() => reader.ReadByte();
|
||||||
|
public bool ReadBool() => reader.ReadBoolean();
|
||||||
|
public int ReadInt() => reader.ReadInt32();
|
||||||
|
public uint ReadUInt() => reader.ReadUInt32();
|
||||||
|
public short ReadShort() => reader.ReadInt16();
|
||||||
|
public ushort ReadUShort() => reader.ReadUInt16();
|
||||||
|
public long ReadLong() => reader.ReadInt64();
|
||||||
|
public ulong ReadULong() => reader.ReadUInt64();
|
||||||
|
public float ReadFloat() => reader.ReadSingle();
|
||||||
|
|
||||||
|
public Vector3 ReadVector3()
|
||||||
|
{
|
||||||
|
float x = reader.ReadSingle();
|
||||||
|
float y = reader.ReadSingle();
|
||||||
|
float z = reader.ReadSingle();
|
||||||
|
return new Vector3(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Quaternion ReadQuaternion()
|
||||||
|
{
|
||||||
|
float x = reader.ReadSingle();
|
||||||
|
float y = reader.ReadSingle();
|
||||||
|
float z = reader.ReadSingle();
|
||||||
|
float w = reader.ReadSingle();
|
||||||
|
return new Quaternion(x, y, z, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ReadString() => reader.ReadString();
|
||||||
|
|
||||||
|
public bool TryReadBytes(int length, out byte[] bytes)
|
||||||
|
{
|
||||||
|
if (length <= 0 || reader.BaseStream.Position + length > reader.BaseStream.Length)
|
||||||
|
{
|
||||||
|
bytes = Array.Empty<byte>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes = reader.ReadBytes(length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
reader?.Dispose();
|
||||||
|
stream?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/NetworkSerialization.cs.meta
Normal file
2
Game/Scripts/Networking/NetworkSerialization.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0303a875e193063078b4557274bb3c5a
|
||||||
101
Game/Scripts/Networking/SteamBootstrap.cs
Normal file
101
Game/Scripts/Networking/SteamBootstrap.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using Steamworks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles SteamAPI initialization and callback pumping. Place once in the initial scene.
|
||||||
|
/// </summary>
|
||||||
|
[DefaultExecutionOrder(-1000)]
|
||||||
|
public class SteamBootstrap : MonoBehaviour
|
||||||
|
{
|
||||||
|
private static bool isInitialized;
|
||||||
|
private static SteamBootstrap instance;
|
||||||
|
|
||||||
|
public static bool IsInitialized => isInitialized;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (instance != null)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance = this;
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
|
|
||||||
|
TryInitializeSteam();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryInitializeSteam()
|
||||||
|
{
|
||||||
|
if (isInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Packsize.Test())
|
||||||
|
{
|
||||||
|
Debug.LogError("[SteamBootstrap] Packsize Test returned false. Wrong Steamworks binaries for this platform?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DllCheck.Test())
|
||||||
|
{
|
||||||
|
Debug.LogError("[SteamBootstrap] DllCheck Test returned false. Missing Steamworks dependencies.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isInitialized = SteamAPI.Init();
|
||||||
|
}
|
||||||
|
catch (System.DllNotFoundException e)
|
||||||
|
{
|
||||||
|
Debug.LogError("[SteamBootstrap] Steamworks native binaries not found: " + e.Message);
|
||||||
|
isInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInitialized)
|
||||||
|
{
|
||||||
|
Debug.LogError("[SteamBootstrap] Failed to initialize Steam API.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Log("[SteamBootstrap] Steam API initialized for user " + SteamFriends.GetPersonaName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!isInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SteamAPI.RunCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (instance == this)
|
||||||
|
{
|
||||||
|
ShutdownSteam();
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShutdownSteam()
|
||||||
|
{
|
||||||
|
if (!isInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SteamAPI.Shutdown();
|
||||||
|
isInitialized = false;
|
||||||
|
Debug.Log("[SteamBootstrap] Steam API shutdown.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/SteamBootstrap.cs.meta
Normal file
2
Game/Scripts/Networking/SteamBootstrap.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f741ec3a1d16461a98d02ec211293449
|
||||||
261
Game/Scripts/Networking/SteamCharacterNetworkBridge.cs
Normal file
261
Game/Scripts/Networking/SteamCharacterNetworkBridge.cs
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
using Steamworks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class SteamCharacterNetworkBridge : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[SerializeField] private ThirdPersonCharacterController characterController;
|
||||||
|
[SerializeField] private NetworkIdentity identity;
|
||||||
|
[SerializeField] private Transform rootTransform;
|
||||||
|
[SerializeField] private NetworkCharacterInputProxy networkInputProxy;
|
||||||
|
|
||||||
|
[Header("Settings")]
|
||||||
|
[SerializeField] private float transformBroadcastInterval = 0.05f;
|
||||||
|
[SerializeField] private float remoteLerpSpeed = 12f;
|
||||||
|
[SerializeField] private ulong ownerSteamId;
|
||||||
|
[SerializeField] private bool autoAssignOwnerToLocalPlayer = true;
|
||||||
|
|
||||||
|
private SteamCoopNetworkManager networkManager;
|
||||||
|
private float broadcastTimer;
|
||||||
|
private bool isRegistered;
|
||||||
|
private bool isLocalPlayer;
|
||||||
|
private bool isAuthority;
|
||||||
|
|
||||||
|
private Vector3 remoteTargetPosition;
|
||||||
|
private Quaternion remoteTargetRotation;
|
||||||
|
private Vector3 remoteTargetVelocity;
|
||||||
|
private bool haveRemoteState;
|
||||||
|
private bool localOverrideSet;
|
||||||
|
|
||||||
|
public void AssignOwner(ulong steamId, bool localPlayer)
|
||||||
|
{
|
||||||
|
ownerSteamId = steamId;
|
||||||
|
isLocalPlayer = localPlayer;
|
||||||
|
localOverrideSet = true;
|
||||||
|
UpdateAuthority();
|
||||||
|
ConfigureController();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLocalPlayer => isLocalPlayer;
|
||||||
|
public bool IsAuthority => isAuthority;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (characterController == null)
|
||||||
|
{
|
||||||
|
characterController = GetComponent<ThirdPersonCharacterController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity == null)
|
||||||
|
{
|
||||||
|
identity = GetComponent<NetworkIdentity>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootTransform == null)
|
||||||
|
{
|
||||||
|
rootTransform = transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkInputProxy == null)
|
||||||
|
{
|
||||||
|
networkInputProxy = GetComponent<NetworkCharacterInputProxy>();
|
||||||
|
if (networkInputProxy == null)
|
||||||
|
{
|
||||||
|
networkInputProxy = gameObject.AddComponent<NetworkCharacterInputProxy>();
|
||||||
|
networkInputProxy.hideFlags = HideFlags.HideInInspector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteTargetPosition = rootTransform.position;
|
||||||
|
remoteTargetRotation = rootTransform.rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
networkManager = SteamCoopNetworkManager.Instance;
|
||||||
|
UpdateAuthority();
|
||||||
|
ConfigureController();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
networkManager = SteamCoopNetworkManager.Instance;
|
||||||
|
RegisterHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
UnregisterHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (networkManager == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthority)
|
||||||
|
{
|
||||||
|
broadcastTimer -= Time.deltaTime;
|
||||||
|
if (broadcastTimer <= 0f)
|
||||||
|
{
|
||||||
|
BroadcastTransform();
|
||||||
|
broadcastTimer = transformBroadcastInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (haveRemoteState)
|
||||||
|
{
|
||||||
|
rootTransform.position = Vector3.Lerp(rootTransform.position, remoteTargetPosition, remoteLerpSpeed * Time.deltaTime);
|
||||||
|
rootTransform.rotation = Quaternion.Slerp(rootTransform.rotation, remoteTargetRotation, remoteLerpSpeed * Time.deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterHandlers()
|
||||||
|
{
|
||||||
|
if (networkManager == null || isRegistered)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkManager.RegisterHandler(NetworkMessageType.PlayerInput, HandlePlayerInputMessage);
|
||||||
|
networkManager.RegisterHandler(NetworkMessageType.CharacterTransform, HandleCharacterTransformMessage);
|
||||||
|
isRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnregisterHandlers()
|
||||||
|
{
|
||||||
|
if (networkManager == null || !isRegistered)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkManager.UnregisterHandler(NetworkMessageType.PlayerInput, HandlePlayerInputMessage);
|
||||||
|
networkManager.UnregisterHandler(NetworkMessageType.CharacterTransform, HandleCharacterTransformMessage);
|
||||||
|
isRegistered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAuthority()
|
||||||
|
{
|
||||||
|
if (networkManager == null)
|
||||||
|
{
|
||||||
|
networkManager = SteamCoopNetworkManager.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isHost = networkManager != null && networkManager.IsHost;
|
||||||
|
ulong localSteamId = SteamBootstrap.IsInitialized ? SteamUser.GetSteamID().m_SteamID : 0UL;
|
||||||
|
|
||||||
|
if (!localOverrideSet && autoAssignOwnerToLocalPlayer && ownerSteamId == 0 && localSteamId != 0)
|
||||||
|
{
|
||||||
|
ownerSteamId = localSteamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localOverrideSet)
|
||||||
|
{
|
||||||
|
isLocalPlayer = ownerSteamId != 0 && ownerSteamId == localSteamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthority = isHost; // Host drives authoritative simulation.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureController()
|
||||||
|
{
|
||||||
|
if (characterController == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthority)
|
||||||
|
{
|
||||||
|
characterController.enabled = true;
|
||||||
|
characterController.SetInputSource(isLocalPlayer ? null : networkInputProxy);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
characterController.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BroadcastTransform()
|
||||||
|
{
|
||||||
|
if (identity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var unityController = GetComponent<CharacterController>();
|
||||||
|
Vector3 velocity = unityController != null ? unityController.velocity : Vector3.zero;
|
||||||
|
var message = new CharacterTransformMessage(identity.NetworkId, rootTransform.position, rootTransform.rotation, velocity);
|
||||||
|
byte[] payload = CharacterTransformMessage.Serialize(message);
|
||||||
|
networkManager.SendToAll(NetworkMessageType.CharacterTransform, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePlayerInputMessage(NetworkMessage message)
|
||||||
|
{
|
||||||
|
if (!isAuthority)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerInputMessage inputMessage = PlayerInputMessage.Deserialize(message.Payload);
|
||||||
|
if (inputMessage.NetworkId != identity.NetworkId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
networkInputProxy.SetInput(inputMessage.MoveInput, inputMessage.JumpPressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCharacterTransformMessage(NetworkMessage message)
|
||||||
|
{
|
||||||
|
if (isAuthority)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CharacterTransformMessage transformMessage = CharacterTransformMessage.Deserialize(message.Payload);
|
||||||
|
if (transformMessage.NetworkId != identity.NetworkId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteTargetPosition = transformMessage.Position;
|
||||||
|
remoteTargetRotation = transformMessage.Rotation;
|
||||||
|
remoteTargetVelocity = transformMessage.Velocity;
|
||||||
|
haveRemoteState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendLocalInput(Vector2 moveInput, bool jump)
|
||||||
|
{
|
||||||
|
if (networkManager == null || identity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = new PlayerInputMessage(identity.NetworkId, moveInput, jump, Vector2.zero);
|
||||||
|
byte[] payload = PlayerInputMessage.Serialize(message);
|
||||||
|
if (!networkManager.IsConnected || networkManager.IsHost)
|
||||||
|
{
|
||||||
|
// If we are host, feed directly.
|
||||||
|
HandlePlayerInputMessage(new NetworkMessage(NetworkMessageType.PlayerInput, payload, SteamUser.GetSteamID().m_SteamID));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CSteamID lobby = networkManager.ActiveLobby;
|
||||||
|
CSteamID lobbyOwner = lobby != CSteamID.Nil ? SteamMatchmaking.GetLobbyOwner(lobby) : CSteamID.Nil;
|
||||||
|
if (lobbyOwner == CSteamID.Nil)
|
||||||
|
{
|
||||||
|
lobbyOwner = SteamUser.GetSteamID();
|
||||||
|
}
|
||||||
|
|
||||||
|
networkManager.SendToPlayer(lobbyOwner, NetworkMessageType.PlayerInput, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a010df74dca8515c3af51a9fc598af17
|
||||||
165
Game/Scripts/Networking/SteamCoopNetworkManager.cs
Normal file
165
Game/Scripts/Networking/SteamCoopNetworkManager.cs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Steamworks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// High level orchestrator for Steam lobby + P2P messaging. Keeps track of handlers per message type.
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class SteamCoopNetworkManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static SteamCoopNetworkManager Instance { get; private set; }
|
||||||
|
|
||||||
|
[SerializeField] private SteamLobbyManager lobbyManager;
|
||||||
|
[SerializeField] private SteamP2PTransport p2pTransport;
|
||||||
|
|
||||||
|
private readonly Dictionary<NetworkMessageType, Action<NetworkMessage>> handlers = new();
|
||||||
|
private bool isHost;
|
||||||
|
private bool isConnected;
|
||||||
|
|
||||||
|
public bool IsHost => isHost;
|
||||||
|
public bool IsConnected => isConnected;
|
||||||
|
public CSteamID ActiveLobby => lobbyManager != null ? lobbyManager.GetActiveLobby() : CSteamID.Nil;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instance = this;
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
|
|
||||||
|
if (lobbyManager == null)
|
||||||
|
{
|
||||||
|
lobbyManager = GetComponentInChildren<SteamLobbyManager>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p2pTransport == null)
|
||||||
|
{
|
||||||
|
p2pTransport = GetComponentInChildren<SteamP2PTransport>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lobbyManager != null)
|
||||||
|
{
|
||||||
|
lobbyManager.LobbyCreated += HandleLobbyCreated;
|
||||||
|
lobbyManager.LobbyJoined += HandleLobbyJoined;
|
||||||
|
lobbyManager.LobbyMemberJoined += HandleLobbyMemberJoined;
|
||||||
|
lobbyManager.LobbyMemberLeft += HandleLobbyMemberLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p2pTransport != null)
|
||||||
|
{
|
||||||
|
p2pTransport.MessageReceived += DispatchMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
if (Instance == this)
|
||||||
|
{
|
||||||
|
Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lobbyManager != null)
|
||||||
|
{
|
||||||
|
lobbyManager.LobbyCreated -= HandleLobbyCreated;
|
||||||
|
lobbyManager.LobbyJoined -= HandleLobbyJoined;
|
||||||
|
lobbyManager.LobbyMemberJoined -= HandleLobbyMemberJoined;
|
||||||
|
lobbyManager.LobbyMemberLeft -= HandleLobbyMemberLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p2pTransport != null)
|
||||||
|
{
|
||||||
|
p2pTransport.MessageReceived -= DispatchMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterHandler(NetworkMessageType type, Action<NetworkMessage> handler)
|
||||||
|
{
|
||||||
|
if (handlers.TryGetValue(type, out Action<NetworkMessage> existing))
|
||||||
|
{
|
||||||
|
existing += handler;
|
||||||
|
handlers[type] = existing;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
handlers[type] = handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnregisterHandler(NetworkMessageType type, Action<NetworkMessage> handler)
|
||||||
|
{
|
||||||
|
if (!handlers.TryGetValue(type, out Action<NetworkMessage> existing))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
existing -= handler;
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
handlers.Remove(type);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
handlers[type] = existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendToAll(NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
|
||||||
|
{
|
||||||
|
p2pTransport?.Broadcast(type, payload, sendType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendToPlayer(CSteamID target, NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
|
||||||
|
{
|
||||||
|
p2pTransport?.Send(target, type, payload, sendType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DispatchMessage(NetworkMessage message)
|
||||||
|
{
|
||||||
|
if (handlers.TryGetValue(message.Type, out Action<NetworkMessage> handler))
|
||||||
|
{
|
||||||
|
handler?.Invoke(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLobbyCreated(CSteamID lobbyId)
|
||||||
|
{
|
||||||
|
isHost = true;
|
||||||
|
isConnected = true;
|
||||||
|
p2pTransport?.SetActiveLobby(lobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLobbyJoined(CSteamID lobbyId)
|
||||||
|
{
|
||||||
|
isConnected = true;
|
||||||
|
p2pTransport?.SetActiveLobby(lobbyId);
|
||||||
|
|
||||||
|
string ownerId = SteamMatchmaking.GetLobbyData(lobbyId, "owner");
|
||||||
|
if (!string.IsNullOrEmpty(ownerId) && ulong.TryParse(ownerId, out ulong ownerSteamId))
|
||||||
|
{
|
||||||
|
isHost = ownerSteamId == SteamUser.GetSteamID().m_SteamID;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isHost = SteamMatchmaking.GetLobbyOwner(lobbyId) == SteamUser.GetSteamID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLobbyMemberJoined(CSteamID member)
|
||||||
|
{
|
||||||
|
Debug.Log("[SteamCoopNetworkManager] Member joined: " + member);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLobbyMemberLeft(CSteamID member)
|
||||||
|
{
|
||||||
|
Debug.Log("[SteamCoopNetworkManager] Member left: " + member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/SteamCoopNetworkManager.cs.meta
Normal file
2
Game/Scripts/Networking/SteamCoopNetworkManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 83320aed3c99a87b692932447a34631e
|
||||||
89
Game/Scripts/Networking/SteamHealthNetworkBridge.cs
Normal file
89
Game/Scripts/Networking/SteamHealthNetworkBridge.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class SteamHealthNetworkBridge : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private Combat.Health health;
|
||||||
|
[SerializeField] private NetworkIdentity identity;
|
||||||
|
|
||||||
|
private SteamCoopNetworkManager networkManager;
|
||||||
|
private bool isRegistered;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (health == null)
|
||||||
|
{
|
||||||
|
health = GetComponent<Combat.Health>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity == null)
|
||||||
|
{
|
||||||
|
identity = GetComponent<NetworkIdentity>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
networkManager = SteamCoopNetworkManager.Instance;
|
||||||
|
if (networkManager != null)
|
||||||
|
{
|
||||||
|
networkManager.RegisterHandler(NetworkMessageType.HealthSync, HandleHealthSync);
|
||||||
|
isRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (health != null)
|
||||||
|
{
|
||||||
|
health.NormalizedHealthChanged += OnHealthChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (isRegistered && networkManager != null)
|
||||||
|
{
|
||||||
|
networkManager.UnregisterHandler(NetworkMessageType.HealthSync, HandleHealthSync);
|
||||||
|
isRegistered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (health != null)
|
||||||
|
{
|
||||||
|
health.NormalizedHealthChanged -= OnHealthChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAuthority()
|
||||||
|
{
|
||||||
|
return networkManager == null || networkManager.IsHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHealthChanged(float normalized)
|
||||||
|
{
|
||||||
|
if (!IsAuthority() || identity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = new HealthSyncMessage(identity.NetworkId, normalized);
|
||||||
|
byte[] payload = HealthSyncMessage.Serialize(message);
|
||||||
|
networkManager.SendToAll(NetworkMessageType.HealthSync, payload, Steamworks.EP2PSend.k_EP2PSendReliable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleHealthSync(NetworkMessage message)
|
||||||
|
{
|
||||||
|
if (IsAuthority() || identity == null || health == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HealthSyncMessage syncMessage = HealthSyncMessage.Deserialize(message.Payload);
|
||||||
|
if (syncMessage.NetworkId != identity.NetworkId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
health.ForceSetNormalizedHealth(syncMessage.NormalizedHealth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/SteamHealthNetworkBridge.cs.meta
Normal file
2
Game/Scripts/Networking/SteamHealthNetworkBridge.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a55005e32d7c7fdd89d4c15872d0466d
|
||||||
116
Game/Scripts/Networking/SteamLobbyManager.cs
Normal file
116
Game/Scripts/Networking/SteamLobbyManager.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using Steamworks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps Steam lobby creation and join logic for cooperative sessions.
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class SteamLobbyManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private int maxPlayers = 4;
|
||||||
|
|
||||||
|
public event Action<LobbyDataUpdate_t> LobbyDataUpdated;
|
||||||
|
public event Action<CSteamID> LobbyCreated;
|
||||||
|
public event Action<CSteamID> LobbyJoined;
|
||||||
|
public event Action<CSteamID> LobbyMemberJoined;
|
||||||
|
public event Action<CSteamID> LobbyMemberLeft;
|
||||||
|
|
||||||
|
private Callback<LobbyCreated_t> lobbyCreatedCallback;
|
||||||
|
private Callback<LobbyEnter_t> lobbyEnterCallback;
|
||||||
|
private Callback<LobbyDataUpdate_t> lobbyDataUpdateCallback;
|
||||||
|
private Callback<LobbyChatUpdate_t> lobbyChatUpdateCallback;
|
||||||
|
|
||||||
|
private CSteamID activeLobbyId;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
lobbyCreatedCallback = Callback<LobbyCreated_t>.Create(OnLobbyCreated);
|
||||||
|
lobbyEnterCallback = Callback<LobbyEnter_t>.Create(OnLobbyEntered);
|
||||||
|
lobbyDataUpdateCallback = Callback<LobbyDataUpdate_t>.Create(OnLobbyDataUpdated);
|
||||||
|
lobbyChatUpdateCallback = Callback<LobbyChatUpdate_t>.Create(OnLobbyChatUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
lobbyCreatedCallback?.Dispose();
|
||||||
|
lobbyEnterCallback?.Dispose();
|
||||||
|
lobbyDataUpdateCallback?.Dispose();
|
||||||
|
lobbyChatUpdateCallback?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HostLobby(string lobbyName)
|
||||||
|
{
|
||||||
|
if (!SteamBootstrap.IsInitialized || !SteamAPI.IsSteamRunning())
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[SteamLobbyManager] Steam is not initialized; cannot create lobby.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypePublic, Mathf.Max(2, maxPlayers));
|
||||||
|
pendingLobbyName = lobbyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void JoinLobby(CSteamID lobbyId)
|
||||||
|
{
|
||||||
|
if (!SteamBootstrap.IsInitialized || !SteamAPI.IsSteamRunning())
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[SteamLobbyManager] Steam not running; cannot join lobby.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SteamMatchmaking.JoinLobby(lobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CSteamID GetActiveLobby() => activeLobbyId;
|
||||||
|
|
||||||
|
private string pendingLobbyName;
|
||||||
|
|
||||||
|
private void OnLobbyCreated(LobbyCreated_t callback)
|
||||||
|
{
|
||||||
|
if (callback.m_eResult != EResult.k_EResultOK)
|
||||||
|
{
|
||||||
|
Debug.LogError("[SteamLobbyManager] Lobby creation failed: " + callback.m_eResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeLobbyId = new CSteamID(callback.m_ulSteamIDLobby);
|
||||||
|
SteamMatchmaking.SetLobbyData(activeLobbyId, "name", string.IsNullOrEmpty(pendingLobbyName) ? "MegaKoop Lobby" : pendingLobbyName);
|
||||||
|
SteamMatchmaking.SetLobbyData(activeLobbyId, "owner", SteamUser.GetSteamID().ToString());
|
||||||
|
LobbyCreated?.Invoke(activeLobbyId);
|
||||||
|
Debug.Log("[SteamLobbyManager] Lobby created " + activeLobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLobbyEntered(LobbyEnter_t callback)
|
||||||
|
{
|
||||||
|
activeLobbyId = new CSteamID(callback.m_ulSteamIDLobby);
|
||||||
|
LobbyJoined?.Invoke(activeLobbyId);
|
||||||
|
Debug.Log("[SteamLobbyManager] Entered lobby " + activeLobbyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLobbyDataUpdated(LobbyDataUpdate_t callback)
|
||||||
|
{
|
||||||
|
LobbyDataUpdated?.Invoke(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLobbyChatUpdate(LobbyChatUpdate_t callback)
|
||||||
|
{
|
||||||
|
CSteamID lobby = new CSteamID(callback.m_ulSteamIDLobby);
|
||||||
|
CSteamID changedUser = new CSteamID(callback.m_ulSteamIDUserChanged);
|
||||||
|
|
||||||
|
EChatMemberStateChange stateChange = (EChatMemberStateChange)callback.m_rgfChatMemberStateChange;
|
||||||
|
|
||||||
|
if ((stateChange & EChatMemberStateChange.k_EChatMemberStateChangeEntered) != 0)
|
||||||
|
{
|
||||||
|
LobbyMemberJoined?.Invoke(changedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((stateChange & (EChatMemberStateChange.k_EChatMemberStateChangeLeft | EChatMemberStateChange.k_EChatMemberStateChangeDisconnected | EChatMemberStateChange.k_EChatMemberStateChangeKicked | EChatMemberStateChange.k_EChatMemberStateChangeBanned)) != 0)
|
||||||
|
{
|
||||||
|
LobbyMemberLeft?.Invoke(changedUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/SteamLobbyManager.cs.meta
Normal file
2
Game/Scripts/Networking/SteamLobbyManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 468b0a865fb2a0b0181db1c60d4e0ea9
|
||||||
44
Game/Scripts/Networking/SteamLocalInputSender.cs
Normal file
44
Game/Scripts/Networking/SteamLocalInputSender.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class SteamLocalInputSender : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private SteamCharacterNetworkBridge characterNetwork;
|
||||||
|
[SerializeField] private float sendInterval = 0.05f;
|
||||||
|
|
||||||
|
private float sendTimer;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (characterNetwork == null)
|
||||||
|
{
|
||||||
|
characterNetwork = GetComponent<SteamCharacterNetworkBridge>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (characterNetwork == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!characterNetwork.IsLocalPlayer || characterNetwork.IsAuthority)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTimer -= Time.deltaTime;
|
||||||
|
Vector2 moveInput = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
|
||||||
|
bool jumpPressed = Input.GetButtonDown("Jump");
|
||||||
|
|
||||||
|
if (sendTimer <= 0f || jumpPressed)
|
||||||
|
{
|
||||||
|
characterNetwork.SendLocalInput(moveInput, jumpPressed);
|
||||||
|
sendTimer = sendInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/SteamLocalInputSender.cs.meta
Normal file
2
Game/Scripts/Networking/SteamLocalInputSender.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 07c0ee7fbf4c1edaf9992cf139839928
|
||||||
114
Game/Scripts/Networking/SteamP2PTransport.cs
Normal file
114
Game/Scripts/Networking/SteamP2PTransport.cs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System;
|
||||||
|
using Steamworks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Low level Steam P2P messaging helper. Prepends a byte message type header for easy routing.
|
||||||
|
/// </summary>
|
||||||
|
public class SteamP2PTransport : MonoBehaviour
|
||||||
|
{
|
||||||
|
public event Action<NetworkMessage> MessageReceived;
|
||||||
|
|
||||||
|
[SerializeField] private EP2PSend defaultSendType = EP2PSend.k_EP2PSendReliable;
|
||||||
|
[SerializeField] private int listenChannel = 0;
|
||||||
|
|
||||||
|
private byte[] receiveBuffer = new byte[8192];
|
||||||
|
private CSteamID activeLobby = CSteamID.Nil;
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!SteamBootstrap.IsInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PumpIncomingPackets();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetActiveLobby(CSteamID lobbyId)
|
||||||
|
{
|
||||||
|
activeLobby = lobbyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(CSteamID recipient, NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
|
||||||
|
{
|
||||||
|
if (!SteamBootstrap.IsInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int payloadLength = payload != null ? payload.Length : 0;
|
||||||
|
byte[] packet = new byte[payloadLength + 1];
|
||||||
|
packet[0] = (byte)type;
|
||||||
|
if (payloadLength > 0)
|
||||||
|
{
|
||||||
|
Buffer.BlockCopy(payload, 0, packet, 1, payloadLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sent = SteamNetworking.SendP2PPacket(recipient, packet, (uint)packet.Length, sendType, listenChannel);
|
||||||
|
if (!sent)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[SteamP2PTransport] Failed to send packet to " + recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Broadcast(NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
|
||||||
|
{
|
||||||
|
if (activeLobby == CSteamID.Nil)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int members = SteamMatchmaking.GetNumLobbyMembers(activeLobby);
|
||||||
|
CSteamID self = SteamUser.GetSteamID();
|
||||||
|
|
||||||
|
for (int i = 0; i < members; i++)
|
||||||
|
{
|
||||||
|
CSteamID member = SteamMatchmaking.GetLobbyMemberByIndex(activeLobby, i);
|
||||||
|
if (member == self)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Send(member, type, payload, sendType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PumpIncomingPackets()
|
||||||
|
{
|
||||||
|
while (SteamNetworking.IsP2PPacketAvailable(out uint packetSize, listenChannel))
|
||||||
|
{
|
||||||
|
if (packetSize == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packetSize > receiveBuffer.Length)
|
||||||
|
{
|
||||||
|
receiveBuffer = new byte[(int)packetSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SteamNetworking.ReadP2PPacket(receiveBuffer, (uint)receiveBuffer.Length, out uint bytesRead, out CSteamID remote, listenChannel))
|
||||||
|
{
|
||||||
|
SteamNetworking.AcceptP2PSessionWithUser(remote);
|
||||||
|
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkMessageType type = (NetworkMessageType)receiveBuffer[0];
|
||||||
|
byte[] payload = new byte[Mathf.Max(0, (int)bytesRead - 1)];
|
||||||
|
if (bytesRead > 1)
|
||||||
|
{
|
||||||
|
Buffer.BlockCopy(receiveBuffer, 1, payload, 0, (int)bytesRead - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageReceived?.Invoke(new NetworkMessage(type, payload, remote.m_SteamID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/SteamP2PTransport.cs.meta
Normal file
2
Game/Scripts/Networking/SteamP2PTransport.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c3923950963f6f6a98a81fda48267e6e
|
||||||
128
Game/Scripts/Networking/SteamWeaponNetworkBridge.cs
Normal file
128
Game/Scripts/Networking/SteamWeaponNetworkBridge.cs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
using MegaKoop.Game.WeaponSystem;
|
||||||
|
using Steamworks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.Networking
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class SteamWeaponNetworkBridge : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private WeaponController weaponController;
|
||||||
|
[SerializeField] private NetworkIdentity identity;
|
||||||
|
[SerializeField] private bool disableLocalFiringWhenClient = true;
|
||||||
|
|
||||||
|
private SteamCoopNetworkManager networkManager;
|
||||||
|
private bool isRegistered;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
networkManager = SteamCoopNetworkManager.Instance;
|
||||||
|
|
||||||
|
if (weaponController == null)
|
||||||
|
{
|
||||||
|
weaponController = GetComponent<WeaponController>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity == null)
|
||||||
|
{
|
||||||
|
identity = GetComponent<NetworkIdentity>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
if (weaponController == null || identity == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[SteamWeaponNetworkBridge] Missing references.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkManager == null)
|
||||||
|
{
|
||||||
|
networkManager = SteamCoopNetworkManager.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkManager != null)
|
||||||
|
{
|
||||||
|
networkManager.RegisterHandler(NetworkMessageType.ProjectileSpawn, HandleProjectileSpawnMessage);
|
||||||
|
isRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
weaponController.ProjectileSpawned += OnProjectileSpawned;
|
||||||
|
|
||||||
|
if (!IsAuthoritative() && disableLocalFiringWhenClient)
|
||||||
|
{
|
||||||
|
weaponController.enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (weaponController != null)
|
||||||
|
{
|
||||||
|
weaponController.ProjectileSpawned -= OnProjectileSpawned;
|
||||||
|
|
||||||
|
if (!IsAuthoritative() && disableLocalFiringWhenClient)
|
||||||
|
{
|
||||||
|
weaponController.enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRegistered && networkManager != null)
|
||||||
|
{
|
||||||
|
networkManager.UnregisterHandler(NetworkMessageType.ProjectileSpawn, HandleProjectileSpawnMessage);
|
||||||
|
isRegistered = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAuthoritative()
|
||||||
|
{
|
||||||
|
if (networkManager == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkManager.IsHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnProjectileSpawned(WeaponController.ProjectileSpawnEvent spawnEvent)
|
||||||
|
{
|
||||||
|
if (!IsAuthoritative())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkManager == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = new ProjectileSpawnMessage(identity.NetworkId, spawnEvent.WeaponIndex, spawnEvent.Position, spawnEvent.Direction, spawnEvent.Speed, spawnEvent.Lifetime, spawnEvent.Damage);
|
||||||
|
byte[] payload = ProjectileSpawnMessage.Serialize(message);
|
||||||
|
networkManager.SendToAll(NetworkMessageType.ProjectileSpawn, payload, EP2PSend.k_EP2PSendUnreliableNoDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleProjectileSpawnMessage(NetworkMessage message)
|
||||||
|
{
|
||||||
|
if (message.Sender == SteamUser.GetSteamID().m_SteamID)
|
||||||
|
{
|
||||||
|
// Ignore our own echo.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectileSpawnMessage spawnMessage = ProjectileSpawnMessage.Deserialize(message.Payload);
|
||||||
|
if (spawnMessage.NetworkId != identity.NetworkId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAuthoritative())
|
||||||
|
{
|
||||||
|
// Host already spawned real projectile, don't duplicate.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
weaponController.SpawnNetworkProjectile(spawnMessage.WeaponIndex, spawnMessage.Position, spawnMessage.Direction, spawnMessage.Speed, spawnMessage.Life, spawnMessage.Damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/Networking/SteamWeaponNetworkBridge.cs.meta
Normal file
2
Game/Scripts/Networking/SteamWeaponNetworkBridge.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dad61aa7b24bddb6b9add5a461263779
|
||||||
204
Game/Scripts/ThirdPersonCamera.cs
Normal file
204
Game/Scripts/ThirdPersonCamera.cs
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game
|
||||||
|
{
|
||||||
|
public class ThirdPersonCamera : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Target")]
|
||||||
|
[SerializeField] private Transform target;
|
||||||
|
[SerializeField] private Vector3 focusOffset = new Vector3(0f, 1.6f, 0f);
|
||||||
|
|
||||||
|
[Header("Orbit")]
|
||||||
|
[SerializeField] private float mouseSensitivity = 180f;
|
||||||
|
[SerializeField] private float minPitch = -35f;
|
||||||
|
[SerializeField] private float maxPitch = 75f;
|
||||||
|
[SerializeField] private float rotationSmoothTime = 0.1f;
|
||||||
|
|
||||||
|
[Header("Distance")]
|
||||||
|
[SerializeField] private float distance = 5f;
|
||||||
|
[SerializeField] private float minDistance = 2f;
|
||||||
|
[SerializeField] private float maxDistance = 8f;
|
||||||
|
[SerializeField] private float zoomSpeed = 4f;
|
||||||
|
[SerializeField] private float distanceSmoothTime = 0.1f;
|
||||||
|
|
||||||
|
[Header("Collision")]
|
||||||
|
[SerializeField] private float obstructionRadius = 0.25f;
|
||||||
|
[SerializeField] private LayerMask obstructionMask = ~0;
|
||||||
|
[SerializeField] private float obstructionBuffer = 0.1f;
|
||||||
|
|
||||||
|
[Header("Cursor")]
|
||||||
|
[SerializeField] private bool lockCursor = true;
|
||||||
|
|
||||||
|
private Vector2 orbitAngles = new Vector2(20f, 0f);
|
||||||
|
private Vector2 currentOrbitAngles;
|
||||||
|
private float pitchVelocity;
|
||||||
|
private float yawVelocity;
|
||||||
|
private float desiredDistance;
|
||||||
|
private float distanceVelocity;
|
||||||
|
private static readonly RaycastHit[] ObstructionHits = new RaycastHit[8];
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
desiredDistance = Mathf.Clamp(distance, minDistance, maxDistance);
|
||||||
|
distance = desiredDistance;
|
||||||
|
currentOrbitAngles = orbitAngles;
|
||||||
|
pitchVelocity = yawVelocity = 0f;
|
||||||
|
|
||||||
|
if (lockCursor)
|
||||||
|
{
|
||||||
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
|
Cursor.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
if (lockCursor)
|
||||||
|
{
|
||||||
|
Cursor.lockState = CursorLockMode.None;
|
||||||
|
Cursor.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LateUpdate()
|
||||||
|
{
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float deltaTime = Time.deltaTime;
|
||||||
|
ReadOrbitInput(deltaTime);
|
||||||
|
ReadZoomInput();
|
||||||
|
|
||||||
|
if (rotationSmoothTime <= 0f)
|
||||||
|
{
|
||||||
|
currentOrbitAngles = orbitAngles;
|
||||||
|
pitchVelocity = yawVelocity = 0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentOrbitAngles.x = Mathf.SmoothDamp(currentOrbitAngles.x, orbitAngles.x, ref pitchVelocity, rotationSmoothTime, Mathf.Infinity, deltaTime);
|
||||||
|
currentOrbitAngles.y = Mathf.SmoothDamp(currentOrbitAngles.y, orbitAngles.y, ref yawVelocity, rotationSmoothTime, Mathf.Infinity, deltaTime);
|
||||||
|
}
|
||||||
|
Quaternion lookRotation = Quaternion.Euler(currentOrbitAngles.x, currentOrbitAngles.y, 0f);
|
||||||
|
|
||||||
|
Vector3 focusPoint = target.TransformPoint(focusOffset);
|
||||||
|
float smoothedDistance;
|
||||||
|
if (distanceSmoothTime <= 0f)
|
||||||
|
{
|
||||||
|
distanceVelocity = 0f;
|
||||||
|
smoothedDistance = desiredDistance;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
smoothedDistance = Mathf.SmoothDamp(distance, desiredDistance, ref distanceVelocity, distanceSmoothTime, Mathf.Infinity, deltaTime);
|
||||||
|
}
|
||||||
|
float unobstructedDistance = smoothedDistance;
|
||||||
|
float adjustedDistance = ResolveObstructions(focusPoint, lookRotation, unobstructedDistance);
|
||||||
|
float finalDistance = Mathf.Min(unobstructedDistance, adjustedDistance);
|
||||||
|
Vector3 finalPosition = focusPoint - lookRotation * Vector3.forward * finalDistance;
|
||||||
|
|
||||||
|
transform.SetPositionAndRotation(finalPosition, lookRotation);
|
||||||
|
distance = finalDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadOrbitInput(float deltaTime)
|
||||||
|
{
|
||||||
|
float lookX = Input.GetAxis("Mouse X");
|
||||||
|
float lookY = Input.GetAxis("Mouse Y");
|
||||||
|
|
||||||
|
if (Mathf.Abs(lookX) > 0.0001f || Mathf.Abs(lookY) > 0.0001f)
|
||||||
|
{
|
||||||
|
orbitAngles.y += lookX * mouseSensitivity * deltaTime;
|
||||||
|
orbitAngles.x -= lookY * mouseSensitivity * deltaTime;
|
||||||
|
orbitAngles.x = Mathf.Clamp(orbitAngles.x, minPitch, maxPitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadZoomInput()
|
||||||
|
{
|
||||||
|
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
||||||
|
if (Mathf.Abs(scroll) > 0.0001f)
|
||||||
|
{
|
||||||
|
desiredDistance = Mathf.Clamp(desiredDistance - scroll * zoomSpeed, minDistance, maxDistance);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
desiredDistance = Mathf.Clamp(desiredDistance, minDistance, maxDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float ResolveObstructions(Vector3 focusPoint, Quaternion lookRotation, float targetDistance)
|
||||||
|
{
|
||||||
|
if (targetDistance <= 0.001f)
|
||||||
|
{
|
||||||
|
return targetDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 direction = lookRotation * Vector3.back;
|
||||||
|
int hitCount = Physics.SphereCastNonAlloc(focusPoint, obstructionRadius, direction, ObstructionHits, targetDistance, obstructionMask, QueryTriggerInteraction.Ignore);
|
||||||
|
|
||||||
|
if (hitCount == 0)
|
||||||
|
{
|
||||||
|
return targetDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
float closestDistance = targetDistance;
|
||||||
|
|
||||||
|
for (int i = 0; i < hitCount; i++)
|
||||||
|
{
|
||||||
|
RaycastHit hit = ObstructionHits[i];
|
||||||
|
if (hit.collider == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform hitTransform = hit.collider.transform;
|
||||||
|
if (target != null && (hitTransform == target || hitTransform.IsChildOf(target)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hit.distance < closestDistance)
|
||||||
|
{
|
||||||
|
closestDistance = Mathf.Max(0f, hit.distance - obstructionBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTarget(Transform newTarget)
|
||||||
|
{
|
||||||
|
target = newTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
minPitch = Mathf.Clamp(minPitch, -89f, 89f);
|
||||||
|
maxPitch = Mathf.Clamp(maxPitch, -89f, 89f);
|
||||||
|
if (maxPitch < minPitch)
|
||||||
|
{
|
||||||
|
float temp = maxPitch;
|
||||||
|
maxPitch = minPitch;
|
||||||
|
minPitch = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseSensitivity = Mathf.Max(0f, mouseSensitivity);
|
||||||
|
zoomSpeed = Mathf.Max(0f, zoomSpeed);
|
||||||
|
obstructionRadius = Mathf.Max(0f, obstructionRadius);
|
||||||
|
obstructionBuffer = Mathf.Clamp(obstructionBuffer, 0f, 1f);
|
||||||
|
rotationSmoothTime = Mathf.Max(0f, rotationSmoothTime);
|
||||||
|
distanceSmoothTime = Mathf.Max(0f, distanceSmoothTime);
|
||||||
|
|
||||||
|
minDistance = Mathf.Max(0.1f, minDistance);
|
||||||
|
maxDistance = Mathf.Max(minDistance, maxDistance);
|
||||||
|
distance = Mathf.Clamp(distance, minDistance, maxDistance);
|
||||||
|
desiredDistance = Mathf.Clamp(distance, minDistance, maxDistance);
|
||||||
|
|
||||||
|
currentOrbitAngles = orbitAngles;
|
||||||
|
pitchVelocity = yawVelocity = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/ThirdPersonCamera.cs.meta
Normal file
2
Game/Scripts/ThirdPersonCamera.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e3ecae0254451c2888cfe94f7a8e825d
|
||||||
196
Game/Scripts/ThirdPersonCharacterController.cs
Normal file
196
Game/Scripts/ThirdPersonCharacterController.cs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game
|
||||||
|
{
|
||||||
|
[RequireComponent(typeof(UnityEngine.CharacterController))]
|
||||||
|
public class ThirdPersonCharacterController : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Movement")]
|
||||||
|
[SerializeField] private float moveSpeed = 5f;
|
||||||
|
[SerializeField] private float rotationSharpness = 12f;
|
||||||
|
|
||||||
|
[Header("Air Control")]
|
||||||
|
[SerializeField] private float airControlResponsiveness = 50f;
|
||||||
|
|
||||||
|
[Header("Jump")]
|
||||||
|
[SerializeField] private float jumpHeight = 1.6f;
|
||||||
|
[SerializeField] private float gravity = -20f;
|
||||||
|
[SerializeField] private float groundedGravity = -5f;
|
||||||
|
|
||||||
|
[Header("Camera Reference")]
|
||||||
|
[SerializeField] private Transform cameraTransform;
|
||||||
|
|
||||||
|
private UnityEngine.CharacterController characterController;
|
||||||
|
private Vector3 planarVelocity;
|
||||||
|
private float verticalVelocity;
|
||||||
|
private bool isGrounded;
|
||||||
|
private MegaKoop.Game.Networking.ICharacterInputSource inputSource;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
characterController = GetComponent<UnityEngine.CharacterController>();
|
||||||
|
|
||||||
|
if (cameraTransform == null)
|
||||||
|
{
|
||||||
|
Camera mainCamera = Camera.main;
|
||||||
|
if (mainCamera != null)
|
||||||
|
{
|
||||||
|
cameraTransform = mainCamera.transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isGrounded = characterController.isGrounded;
|
||||||
|
if (isGrounded)
|
||||||
|
{
|
||||||
|
verticalVelocity = groundedGravity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
Vector2 moveInput = ReadMovementInput();
|
||||||
|
Vector3 desiredMove = CalculateDesiredMove(moveInput);
|
||||||
|
bool hasMoveInput = desiredMove.sqrMagnitude > 0f;
|
||||||
|
|
||||||
|
UpdatePlanarVelocity(desiredMove, hasMoveInput);
|
||||||
|
TryRotateTowardsMovement(desiredMove, hasMoveInput);
|
||||||
|
|
||||||
|
UpdateGroundedStateBeforeGravity();
|
||||||
|
HandleJumpInput();
|
||||||
|
ApplyGravity();
|
||||||
|
|
||||||
|
Vector3 velocity = planarVelocity;
|
||||||
|
velocity.y = verticalVelocity;
|
||||||
|
|
||||||
|
CollisionFlags collisionFlags = characterController.Move(velocity * Time.deltaTime);
|
||||||
|
isGrounded = (collisionFlags & CollisionFlags.Below) != 0;
|
||||||
|
|
||||||
|
if (isGrounded && verticalVelocity < 0f)
|
||||||
|
{
|
||||||
|
verticalVelocity = groundedGravity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetInputSource(MegaKoop.Game.Networking.ICharacterInputSource source)
|
||||||
|
{
|
||||||
|
inputSource = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 ReadMovementInput()
|
||||||
|
{
|
||||||
|
if (inputSource != null)
|
||||||
|
{
|
||||||
|
Vector2 sourceInput = inputSource.MoveInput;
|
||||||
|
return Vector2.ClampMagnitude(sourceInput, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
float horizontal = Input.GetAxisRaw("Horizontal");
|
||||||
|
float vertical = Input.GetAxisRaw("Vertical");
|
||||||
|
Vector2 input = new Vector2(horizontal, vertical);
|
||||||
|
input = Vector2.ClampMagnitude(input, 1f);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3 CalculateDesiredMove(Vector2 input)
|
||||||
|
{
|
||||||
|
Vector3 forward = Vector3.forward;
|
||||||
|
Vector3 right = Vector3.right;
|
||||||
|
|
||||||
|
if (cameraTransform != null)
|
||||||
|
{
|
||||||
|
forward = cameraTransform.forward;
|
||||||
|
right = cameraTransform.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
forward.y = 0f;
|
||||||
|
right.y = 0f;
|
||||||
|
|
||||||
|
forward.Normalize();
|
||||||
|
right.Normalize();
|
||||||
|
|
||||||
|
Vector3 desiredMove = forward * input.y + right * input.x;
|
||||||
|
if (desiredMove.sqrMagnitude > 1f)
|
||||||
|
{
|
||||||
|
desiredMove.Normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
return desiredMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePlanarVelocity(Vector3 desiredMove, bool hasMoveInput)
|
||||||
|
{
|
||||||
|
if (isGrounded)
|
||||||
|
{
|
||||||
|
planarVelocity = hasMoveInput ? desiredMove * moveSpeed : Vector3.zero;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (airControlResponsiveness <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 targetVelocity = hasMoveInput ? desiredMove * moveSpeed : Vector3.zero;
|
||||||
|
float maxDelta = airControlResponsiveness * Time.deltaTime;
|
||||||
|
planarVelocity = Vector3.MoveTowards(planarVelocity, targetVelocity, maxDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryRotateTowardsMovement(Vector3 desiredMove, bool hasMoveInput)
|
||||||
|
{
|
||||||
|
if (!hasMoveInput)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Quaternion targetRotation = Quaternion.LookRotation(desiredMove, Vector3.up);
|
||||||
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSharpness * Time.deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGroundedStateBeforeGravity()
|
||||||
|
{
|
||||||
|
if (isGrounded && verticalVelocity < 0f)
|
||||||
|
{
|
||||||
|
verticalVelocity = groundedGravity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleJumpInput()
|
||||||
|
{
|
||||||
|
if (!isGrounded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShouldJumpThisFrame())
|
||||||
|
{
|
||||||
|
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
||||||
|
isGrounded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldJumpThisFrame()
|
||||||
|
{
|
||||||
|
if (inputSource != null)
|
||||||
|
{
|
||||||
|
return inputSource.JumpPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Input.GetButtonDown("Jump");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyGravity()
|
||||||
|
{
|
||||||
|
verticalVelocity += gravity * Time.deltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
moveSpeed = Mathf.Max(0f, moveSpeed);
|
||||||
|
rotationSharpness = Mathf.Max(0f, rotationSharpness);
|
||||||
|
jumpHeight = Mathf.Max(0f, jumpHeight);
|
||||||
|
airControlResponsiveness = Mathf.Max(0f, airControlResponsiveness);
|
||||||
|
gravity = Mathf.Min(-0.01f, gravity);
|
||||||
|
groundedGravity = Mathf.Clamp(groundedGravity, gravity, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Game/Scripts/ThirdPersonCharacterController.cs.meta
Normal file
11
Game/Scripts/ThirdPersonCharacterController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9309e1c5110afc714b0b9b9d10323469
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Game/Scripts/UI.meta
Normal file
8
Game/Scripts/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ee0194cba591c20dfa044c4c9c8e595f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
41
Game/Scripts/UI/FontAssetPreloader.cs
Normal file
41
Game/Scripts/UI/FontAssetPreloader.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.TextCore.Text;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Preloads required characters into dynamic font atlases on the main thread to avoid runtime race conditions.
|
||||||
|
/// Attach once to a bootstrap GameObject and assign all UI font assets used by UITK.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FontAssetPreloader : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private FontAsset[] fontAssets;
|
||||||
|
[SerializeField, TextArea(1, 4)] private string charactersToPreload = "…•-";
|
||||||
|
|
||||||
|
private System.Collections.IEnumerator Start()
|
||||||
|
{
|
||||||
|
if (fontAssets == null || fontAssets.Length == 0)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait one frame to ensure the scene finished loading on the main thread.
|
||||||
|
yield return null;
|
||||||
|
|
||||||
|
foreach (FontAsset asset in fontAssets)
|
||||||
|
{
|
||||||
|
if (asset == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to add characters that are commonly generated at runtime (ellipsis, bullets, dashes, etc.).
|
||||||
|
bool allAdded = asset.TryAddCharacters(charactersToPreload, out string missingCharacters);
|
||||||
|
if (!allAdded && !string.IsNullOrEmpty(missingCharacters))
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[FontAssetPreloader] Missing glyphs '{missingCharacters}' in font '{asset.name}'. Consider adding a fallback font.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/UI/FontAssetPreloader.cs.meta
Normal file
2
Game/Scripts/UI/FontAssetPreloader.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5f85fa1035779f1bbba9cd75923e55d0
|
||||||
534
Game/Scripts/WeaponController.cs
Normal file
534
Game/Scripts/WeaponController.cs
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MegaKoop.Game.Combat;
|
||||||
|
using MegaKoop.Game.WeaponSystem;
|
||||||
|
using UnityEngine;
|
||||||
|
using Random = UnityEngine.Random;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.WeaponSystem
|
||||||
|
{
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class WeaponController : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Ownership")]
|
||||||
|
[SerializeField] private Team ownerTeam = Team.Heroes;
|
||||||
|
[SerializeField] private Transform weaponSocket;
|
||||||
|
|
||||||
|
[Header("Targeting")]
|
||||||
|
[SerializeField] private float acquisitionRadius = 30f;
|
||||||
|
[SerializeField] private float retargetInterval = 0.25f;
|
||||||
|
[SerializeField] private LayerMask targetMask = Physics.DefaultRaycastLayers;
|
||||||
|
[SerializeField] private LayerMask lineOfSightMask = Physics.DefaultRaycastLayers;
|
||||||
|
[SerializeField] private bool requireLineOfSight = true;
|
||||||
|
|
||||||
|
[Header("Loadout")]
|
||||||
|
[SerializeField] private WeaponDefinition[] startingWeapons;
|
||||||
|
[SerializeField] private bool autoFire = true;
|
||||||
|
|
||||||
|
private static readonly RaycastHit[] LineOfSightHitsBuffer = new RaycastHit[8];
|
||||||
|
|
||||||
|
private readonly List<WeaponRuntime> equippedWeapons = new();
|
||||||
|
private readonly HashSet<Collider> ignoredOwnerColliders = new();
|
||||||
|
|
||||||
|
public event Action<ProjectileSpawnEvent> ProjectileSpawned;
|
||||||
|
|
||||||
|
private float maxWeaponRange;
|
||||||
|
private float retargetTimer;
|
||||||
|
private TargetInfo currentTarget;
|
||||||
|
private bool hasTarget;
|
||||||
|
private Collider[] ownerColliders;
|
||||||
|
private Health ownerHealth;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
ownerHealth = GetComponent<Health>();
|
||||||
|
if (ownerHealth != null)
|
||||||
|
{
|
||||||
|
ownerTeam = ownerHealth.Team;
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerColliders = GetComponentsInChildren<Collider>();
|
||||||
|
if (ownerColliders != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < ownerColliders.Length; i++)
|
||||||
|
{
|
||||||
|
Collider ownerCollider = ownerColliders[i];
|
||||||
|
if (ownerCollider == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoredOwnerColliders.Add(ownerCollider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (weaponSocket == null)
|
||||||
|
{
|
||||||
|
weaponSocket = transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startingWeapons != null)
|
||||||
|
{
|
||||||
|
foreach (WeaponDefinition definition in startingWeapons)
|
||||||
|
{
|
||||||
|
Equip(definition, weaponSocket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (equippedWeapons.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoFire)
|
||||||
|
{
|
||||||
|
UpdateTargetSelection(Time.deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
TargetInfo target = hasTarget ? currentTarget : default;
|
||||||
|
|
||||||
|
foreach (WeaponRuntime weapon in equippedWeapons)
|
||||||
|
{
|
||||||
|
weapon.Tick(Time.deltaTime, target, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Equip(WeaponDefinition definition, Transform mount)
|
||||||
|
{
|
||||||
|
if (definition == null || mount == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int weaponIndex = equippedWeapons.Count;
|
||||||
|
WeaponRuntime runtime = new WeaponRuntime(definition, mount, weaponIndex);
|
||||||
|
equippedWeapons.Add(runtime);
|
||||||
|
maxWeaponRange = Mathf.Max(maxWeaponRange, definition.Range);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unequip(WeaponDefinition definition)
|
||||||
|
{
|
||||||
|
if (definition == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = equippedWeapons.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (equippedWeapons[i].Definition == definition)
|
||||||
|
{
|
||||||
|
equippedWeapons[i].Dispose();
|
||||||
|
equippedWeapons.RemoveAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RecalculateMaxRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
foreach (WeaponRuntime weapon in equippedWeapons)
|
||||||
|
{
|
||||||
|
weapon.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
equippedWeapons.Clear();
|
||||||
|
hasTarget = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTargetSelection(float deltaTime)
|
||||||
|
{
|
||||||
|
retargetTimer -= deltaTime;
|
||||||
|
|
||||||
|
if (hasTarget)
|
||||||
|
{
|
||||||
|
if (!currentTarget.IsValid)
|
||||||
|
{
|
||||||
|
hasTarget = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float distance = Vector3.Distance(transform.position, currentTarget.Transform.position);
|
||||||
|
float allowedRange = Mathf.Max(acquisitionRadius, maxWeaponRange);
|
||||||
|
if (distance > allowedRange * 1.1f)
|
||||||
|
{
|
||||||
|
hasTarget = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retargetTimer > 0f && hasTarget)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
retargetTimer = retargetInterval;
|
||||||
|
AcquireTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AcquireTarget()
|
||||||
|
{
|
||||||
|
float searchRadius = Mathf.Max(acquisitionRadius, maxWeaponRange);
|
||||||
|
if (searchRadius <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Collider[] hits = Physics.OverlapSphere(transform.position, searchRadius, targetMask, QueryTriggerInteraction.Collide);
|
||||||
|
TargetInfo bestTarget = default;
|
||||||
|
float bestScore = float.MaxValue;
|
||||||
|
|
||||||
|
foreach (Collider hit in hits)
|
||||||
|
{
|
||||||
|
if (hit == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoredOwnerColliders.Contains(hit))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDamageable damageable = hit.GetComponentInParent<IDamageable>();
|
||||||
|
if (damageable == null || !damageable.IsAlive)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (damageable.Team == ownerTeam && ownerTeam != Team.Neutral)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (damageable is not Component component)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform targetTransform = component.transform;
|
||||||
|
float sqrDistance = (targetTransform.position - transform.position).sqrMagnitude;
|
||||||
|
if (sqrDistance > maxWeaponRange * maxWeaponRange)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float score = sqrDistance;
|
||||||
|
if (score < bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
bestTarget = new TargetInfo(damageable, hit, targetTransform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTarget = bestTarget.IsValid;
|
||||||
|
currentTarget = bestTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecalculateMaxRange()
|
||||||
|
{
|
||||||
|
maxWeaponRange = 0f;
|
||||||
|
for (int i = 0; i < equippedWeapons.Count; i++)
|
||||||
|
{
|
||||||
|
WeaponRuntime weapon = equippedWeapons[i];
|
||||||
|
weapon.SetIndex(i);
|
||||||
|
maxWeaponRange = Mathf.Max(maxWeaponRange, weapon.Definition.Range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3 GetAimPoint(TargetInfo target)
|
||||||
|
{
|
||||||
|
if (!target.IsValid)
|
||||||
|
{
|
||||||
|
return Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.Collider != null)
|
||||||
|
{
|
||||||
|
return target.Collider.bounds.center;
|
||||||
|
}
|
||||||
|
|
||||||
|
return target.Transform.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasLineOfSight(Transform muzzle, Vector3 aimPoint, TargetInfo target)
|
||||||
|
{
|
||||||
|
if (!requireLineOfSight)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 origin = muzzle.position;
|
||||||
|
Vector3 toTarget = aimPoint - origin;
|
||||||
|
float distance = toTarget.magnitude;
|
||||||
|
if (distance <= 0.01f)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 direction = toTarget.normalized;
|
||||||
|
int hitCount = Physics.RaycastNonAlloc(origin, direction, LineOfSightHitsBuffer, distance, lineOfSightMask, QueryTriggerInteraction.Ignore);
|
||||||
|
|
||||||
|
if (hitCount == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
float targetDistance = float.MaxValue;
|
||||||
|
bool hasTargetHit = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < hitCount; i++)
|
||||||
|
{
|
||||||
|
RaycastHit hit = LineOfSightHitsBuffer[i];
|
||||||
|
Collider hitCollider = hit.collider;
|
||||||
|
if (hitCollider == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoredOwnerColliders.Contains(hitCollider))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.Collider != null && hitCollider == target.Collider)
|
||||||
|
{
|
||||||
|
targetDistance = hit.distance;
|
||||||
|
hasTargetHit = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hit.distance < targetDistance)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasTargetHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnProjectile(WeaponRuntime weapon, Transform muzzle, Vector3 direction)
|
||||||
|
{
|
||||||
|
Projectile projectilePrefab = weapon.Definition.ProjectilePrefab;
|
||||||
|
if (projectilePrefab == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Quaternion rotation = direction.sqrMagnitude > 0f ? Quaternion.LookRotation(direction) : muzzle.rotation;
|
||||||
|
Projectile projectile = Instantiate(projectilePrefab, muzzle.position, rotation);
|
||||||
|
projectile.Initialize(direction, weapon.Definition.ProjectileSpeed, weapon.Definition.Damage, weapon.Definition.ProjectileLifetime, gameObject, ownerTeam, ownerColliders, weapon.Definition.HitMask);
|
||||||
|
|
||||||
|
ProjectileSpawned?.Invoke(new ProjectileSpawnEvent(weapon.Index, muzzle.position, direction, weapon.Definition.ProjectileSpeed, weapon.Definition.ProjectileLifetime, weapon.Definition.Damage));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SpawnNetworkProjectile(int weaponIndex, Vector3 position, Vector3 direction, float speed, float lifetime, float damage)
|
||||||
|
{
|
||||||
|
if (weaponIndex < 0 || weaponIndex >= equippedWeapons.Count)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WeaponRuntime weapon = equippedWeapons[weaponIndex];
|
||||||
|
weapon.SpawnFromNetwork(position, direction, speed, lifetime, damage, ownerTeam, ownerColliders, gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FireWeapon(WeaponRuntime weapon, TargetInfo target, Transform muzzle)
|
||||||
|
{
|
||||||
|
Vector3 aimPoint = GetAimPoint(target);
|
||||||
|
Vector3 toTarget = aimPoint - muzzle.position;
|
||||||
|
if (toTarget.sqrMagnitude <= 0.0001f)
|
||||||
|
{
|
||||||
|
toTarget = muzzle.forward;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HasLineOfSight(muzzle, aimPoint, target))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 baseDirection = toTarget.normalized;
|
||||||
|
int projectiles = weapon.Definition.ProjectilesPerShot;
|
||||||
|
float spread = weapon.Definition.SpreadAngle;
|
||||||
|
|
||||||
|
for (int i = 0; i < projectiles; i++)
|
||||||
|
{
|
||||||
|
Vector3 direction = baseDirection;
|
||||||
|
if (spread > 0f)
|
||||||
|
{
|
||||||
|
Vector2 offset = Random.insideUnitCircle * spread;
|
||||||
|
Quaternion spreadRotation = Quaternion.AngleAxis(offset.x, muzzle.up) * Quaternion.AngleAxis(offset.y, muzzle.right);
|
||||||
|
direction = spreadRotation * baseDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpawnProjectile(weapon, muzzle, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
weapon.ResetCooldown();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct TargetInfo
|
||||||
|
{
|
||||||
|
public readonly IDamageable Damageable;
|
||||||
|
public readonly Collider Collider;
|
||||||
|
public readonly Transform Transform;
|
||||||
|
|
||||||
|
public TargetInfo(IDamageable damageable, Collider collider, Transform transform)
|
||||||
|
{
|
||||||
|
Damageable = damageable;
|
||||||
|
Collider = collider;
|
||||||
|
Transform = transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValid => Damageable != null && Damageable.IsAlive && Transform != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class WeaponRuntime
|
||||||
|
{
|
||||||
|
private readonly Transform mountPoint;
|
||||||
|
private readonly Transform fallbackMuzzle;
|
||||||
|
private readonly Transform[] muzzles;
|
||||||
|
private int muzzleIndex;
|
||||||
|
private float cooldown;
|
||||||
|
|
||||||
|
public WeaponDefinition Definition { get; }
|
||||||
|
public WeaponView ViewInstance { get; }
|
||||||
|
public int Index { get; private set; }
|
||||||
|
|
||||||
|
public WeaponRuntime(WeaponDefinition definition, Transform mount, int index)
|
||||||
|
{
|
||||||
|
Definition = definition ?? throw new ArgumentNullException(nameof(definition));
|
||||||
|
mountPoint = mount;
|
||||||
|
Index = index;
|
||||||
|
|
||||||
|
if (definition.ViewPrefab != null)
|
||||||
|
{
|
||||||
|
ViewInstance = UnityEngine.Object.Instantiate(definition.ViewPrefab, mountPoint);
|
||||||
|
ViewInstance.transform.localPosition = Vector3.zero;
|
||||||
|
ViewInstance.transform.localRotation = Quaternion.identity;
|
||||||
|
ViewInstance.transform.localScale = Vector3.one;
|
||||||
|
muzzles = ViewInstance.Muzzles;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ViewInstance = null;
|
||||||
|
muzzles = Array.Empty<Transform>();
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackMuzzle = mountPoint;
|
||||||
|
muzzleIndex = 0;
|
||||||
|
cooldown = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tick(float deltaTime, TargetInfo target, WeaponController controller)
|
||||||
|
{
|
||||||
|
cooldown = Mathf.Max(0f, cooldown - deltaTime);
|
||||||
|
if (cooldown > 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.IsValid)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Transform muzzle = GetMuzzle();
|
||||||
|
if (muzzle == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 aimPoint = controller.GetAimPoint(target);
|
||||||
|
Vector3 toTarget = aimPoint - muzzle.position;
|
||||||
|
float sqrDistance = toTarget.sqrMagnitude;
|
||||||
|
float maxRange = Definition.Range;
|
||||||
|
if (sqrDistance > maxRange * maxRange)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.FireWeapon(this, target, muzzle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transform GetMuzzle()
|
||||||
|
{
|
||||||
|
if (muzzles != null && muzzles.Length > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < muzzles.Length; i++)
|
||||||
|
{
|
||||||
|
int current = (muzzleIndex + i) % muzzles.Length;
|
||||||
|
Transform muzzle = muzzles[current];
|
||||||
|
if (muzzle != null)
|
||||||
|
{
|
||||||
|
muzzleIndex = (current + 1) % muzzles.Length;
|
||||||
|
return muzzle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackMuzzle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetCooldown()
|
||||||
|
{
|
||||||
|
cooldown = Definition.FireInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (ViewInstance != null)
|
||||||
|
{
|
||||||
|
UnityEngine.Object.Destroy(ViewInstance.gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetIndex(int newIndex)
|
||||||
|
{
|
||||||
|
Index = newIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SpawnFromNetwork(Vector3 position, Vector3 direction, float speed, float lifetime, float damage, Team ownerTeam, Collider[] ownerColliders, GameObject owner)
|
||||||
|
{
|
||||||
|
Projectile projectilePrefab = Definition.ProjectilePrefab;
|
||||||
|
if (projectilePrefab == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Quaternion rotation = direction.sqrMagnitude > 0f ? Quaternion.LookRotation(direction) : fallbackMuzzle.rotation;
|
||||||
|
Projectile projectile = UnityEngine.Object.Instantiate(projectilePrefab, position, rotation);
|
||||||
|
float resolvedSpeed = speed > 0f ? speed : Definition.ProjectileSpeed;
|
||||||
|
float resolvedLifetime = lifetime > 0f ? lifetime : Definition.ProjectileLifetime;
|
||||||
|
float resolvedDamage = damage > 0f ? damage : Definition.Damage;
|
||||||
|
projectile.Initialize(direction, resolvedSpeed, resolvedDamage, resolvedLifetime, owner, ownerTeam, ownerColliders, Definition.HitMask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ProjectileSpawnEvent
|
||||||
|
{
|
||||||
|
public readonly int WeaponIndex;
|
||||||
|
public readonly Vector3 Position;
|
||||||
|
public readonly Vector3 Direction;
|
||||||
|
public readonly float Speed;
|
||||||
|
public readonly float Lifetime;
|
||||||
|
public readonly float Damage;
|
||||||
|
|
||||||
|
public ProjectileSpawnEvent(int weaponIndex, Vector3 position, Vector3 direction, float speed, float lifetime, float damage)
|
||||||
|
{
|
||||||
|
WeaponIndex = weaponIndex;
|
||||||
|
Position = position;
|
||||||
|
Direction = direction;
|
||||||
|
Speed = speed;
|
||||||
|
Lifetime = lifetime;
|
||||||
|
Damage = damage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/WeaponController.cs.meta
Normal file
2
Game/Scripts/WeaponController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e6e77e83dcc282364afb3428637c723c
|
||||||
8
Game/Scripts/WeaponSystem.meta
Normal file
8
Game/Scripts/WeaponSystem.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 44d32c675960e4dceb05fc13f06e43bd
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
123
Game/Scripts/WeaponSystem/Projectile.cs
Normal file
123
Game/Scripts/WeaponSystem/Projectile.cs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
using MegaKoop.Game.Combat;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.WeaponSystem
|
||||||
|
{
|
||||||
|
public class Projectile : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private float hitRadius = 0.05f;
|
||||||
|
[SerializeField] private LayerMask hitMask = Physics.DefaultRaycastLayers;
|
||||||
|
[SerializeField] private bool alignToVelocity = true;
|
||||||
|
|
||||||
|
private Vector3 direction;
|
||||||
|
private float speed;
|
||||||
|
private float damage;
|
||||||
|
private float lifetime;
|
||||||
|
private float timeAlive;
|
||||||
|
private Team sourceTeam;
|
||||||
|
private GameObject owner;
|
||||||
|
private Collider[] projectileColliders;
|
||||||
|
|
||||||
|
public void Initialize(Vector3 shotDirection, float projectileSpeed, float damageAmount, float projectileLifetime, GameObject projectileOwner, Team ownerTeam, Collider[] ownerColliders, LayerMask mask)
|
||||||
|
{
|
||||||
|
direction = shotDirection.sqrMagnitude > 0f ? shotDirection.normalized : transform.forward;
|
||||||
|
speed = Mathf.Max(0f, projectileSpeed);
|
||||||
|
damage = Mathf.Max(0f, damageAmount);
|
||||||
|
lifetime = Mathf.Max(0.01f, projectileLifetime);
|
||||||
|
owner = projectileOwner;
|
||||||
|
sourceTeam = ownerTeam;
|
||||||
|
hitMask = mask;
|
||||||
|
timeAlive = 0f;
|
||||||
|
|
||||||
|
projectileColliders ??= GetComponentsInChildren<Collider>();
|
||||||
|
if (ownerColliders != null && projectileColliders != null)
|
||||||
|
{
|
||||||
|
foreach (Collider ownerCollider in ownerColliders)
|
||||||
|
{
|
||||||
|
if (ownerCollider == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Collider projectileCollider in projectileColliders)
|
||||||
|
{
|
||||||
|
if (projectileCollider == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Physics.IgnoreCollision(projectileCollider, ownerCollider, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
float deltaTime = Time.deltaTime;
|
||||||
|
Vector3 displacement = direction * speed * deltaTime;
|
||||||
|
float distance = displacement.magnitude;
|
||||||
|
|
||||||
|
if (distance > 0f)
|
||||||
|
{
|
||||||
|
if (Physics.SphereCast(transform.position, hitRadius, direction, out RaycastHit hitInfo, distance, hitMask, QueryTriggerInteraction.Ignore))
|
||||||
|
{
|
||||||
|
TryHandleHit(hitInfo.collider, hitInfo.point, hitInfo.normal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform.position += displacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alignToVelocity && distance > 0f)
|
||||||
|
{
|
||||||
|
transform.forward = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeAlive += deltaTime;
|
||||||
|
if (timeAlive >= lifetime)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTriggerEnter(Collider other)
|
||||||
|
{
|
||||||
|
TryHandleHit(other, transform.position, -direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryHandleHit(Collider other, Vector3 hitPoint, Vector3 hitNormal)
|
||||||
|
{
|
||||||
|
if (other == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (owner != null)
|
||||||
|
{
|
||||||
|
if (other.transform.IsChildOf(owner.transform))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IDamageable damageable = other.GetComponentInParent<IDamageable>();
|
||||||
|
if (damageable != null && damageable.IsAlive)
|
||||||
|
{
|
||||||
|
bool isFriendly = damageable.Team == sourceTeam && damageable.Team != Team.Neutral;
|
||||||
|
if (isFriendly)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (damage > 0f)
|
||||||
|
{
|
||||||
|
var payload = new DamagePayload(damage, hitPoint, hitNormal, owner, sourceTeam, other.gameObject);
|
||||||
|
damageable.ApplyDamage(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Destroy(gameObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/WeaponSystem/Projectile.cs.meta
Normal file
2
Game/Scripts/WeaponSystem/Projectile.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 038cb7cb48b0cb10da7ebba4ba473c63
|
||||||
48
Game/Scripts/WeaponSystem/WeaponDefinition.cs
Normal file
48
Game/Scripts/WeaponSystem/WeaponDefinition.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.WeaponSystem
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(fileName = "WeaponDefinition", menuName = "MegaKoop/Weapons/Weapon Definition", order = 0)]
|
||||||
|
public class WeaponDefinition : ScriptableObject
|
||||||
|
{
|
||||||
|
[Header("Presentation")]
|
||||||
|
[SerializeField] private string displayName = "Weapon";
|
||||||
|
[SerializeField] private WeaponView viewPrefab;
|
||||||
|
|
||||||
|
[Header("Projectile")]
|
||||||
|
[SerializeField] private Projectile projectilePrefab;
|
||||||
|
[SerializeField] private float projectileSpeed = 25f;
|
||||||
|
[SerializeField] private float projectileLifetime = 3f;
|
||||||
|
|
||||||
|
[Header("Firing")]
|
||||||
|
[SerializeField] private float shotsPerSecond = 2f;
|
||||||
|
[SerializeField] private float baseDamage = 10f;
|
||||||
|
[SerializeField] private float range = 25f;
|
||||||
|
[SerializeField] private int projectilesPerShot = 1;
|
||||||
|
[SerializeField, Range(0f, 45f)] private float spreadAngle = 0f;
|
||||||
|
[SerializeField] private LayerMask hitMask = Physics.DefaultRaycastLayers;
|
||||||
|
|
||||||
|
public string DisplayName => displayName;
|
||||||
|
public WeaponView ViewPrefab => viewPrefab;
|
||||||
|
public Projectile ProjectilePrefab => projectilePrefab;
|
||||||
|
public float ProjectileSpeed => Mathf.Max(0f, projectileSpeed);
|
||||||
|
public float ProjectileLifetime => Mathf.Max(0.05f, projectileLifetime);
|
||||||
|
public float FireInterval => shotsPerSecond <= 0f ? float.MaxValue : 1f / shotsPerSecond;
|
||||||
|
public float Damage => Mathf.Max(0f, baseDamage);
|
||||||
|
public float Range => Mathf.Max(0f, range);
|
||||||
|
public int ProjectilesPerShot => Mathf.Max(1, projectilesPerShot);
|
||||||
|
public float SpreadAngle => Mathf.Abs(spreadAngle);
|
||||||
|
public LayerMask HitMask => hitMask;
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
projectileSpeed = Mathf.Max(0f, projectileSpeed);
|
||||||
|
projectileLifetime = Mathf.Max(0.05f, projectileLifetime);
|
||||||
|
shotsPerSecond = Mathf.Max(0.01f, shotsPerSecond);
|
||||||
|
baseDamage = Mathf.Max(0f, baseDamage);
|
||||||
|
range = Mathf.Max(0f, range);
|
||||||
|
projectilesPerShot = Mathf.Max(1, projectilesPerShot);
|
||||||
|
spreadAngle = Mathf.Clamp(spreadAngle, 0f, 90f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/WeaponSystem/WeaponDefinition.cs.meta
Normal file
2
Game/Scripts/WeaponSystem/WeaponDefinition.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2fd5ff95d6e2be5d09b6c054570cef0a
|
||||||
43
Game/Scripts/WeaponSystem/WeaponView.cs
Normal file
43
Game/Scripts/WeaponSystem/WeaponView.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace MegaKoop.Game.WeaponSystem
|
||||||
|
{
|
||||||
|
public class WeaponView : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField] private Transform[] muzzles;
|
||||||
|
|
||||||
|
public Transform[] Muzzles => muzzles;
|
||||||
|
|
||||||
|
public Transform GetMuzzle(int index = 0)
|
||||||
|
{
|
||||||
|
if (muzzles == null || muzzles.Length == 0)
|
||||||
|
{
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
index = Mathf.Clamp(index, 0, muzzles.Length - 1);
|
||||||
|
return muzzles[index] == null ? transform : muzzles[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
if (muzzles == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gizmos.color = Color.cyan;
|
||||||
|
foreach (Transform muzzle in muzzles)
|
||||||
|
{
|
||||||
|
if (muzzle == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gizmos.DrawLine(muzzle.position, muzzle.position + muzzle.forward * 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Game/Scripts/WeaponSystem/WeaponView.cs.meta
Normal file
2
Game/Scripts/WeaponSystem/WeaponView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5590d430717d95f878b7929af7bf28ff
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"version": 1,
|
||||||
"name": "InputSystem_Actions",
|
"name": "InputSystem_Actions",
|
||||||
"maps": [
|
"maps": [
|
||||||
{
|
{
|
||||||
|
|||||||
8
Scenes/CharacterScene.meta
Normal file
8
Scenes/CharacterScene.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d254ee0417feae63888d6c9b45dcb33e
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
691
Scenes/CharacterScene.unity
Normal file
691
Scenes/CharacterScene.unity
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!29 &1
|
||||||
|
OcclusionCullingSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 2
|
||||||
|
m_OcclusionBakeSettings:
|
||||||
|
smallestOccluder: 5
|
||||||
|
smallestHole: 0.25
|
||||||
|
backfaceThreshold: 100
|
||||||
|
m_SceneGUID: 00000000000000000000000000000000
|
||||||
|
m_OcclusionCullingData: {fileID: 0}
|
||||||
|
--- !u!104 &2
|
||||||
|
RenderSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 10
|
||||||
|
m_Fog: 0
|
||||||
|
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||||
|
m_FogMode: 3
|
||||||
|
m_FogDensity: 0.01
|
||||||
|
m_LinearFogStart: 0
|
||||||
|
m_LinearFogEnd: 300
|
||||||
|
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
|
||||||
|
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
|
||||||
|
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
|
||||||
|
m_AmbientIntensity: 1
|
||||||
|
m_AmbientMode: 0
|
||||||
|
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
|
||||||
|
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_HaloStrength: 0.5
|
||||||
|
m_FlareStrength: 1
|
||||||
|
m_FlareFadeSpeed: 3
|
||||||
|
m_HaloTexture: {fileID: 0}
|
||||||
|
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
m_DefaultReflectionMode: 0
|
||||||
|
m_DefaultReflectionResolution: 128
|
||||||
|
m_ReflectionBounces: 1
|
||||||
|
m_ReflectionIntensity: 1
|
||||||
|
m_CustomReflection: {fileID: 0}
|
||||||
|
m_Sun: {fileID: 0}
|
||||||
|
m_UseRadianceAmbientProbe: 0
|
||||||
|
--- !u!157 &3
|
||||||
|
LightmapSettings:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 13
|
||||||
|
m_BakeOnSceneLoad: 0
|
||||||
|
m_GISettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_BounceScale: 1
|
||||||
|
m_IndirectOutputScale: 1
|
||||||
|
m_AlbedoBoost: 1
|
||||||
|
m_EnvironmentLightingMode: 0
|
||||||
|
m_EnableBakedLightmaps: 1
|
||||||
|
m_EnableRealtimeLightmaps: 0
|
||||||
|
m_LightmapEditorSettings:
|
||||||
|
serializedVersion: 12
|
||||||
|
m_Resolution: 2
|
||||||
|
m_BakeResolution: 40
|
||||||
|
m_AtlasSize: 1024
|
||||||
|
m_AO: 0
|
||||||
|
m_AOMaxDistance: 1
|
||||||
|
m_CompAOExponent: 1
|
||||||
|
m_CompAOExponentDirect: 0
|
||||||
|
m_ExtractAmbientOcclusion: 0
|
||||||
|
m_Padding: 2
|
||||||
|
m_LightmapParameters: {fileID: 0}
|
||||||
|
m_LightmapsBakeMode: 1
|
||||||
|
m_TextureCompression: 1
|
||||||
|
m_ReflectionCompression: 2
|
||||||
|
m_MixedBakeMode: 2
|
||||||
|
m_BakeBackend: 1
|
||||||
|
m_PVRSampling: 1
|
||||||
|
m_PVRDirectSampleCount: 32
|
||||||
|
m_PVRSampleCount: 512
|
||||||
|
m_PVRBounces: 2
|
||||||
|
m_PVREnvironmentSampleCount: 256
|
||||||
|
m_PVREnvironmentReferencePointCount: 2048
|
||||||
|
m_PVRFilteringMode: 1
|
||||||
|
m_PVRDenoiserTypeDirect: 1
|
||||||
|
m_PVRDenoiserTypeIndirect: 1
|
||||||
|
m_PVRDenoiserTypeAO: 1
|
||||||
|
m_PVRFilterTypeDirect: 0
|
||||||
|
m_PVRFilterTypeIndirect: 0
|
||||||
|
m_PVRFilterTypeAO: 0
|
||||||
|
m_PVREnvironmentMIS: 1
|
||||||
|
m_PVRCulling: 1
|
||||||
|
m_PVRFilteringGaussRadiusDirect: 1
|
||||||
|
m_PVRFilteringGaussRadiusIndirect: 1
|
||||||
|
m_PVRFilteringGaussRadiusAO: 1
|
||||||
|
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
|
||||||
|
m_PVRFilteringAtrousPositionSigmaIndirect: 2
|
||||||
|
m_PVRFilteringAtrousPositionSigmaAO: 1
|
||||||
|
m_ExportTrainingData: 0
|
||||||
|
m_TrainingDataDestination: TrainingData
|
||||||
|
m_LightProbeSampleCountMultiplier: 4
|
||||||
|
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_LightingSettings: {fileID: 0}
|
||||||
|
--- !u!196 &4
|
||||||
|
NavMeshSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_BuildSettings:
|
||||||
|
serializedVersion: 3
|
||||||
|
agentTypeID: 0
|
||||||
|
agentRadius: 0.5
|
||||||
|
agentHeight: 2
|
||||||
|
agentSlope: 45
|
||||||
|
agentClimb: 0.4
|
||||||
|
ledgeDropHeight: 0
|
||||||
|
maxJumpAcrossDistance: 0
|
||||||
|
minRegionArea: 2
|
||||||
|
manualCellSize: 0
|
||||||
|
cellSize: 0.16666667
|
||||||
|
manualTileSize: 0
|
||||||
|
tileSize: 256
|
||||||
|
buildHeightMesh: 0
|
||||||
|
maxJobWorkers: 0
|
||||||
|
preserveTilesOutsideBounds: 0
|
||||||
|
debug:
|
||||||
|
m_Flags: 0
|
||||||
|
m_NavMeshData: {fileID: 0}
|
||||||
|
--- !u!1 &98592370
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 98592372}
|
||||||
|
- component: {fileID: 98592371}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Directional Light
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!108 &98592371
|
||||||
|
Light:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 98592370}
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 11
|
||||||
|
m_Type: 1
|
||||||
|
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
|
||||||
|
m_Intensity: 1
|
||||||
|
m_Range: 10
|
||||||
|
m_SpotAngle: 30
|
||||||
|
m_InnerSpotAngle: 21.80208
|
||||||
|
m_CookieSize: 10
|
||||||
|
m_Shadows:
|
||||||
|
m_Type: 2
|
||||||
|
m_Resolution: -1
|
||||||
|
m_CustomResolution: -1
|
||||||
|
m_Strength: 1
|
||||||
|
m_Bias: 0.05
|
||||||
|
m_NormalBias: 0.4
|
||||||
|
m_NearPlane: 0.2
|
||||||
|
m_CullingMatrixOverride:
|
||||||
|
e00: 1
|
||||||
|
e01: 0
|
||||||
|
e02: 0
|
||||||
|
e03: 0
|
||||||
|
e10: 0
|
||||||
|
e11: 1
|
||||||
|
e12: 0
|
||||||
|
e13: 0
|
||||||
|
e20: 0
|
||||||
|
e21: 0
|
||||||
|
e22: 1
|
||||||
|
e23: 0
|
||||||
|
e30: 0
|
||||||
|
e31: 0
|
||||||
|
e32: 0
|
||||||
|
e33: 1
|
||||||
|
m_UseCullingMatrixOverride: 0
|
||||||
|
m_Cookie: {fileID: 0}
|
||||||
|
m_DrawHalo: 0
|
||||||
|
m_Flare: {fileID: 0}
|
||||||
|
m_RenderMode: 0
|
||||||
|
m_CullingMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 4294967295
|
||||||
|
m_RenderingLayerMask: 1
|
||||||
|
m_Lightmapping: 4
|
||||||
|
m_LightShadowCasterMode: 0
|
||||||
|
m_AreaSize: {x: 1, y: 1}
|
||||||
|
m_BounceIntensity: 1
|
||||||
|
m_ColorTemperature: 6570
|
||||||
|
m_UseColorTemperature: 0
|
||||||
|
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
|
||||||
|
m_UseBoundingSphereOverride: 0
|
||||||
|
m_UseViewFrustumForShadowCasterCull: 1
|
||||||
|
m_ForceVisible: 0
|
||||||
|
m_ShadowRadius: 0
|
||||||
|
m_ShadowAngle: 0
|
||||||
|
m_LightUnit: 1
|
||||||
|
m_LuxAtDistance: 1
|
||||||
|
m_EnableSpotReflector: 1
|
||||||
|
--- !u!4 &98592372
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 98592370}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
|
||||||
|
m_LocalPosition: {x: 0, y: 3, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
|
||||||
|
--- !u!1 &1056472736
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1056472737}
|
||||||
|
- component: {fileID: 1056472740}
|
||||||
|
- component: {fileID: 1056472739}
|
||||||
|
- component: {fileID: 1056472738}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: MainCamera
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &1056472737
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1056472736}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 2.126, z: -3.048}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 1388409560}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &1056472738
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1056472736}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: e3ecae0254451c2888cfe94f7a8e825d, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.ThirdPersonCamera
|
||||||
|
target: {fileID: 1388409560}
|
||||||
|
focusOffset: {x: 0, y: 1.6, z: 0}
|
||||||
|
mouseSensitivity: 180
|
||||||
|
minPitch: -35
|
||||||
|
maxPitch: 75
|
||||||
|
rotationSmoothTime: 0.1
|
||||||
|
distance: 5
|
||||||
|
minDistance: 2
|
||||||
|
maxDistance: 8
|
||||||
|
zoomSpeed: 4
|
||||||
|
distanceSmoothTime: 0.1
|
||||||
|
obstructionRadius: 0.25
|
||||||
|
obstructionMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 4294967295
|
||||||
|
lockCursor: 1
|
||||||
|
--- !u!81 &1056472739
|
||||||
|
AudioListener:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1056472736}
|
||||||
|
m_Enabled: 1
|
||||||
|
--- !u!20 &1056472740
|
||||||
|
Camera:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1056472736}
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 2
|
||||||
|
m_ClearFlags: 1
|
||||||
|
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
|
||||||
|
m_projectionMatrixMode: 1
|
||||||
|
m_GateFitMode: 2
|
||||||
|
m_FOVAxisMode: 0
|
||||||
|
m_Iso: 200
|
||||||
|
m_ShutterSpeed: 0.005
|
||||||
|
m_Aperture: 16
|
||||||
|
m_FocusDistance: 10
|
||||||
|
m_FocalLength: 50
|
||||||
|
m_BladeCount: 5
|
||||||
|
m_Curvature: {x: 2, y: 11}
|
||||||
|
m_BarrelClipping: 0.25
|
||||||
|
m_Anamorphism: 0
|
||||||
|
m_SensorSize: {x: 36, y: 24}
|
||||||
|
m_LensShift: {x: 0, y: 0}
|
||||||
|
m_NormalizedViewPortRect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 1
|
||||||
|
height: 1
|
||||||
|
near clip plane: 0.3
|
||||||
|
far clip plane: 1000
|
||||||
|
field of view: 60
|
||||||
|
orthographic: 0
|
||||||
|
orthographic size: 5
|
||||||
|
m_Depth: 0
|
||||||
|
m_CullingMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 4294967295
|
||||||
|
m_RenderingPath: -1
|
||||||
|
m_TargetTexture: {fileID: 0}
|
||||||
|
m_TargetDisplay: 0
|
||||||
|
m_TargetEye: 3
|
||||||
|
m_HDR: 1
|
||||||
|
m_AllowMSAA: 1
|
||||||
|
m_AllowDynamicResolution: 0
|
||||||
|
m_ForceIntoRT: 0
|
||||||
|
m_OcclusionCulling: 1
|
||||||
|
m_StereoConvergence: 10
|
||||||
|
m_StereoSeparation: 0.022
|
||||||
|
--- !u!1 &1124421571
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1124421575}
|
||||||
|
- component: {fileID: 1124421574}
|
||||||
|
- component: {fileID: 1124421573}
|
||||||
|
- component: {fileID: 1124421572}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Plane
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!64 &1124421572
|
||||||
|
MeshCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1124421571}
|
||||||
|
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!23 &1124421573
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1124421571}
|
||||||
|
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: 10303, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
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: 0
|
||||||
|
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_AdditionalVertexStreams: {fileID: 0}
|
||||||
|
--- !u!33 &1124421574
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1124421571}
|
||||||
|
m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!4 &1124421575
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1124421571}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 1.5986, y: 0, z: -0.95271}
|
||||||
|
m_LocalScale: {x: 5.3009, y: 1, z: 3.5896}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &1388409556 stripped
|
||||||
|
GameObject:
|
||||||
|
m_CorrespondingSourceObject: {fileID: 193682, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
m_PrefabInstance: {fileID: 2051203598}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
--- !u!114 &1388409557
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1388409556}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 9309e1c5110afc714b0b9b9d10323469, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::MegaKoop.Game.ThirdPersonCharacterController
|
||||||
|
moveSpeed: 5
|
||||||
|
acceleration: 12
|
||||||
|
deceleration: 18
|
||||||
|
rotationSharpness: 12
|
||||||
|
airControlPercent: 0.35
|
||||||
|
jumpHeight: 1.6
|
||||||
|
gravity: -20
|
||||||
|
groundedGravity: -5
|
||||||
|
cameraTransform: {fileID: 1056472737}
|
||||||
|
--- !u!143 &1388409558
|
||||||
|
CharacterController:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1388409556}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
|
m_ProvidesContacts: 0
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 3
|
||||||
|
m_Height: 2
|
||||||
|
m_Radius: 0.5
|
||||||
|
m_SlopeLimit: 45
|
||||||
|
m_StepOffset: 0.3
|
||||||
|
m_SkinWidth: 0.08
|
||||||
|
m_MinMoveDistance: 0.001
|
||||||
|
m_Center: {x: 0, y: 0.99, z: 0}
|
||||||
|
--- !u!4 &1388409560 stripped
|
||||||
|
Transform:
|
||||||
|
m_CorrespondingSourceObject: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
m_PrefabInstance: {fileID: 2051203598}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
--- !u!1 &1888194194
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1888194197}
|
||||||
|
- component: {fileID: 1888194196}
|
||||||
|
- component: {fileID: 1888194195}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Main Camera
|
||||||
|
m_TagString: MainCamera
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!81 &1888194195
|
||||||
|
AudioListener:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1888194194}
|
||||||
|
m_Enabled: 1
|
||||||
|
--- !u!20 &1888194196
|
||||||
|
Camera:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1888194194}
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 2
|
||||||
|
m_ClearFlags: 1
|
||||||
|
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
|
||||||
|
m_projectionMatrixMode: 1
|
||||||
|
m_GateFitMode: 2
|
||||||
|
m_FOVAxisMode: 0
|
||||||
|
m_Iso: 200
|
||||||
|
m_ShutterSpeed: 0.005
|
||||||
|
m_Aperture: 16
|
||||||
|
m_FocusDistance: 10
|
||||||
|
m_FocalLength: 50
|
||||||
|
m_BladeCount: 5
|
||||||
|
m_Curvature: {x: 2, y: 11}
|
||||||
|
m_BarrelClipping: 0.25
|
||||||
|
m_Anamorphism: 0
|
||||||
|
m_SensorSize: {x: 36, y: 24}
|
||||||
|
m_LensShift: {x: 0, y: 0}
|
||||||
|
m_NormalizedViewPortRect:
|
||||||
|
serializedVersion: 2
|
||||||
|
x: 0
|
||||||
|
y: 0
|
||||||
|
width: 1
|
||||||
|
height: 1
|
||||||
|
near clip plane: 0.3
|
||||||
|
far clip plane: 1000
|
||||||
|
field of view: 60
|
||||||
|
orthographic: 0
|
||||||
|
orthographic size: 5
|
||||||
|
m_Depth: -1
|
||||||
|
m_CullingMask:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 4294967295
|
||||||
|
m_RenderingPath: -1
|
||||||
|
m_TargetTexture: {fileID: 0}
|
||||||
|
m_TargetDisplay: 0
|
||||||
|
m_TargetEye: 3
|
||||||
|
m_HDR: 1
|
||||||
|
m_AllowMSAA: 1
|
||||||
|
m_AllowDynamicResolution: 0
|
||||||
|
m_ForceIntoRT: 0
|
||||||
|
m_OcclusionCulling: 1
|
||||||
|
m_StereoConvergence: 10
|
||||||
|
m_StereoSeparation: 0.022
|
||||||
|
--- !u!4 &1888194197
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1888194194}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 1, z: -10}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1001 &2051203598
|
||||||
|
PrefabInstance:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Modification:
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransformParent: {fileID: 0}
|
||||||
|
m_Modifications:
|
||||||
|
- target: {fileID: 193682, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_Name
|
||||||
|
value: Witch
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.x
|
||||||
|
value: 3.4360476
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalPosition.z
|
||||||
|
value: -2.4662037
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.w
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.x
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalRotation.z
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.x
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
propertyPath: m_LocalEulerAnglesHint.z
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
m_RemovedComponents: []
|
||||||
|
m_RemovedGameObjects:
|
||||||
|
- {fileID: 175166, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 118938, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 196212, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 148926, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 157446, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 127684, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 105362, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 113388, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 170868, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 100918, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 121842, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 143904, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 168556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 150778, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 110758, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 107294, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 122390, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
- {fileID: 171546, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
m_AddedGameObjects:
|
||||||
|
- targetCorrespondingSourceObject: {fileID: 455556, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
insertIndex: -1
|
||||||
|
addedObject: {fileID: 1056472737}
|
||||||
|
m_AddedComponents:
|
||||||
|
- targetCorrespondingSourceObject: {fileID: 193682, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
insertIndex: -1
|
||||||
|
addedObject: {fileID: 1388409558}
|
||||||
|
- targetCorrespondingSourceObject: {fileID: 193682, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
insertIndex: -1
|
||||||
|
addedObject: {fileID: 1388409557}
|
||||||
|
m_SourcePrefab: {fileID: 100100000, guid: b54daf0d815d7069abbc96eff093977e, type: 3}
|
||||||
|
--- !u!1660057539 &9223372036854775807
|
||||||
|
SceneRoots:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_Roots:
|
||||||
|
- {fileID: 1888194197}
|
||||||
|
- {fileID: 98592372}
|
||||||
|
- {fileID: 1124421575}
|
||||||
|
- {fileID: 2051203598}
|
||||||
7
Scenes/CharacterScene.unity.meta
Normal file
7
Scenes/CharacterScene.unity.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f7ec5dd369a7204f6b622d16da378f11
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
BIN
Scenes/CharacterScene/NavMesh-Plane.asset
Normal file
BIN
Scenes/CharacterScene/NavMesh-Plane.asset
Normal file
Binary file not shown.
8
Scenes/CharacterScene/NavMesh-Plane.asset.meta
Normal file
8
Scenes/CharacterScene/NavMesh-Plane.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 01f7d0e6f128a79a8ade52bc1643d6eb
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 23800000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user