Compare commits

...

2 Commits

Author SHA1 Message Date
70d0b8690e Merge branch 'master' of https://gitea.mareksorokin.cz/mrarake/megakoop 2025-10-05 18:23:27 +02:00
174a399ee7 Characters 2025-10-05 18:21:16 +02:00
77 changed files with 14406 additions and 0 deletions

8
Game.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3aa05ef999c0db8c9a37646187699217
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

32
Game/AGENTS.md Normal file
View 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 Unitys 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
View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 26880776f1200689dbc1be5c197a03c0
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Game/Enemy.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f250f7f11c39195b2a38e7f052154182
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Game/Enemy/Golem.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a9df8db4a9428a557a3f6f46b6652d79
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View 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
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: de06818812108c54f8cc6429c1370c05
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Game/Hero/Weapons.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cf250eac73dbba38d947df7a05910bc5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2f55c0b12239df279b90e7e62c02f093
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 6703b124cb13a577c8aae6a4851d0274
timeCreated: 1526434856
licenseType: Store
NativeFormatImporter:
mainObjectFileID: 100100000
userData:
assetBundleName:
assetBundleVariant:

View 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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 912de7c15ecf9d38d9be364bc0ac0f70
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View 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}

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fe75fe22781f92b369675fdfc9657f7d
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Game/Scripts.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e1468aaa4e1f2d7d08d7d6e9acb26be7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Game/Scripts/Combat.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 43cbd7a1d6f0054838d3503a2cc2b68f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7e3b00f0d0eff2b0e813e41895012e02

View 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();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 920077a2cfc4cde58a4ceeea4b96ebe9

View File

@@ -0,0 +1,9 @@
namespace MegaKoop.Game.Combat
{
public interface IDamageable
{
Team Team { get; }
bool IsAlive { get; }
void ApplyDamage(DamagePayload payload);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2044a5c09d25d50beb90c486ace32d50

View File

@@ -0,0 +1,9 @@
namespace MegaKoop.Game.Combat
{
public enum Team
{
Neutral = 0,
Heroes = 1,
Enemies = 2
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 620167bdb0a57e180b2bee989e9ee5d1

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: caf731eed00799f15a7315a669658012
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
using UnityEngine;
namespace MegaKoop.Game.Networking
{
public interface ICharacterInputSource
{
Vector2 MoveInput { get; }
bool JumpPressed { get; }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 403a3945388ae5082bf455368a91a800

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e3118c9c432a7acbd824645749251552

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e6f99ca4475ead269829ba672213d5c

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 28ade96a26da5d9f693df8a5a80b0970

View 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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0303a875e193063078b4557274bb3c5a

View 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.");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f741ec3a1d16461a98d02ec211293449

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a010df74dca8515c3af51a9fc598af17

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 83320aed3c99a87b692932447a34631e

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a55005e32d7c7fdd89d4c15872d0466d

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 468b0a865fb2a0b0181db1c60d4e0ea9

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 07c0ee7fbf4c1edaf9992cf139839928

View 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));
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c3923950963f6f6a98a81fda48267e6e

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dad61aa7b24bddb6b9add5a461263779

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e3ecae0254451c2888cfe94f7a8e825d

View 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);
}
}
}

View 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
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee0194cba591c20dfa044c4c9c8e595f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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.");
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5f85fa1035779f1bbba9cd75923e55d0

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e6e77e83dcc282364afb3428637c723c

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 44d32c675960e4dceb05fc13f06e43bd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 038cb7cb48b0cb10da7ebba4ba473c63

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2fd5ff95d6e2be5d09b6c054570cef0a

View 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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5590d430717d95f878b7929af7bf28ff

View File

@@ -1,4 +1,5 @@
{
"version": 1,
"name": "InputSystem_Actions",
"maps": [
{

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d254ee0417feae63888d6c9b45dcb33e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

691
Scenes/CharacterScene.unity Normal file
View 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}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f7ec5dd369a7204f6b622d16da378f11
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 01f7d0e6f128a79a8ade52bc1643d6eb
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant: