Author SHA1 Message Date
MrTorex e654a0daf4 Иду делать твиа :( 2025-11-20 10:23:33 +03:00
MrTorex cfe5b39cc9 Добавлены модельки/материалы деда, мобы переделаны под меши 2025-11-20 10:06:45 +03:00
MrTorex c14880457d Merge branch 'mobs-dev' into GraphicsModelsGrossGlossBeuaty 2025-11-20 08:43:00 +03:00
MrTorex 344470978d страдание 2025-11-20 08:41:37 +03:00
MrTorex dab970737a Добавлен конфиг для фога
Завтра будут спавнпоинты + модельки
2025-11-20 08:34:20 +03:00
MrTorex afdd523682 добавлены конфиги для дурнев 2025-11-19 20:11:24 +03:00
MrTorex 575e66425a грязюка 2025-11-19 17:56:53 +03:00
MrTorex c470f63409 я ебаный бездарь 2025-11-19 14:46:16 +03:00
ThomasCharester 5da64a7b3d Меню, дверь, цвет 2025-11-19 14:45:45 +03:00
ThomasCharester c4ab11488b Ещё модели 2025-11-19 12:53:09 +03:00
ThomasCharester cb968d782b Освещение, модели, текстуры 2025-11-18 20:07:45 +03:00
ThomasCharester 522f51be3f Текстуры модели и пр 2025-11-18 15:02:12 +03:00
MrTorex 7024e1daf0 темп 2025-11-18 12:27:35 +03:00
MrTorex 020934e07f Инишиал мобы 2025-11-17 14:45:51 +03:00
ThomasCharester d3fbd4c5c3 Артём хуесос 2025-11-15 19:11:30 +03:00
ThomasCharester 5b09068b0c Merge branch 'SoundManager' into PlayerAnimator 2025-11-15 18:31:13 +03:00
ThomasCharester ddb76a5b93 Игрок теперь играется 2025-11-15 18:30:31 +03:00
ThomasCharester 0f708ff4cb Merge branch 'movement-dev' into PlayerAnimator 2025-11-15 17:14:16 +03:00
ThomasCharester 721d5ebf9c Небольшие изменения, реально незначительные 2025-11-15 14:02:16 +03:00
MrTorex 3bb99aaf1e Фикс приколов с камерой 2025-11-15 13:59:32 +03:00
MrTorex a2acea39dc temp 2025-11-15 13:33:44 +03:00
ThomasCharester 5c1fd1510d Merge branch 'SoundManager' into Dev 2025-11-15 01:15:01 +03:00
ThomasCharester e128ee59f9 Чистка мусора 2025-11-15 01:14:11 +03:00
ThomasCharester 5c81bde5db Приемлемое воспроизведение музыки 2025-11-15 01:12:33 +03:00
ThomasCharester 05449642d5 Merge branch 'SoundManager' into Dev 2025-11-14 23:10:31 +03:00
ThomasCharester 3020140360 Мусор - 2025-11-14 23:07:55 +03:00
ThomasCharester e67fea2051 В инфраструктуре начался сущий кошмар 2025-11-14 22:21:51 +03:00
MrTorex d1aad440f5 Осталось пофиксить толка бак с камерай 2025-11-14 15:02:33 +03:00
MrTorex 0e1c274677 Продолжение багфикса 2025-11-14 13:12:49 +03:00
ThomasCharester 346dc97fef Музыка есть 2025-11-13 23:13:41 +03:00
ThomasCharester 557c4c3dee Audio система в процессе 2025-11-13 12:31:43 +03:00
MrTorex dbabd847c1 bugfix no 1 2025-11-12 20:04:06 +03:00
MrTorex 874280741c Допил наклонов полноценный 2025-11-11 19:48:37 +03:00
ThomasCharester b13cf8cfc0 Дебажный менеджер музыки 2025-11-11 17:39:04 +03:00
MrTorex 177464cc09 Я ЛЮБЛЮ ПУЛ 2025-11-11 15:00:49 +03:00
MrTorex f8448ba922 just for syncing changes 2025-11-10 22:16:36 +03:00
ThomasCharester 0f8a629e4b Перекинул энтерстейт 2025-11-08 18:06:12 +03:00
ThomasCharester 7bc15cc03d Перекинул энтерстейт 2025-11-03 16:45:41 +03:00
ThomasCharester d861e06caf Фикс утечки памяти. Первой наверное... 2025-11-03 12:54:45 +03:00
ThomasCharester f940cbffe0 Теперь Addressables и UniTask в InstantiateAsync и прочих процессах 2025-11-03 12:31:26 +03:00
ThomasCharester 6b1f24b72e Addressables in process 2025-11-02 23:29:29 +03:00
ThomasCharester 1ad87e7432 Unitask добавлен 2025-11-02 18:02:45 +03:00
ThomasCharester 09219d9091 Этот интерфейс... Добавил инструменты и заготовки 2025-11-02 17:31:45 +03:00
ThomasCharester 37f5d15410 Диспетчер в процессе 2025-11-01 11:34:34 +03:00
ThomasCharester 019da8cfd3 Экран смерти, смерть, бабки, Интерфейс, Шлюхи 2025-11-01 01:45:46 +03:00
ThomasCharester edf3821c0d Межинтерфейс 2025-11-01 00:54:24 +03:00
ThomasCharester 45fe19be24 Интерфейс в процессе 2025-10-31 23:19:16 +03:00
ThomasCharester bea109175b Допил интерфейса, здоровья, денег 2025-10-30 21:13:42 +03:00
MrTorex b214bb0f63 Инишиал механика наклона при залазании на стены
Нуждается в доработке (как минимум чтобы вращалось в правильную сторону), но вроде-как выглядит прикольно
2025-10-29 20:09:39 +03:00
ThomasCharester 0ac5ba1e1c Интерфейс прокачан 2025-10-29 20:06:37 +03:00
ThomasCharester ab9d444072 Система здоровья 2025-10-29 16:02:05 +03:00
ThomasCharester 2335908a90 Какие штуки 2025-10-29 16:01:46 +03:00
ThomasCharester ae76edb0d0 Сделан сервис UI и заготовки панелек 2025-10-29 16:01:18 +03:00
ThomasCharester 6f2b063700 Небольшой фикс инфраструктуры 2025-10-29 15:57:27 +03:00
MrTorex 0e188efe59 Инишиал механика наклона при залазании на стены
Нуждается в доработке (как минимум чтобы вращалось в правильную сторону), но вроде-как выглядит прикольно
2025-10-29 15:00:25 +03:00
ThomasCharester c6d1c3bffd Static camera state, Scene environments 2025-10-29 14:13:09 +03:00
MrTorex 07ce27618c Перенос всех магических чисел в градусы 2025-10-29 12:11:03 +03:00
MrTorex fb85b22e96 Вынос настроек персонажа в конфиг
В который сейчас будет добавлено пару новых настроек
2025-10-29 11:17:27 +03:00
MrTorex 665114b7dc Соединение всё со всем что имеется, дописывание стейт-машины
Теперь мне удобно кастить и получать тип камеры, всё вроде работает нормисно
2025-10-29 01:02:02 +03:00
ThomasCharester 4549617275 Теперь камера - это стейт-машина 2025-10-28 22:11:46 +03:00
ThomasCharester db541fed8a Фикс hardcrash а при старте 2025-10-28 22:11:02 +03:00
ThomasCharester 085429d5ca Небольшая перепись камеры 2025-10-28 22:10:45 +03:00
ThomasCharester 981d081bed Переполох в камере, насрано 2025-10-28 22:10:25 +03:00
MrTorex 6148889fc1 Merge branch 'movement-dev' of https://github.com/ThomasCharester/CourierOld into movement-dev 2025-10-28 12:54:03 +03:00
MrTorex 7538ce81af Допил конфига, переделаны комментарии
Чисто для удобства, чтобы не было много грязюки
2025-10-28 12:42:17 +03:00
MrTorex 9a3743988d Завершение рефакторинга, фикс бага с запросом слайда
Теперь всё поделено по логическим классам, чтобы было помодульно, а также если выполняется запрос на слайд в момент, когда слайд не может быть выполнен, запрос отменяется
2025-10-27 18:16:24 +03:00
621 changed files with 41197 additions and 1805 deletions

No files matched your search

@@ -0,0 +1,17 @@
%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: dea69d41f90c6ea4fa55c27c1d60c145, type: 3}
m_Name: AddressableAssetGroupSortSettings
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.GroupSchemas.AddressableAssetGroupSortSettings
sortOrder:
- 7e9d70e73e500eb9287683466ebf646f
- d447fc60305c17a17bf20948ab295a45
@@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 6938984fc7d25285abb4922dd46d9272 guid: 88c15d5f41d582974a9b7218c493e4a7
NativeFormatImporter: NativeFormatImporter:
externalObjects: {} externalObjects: {}
mainObjectFileID: 11400000 mainObjectFileID: 11400000
@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: d447fc60305c17a17bf20948ab295a45 m_DefaultGroup: d447fc60305c17a17bf20948ab295a45
m_currentHash: m_currentHash:
serializedVersion: 2 serializedVersion: 2
Hash: 00000000000000000000000000000000 Hash: adba03da8c5086ecbcf5b8db72996dd1
m_OptimizeCatalogSize: 0 m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0 m_BuildRemoteCatalog: 0
m_CatalogRequestsTimeout: 0 m_CatalogRequestsTimeout: 0
@@ -54,12 +54,13 @@ MonoBehaviour:
m_Id: m_Id:
m_RemoteCatalogLoadPath: m_RemoteCatalogLoadPath:
m_Id: m_Id:
m_ContentStateBuildPathProfileVariableName: m_ContentStateBuildPathProfileVariableName: <default settings path>
m_CustomContentStateBuildPath: m_CustomContentStateBuildPath:
m_ContentStateBuildPath: m_ContentStateBuildPath:
m_BuildAddressablesWithPlayerBuild: 0 m_BuildAddressablesWithPlayerBuild: 0
m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]' m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]'
m_GroupAssets: m_GroupAssets:
- {fileID: 11400000, guid: ba51c0b85beab267d8f41a72df4fb657, type: 2}
- {fileID: 11400000, guid: 7c592a6445170e8379910601b962cb30, type: 2} - {fileID: 11400000, guid: 7c592a6445170e8379910601b962cb30, type: 2}
m_BuildSettings: m_BuildSettings:
m_LogResourceManagerExceptions: 1 m_LogResourceManagerExceptions: 1
@@ -0,0 +1,21 @@
%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: bbb281ee3bf0b054c82ac2347e9e782c, type: 3}
m_Name: Audio
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.AddressableAssetGroup
m_GroupName: Audio
m_GUID: 7e9d70e73e500eb9287683466ebf646f
m_SerializeEntries: []
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 2588e84eb64800d0781c574a37fa48d8, type: 2}
m_SchemaSet:
m_Schemas: []
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ba51c0b85beab267d8f41a72df4fb657
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -14,7 +14,177 @@ MonoBehaviour:
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.AddressableAssetGroup m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.AddressableAssetGroup
m_GroupName: Default Local Group m_GroupName: Default Local Group
m_GUID: d447fc60305c17a17bf20948ab295a45 m_GUID: d447fc60305c17a17bf20948ab295a45
m_SerializeEntries: [] m_SerializeEntries:
- m_GUID: 03e51c504fa7e0624ba855ec35b60bcc
m_Address: absolutely
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 111011400ce13ae859c52c757aef859e
m_Address: ahfreeman
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 144ab5188f8ad7a0a92c75703bd12e60
m_Address: launch_upmenu1
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1d7110a045075b22fa27d62bd5f2784e
m_Address: 357_shot1
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 1f79c7b1e492a4ea2a8d666cdead0c52
m_Address: Audio/Dispatcher/take_damage4
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2301400160a23924eb27c4bdbe797739
m_Address: 'Audio/Music/Nightmargin (Oneshot OST) - The Prophecy '
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 23e11d2c8a4f12f4ea9a7a43d1118464
m_Address: Gameplay
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 29bff8011973e4e519906b9da8fca0fc
m_Address: Audio/Dispatcher/late3
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 29ee630aab7bbef8799dc76ed0c239f7
m_Address: Audio/Dispatcher/late4
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2ed2d93a9f39d4edba511cb7396c7bb6
m_Address: Prefabs/Enemies/Cockroach
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2f6134357dc2a5641a846a7478ce4c2c
m_Address: bravo!
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 31f16c866d3224ab8a7e0fabf3745ec5
m_Address: Audio/Dispatcher/late5
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 3831a36e0c8e48f659517a6bb99378b6
m_Address: cbar_hit1
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 48aebfe8826dc0df79d047b5afbf8f5b
m_Address: Canvas
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4bd64d1fcecc9c6b09d81e78b7dcfebe
m_Address: Audio/Dispatcher/take_damage1
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 735f38402ea347fa381408293503a24e
m_Address: Audio/Dispatcher/receiving_an_order1
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 7c5caf3ceee44f71588860c5c1fbdbe0
m_Address: Audio/Dispatcher/take_damage3
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 803e40b883c823b89a91aa8b9d9fc483
m_Address: valve_sound
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 81844a99f5b786b41b831174f8fb6d3a
m_Address: Prefabs/Player/CharacterController
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 82c71411174bfa86ca1f676b9d389f94
m_Address: bastard!
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8c2be26d359d54063825fedfad7a493b
m_Address: Audio/Dispatcher/late2
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 8fa89a492fc3a3768a0f0ba1bb7080e0
m_Address: Audio/Dispatcher/late1
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 9293380a88bf6736889be29722b94269
m_Address: Audio/AudioSource
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: a306df57f348ce7cf900d0b9431739ee
m_Address: launch_selectmenu
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b2c9f83c47783069aabb564f68cb0cbb
m_Address: absolutelynot
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b2e8dc2aeb0311cd8a826b51cf8e94dc
m_Address: GameOver
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: b33855c694de89386a25c56cbe463a0e
m_Address: Prefabs/Enemies/DrunkenGrandpa
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: c566fc932edefbbee9e53271eac3c560
m_Address: MainMenu
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: cab4b1ff78dd6b1fba61aa165aa17b4d
m_Address: Audio/Dispatcher/receiving_an_order2
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: ebe18293a5ef86704b23dad3a1786476
m_Address: Half-Life01
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f23b9fce9594618e5b75845f1cae50de
m_Address: LoadingScreen
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f3d7dc64b34919ad7b50a3dd307a935b
m_Address: Prefabs/Enemies/Fog
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f4b6fa54ecac6cc418f98824491aa17a
m_Address: Audio/Music/SANABI - JUSTICE Is Watching You
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f74a409bf11b25148ad305acc05b14f5
m_Address: Audio/Dispatcher/take_damage2
m_ReadOnly: 0
m_SerializedLabels: []
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0 m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 2588e84eb64800d0781c574a37fa48d8, type: 2} m_Settings: {fileID: 11400000, guid: 2588e84eb64800d0781c574a37fa48d8, type: 2}
m_SchemaSet: m_SchemaSet:
@@ -0,0 +1,28 @@
%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: 7e3976da977cb49238499ea3b4c237ae, type: 3}
m_Name: ProfileDataSourceSettings
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.ProfileDataSourceSettings
profileGroupTypes:
- m_GroupTypePrefix: Built-In
m_Variables:
- m_Suffix: BuildPath
m_Value: '[UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]'
- m_Suffix: LoadPath
m_Value: '{UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]'
environments: []
currentEnvironment:
id:
projectId:
projectGenesisId:
name:
isDefault: 0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7e2b0e48c9649284a9541c4d1dad59c2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,12 @@
using Code.App.StateMachine;
using Code.StateMachine;
namespace Code.App.AppStates
{
public class BootstrapState : BaseState<AppStateMachineContext>
{
public BootstrapState(AppStateMachineContext context) : base(context)
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0183e73a08c64332babded3df3d06b95
timeCreated: 1762122962
@@ -1,5 +1,8 @@
using Code.App.StateMachine; using Code.App.StateMachine;
using Code.Scenes;
using Code.StateMachine; using Code.StateMachine;
using Code.UI;
using UnityEngine;
namespace Code.App.AppStates namespace Code.App.AppStates
{ {
@@ -7,17 +10,32 @@ namespace Code.App.AppStates
{ {
public GameOverState(AppStateMachineContext context) : base(context) public GameOverState(AppStateMachineContext context) : base(context)
{ } { }
private float damnTimer = 3f; // TODO Удалить при первой возможности
public override void EnterState() public override void EnterState()
{ {
Context.UIService.SwitchPanelTo(UIPanel.GameOver);
base.EnterState(); base.EnterState();
} }
private void ToMainMenu()
{
Context.StateMachine.SwitchState(AppState.MainMenu)
.AllowStateTransition(AppState.Gameplay);
}
public override void Tick() public override void Tick()
{ } {
damnTimer -= Time.deltaTime;
if (damnTimer <= float.Epsilon) ToMainMenu();
}
public override void ExitState() public override void ExitState()
{ {
damnTimer = 5f;
Context.UIService.LoadingScreen.Show();
base.ExitState(); base.ExitState();
} }
} }
@@ -1,47 +1,160 @@
using Code.App.StateMachine; using Code.App.StateMachine;
using Code.Audio;
using Code.Gameplay.Camera; using Code.Gameplay.Camera;
using Code.Gameplay.Camera.CameraStates;
using Code.Gameplay.Character.Core;
using Code.Gameplay.Player; using Code.Gameplay.Player;
using Code.Input; using Code.Input;
using Code.Scenes; using Code.Scenes;
using Code.Scenes.SceneEnvironments;
using Code.StateMachine; using Code.StateMachine;
using Code.UI; using Code.UI;
using Cysharp.Threading.Tasks;
using Reflex.Extensions;
using UnityEngine; using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
namespace Code.App.AppStates namespace Code.App.AppStates
{ {
public class GameplayState : BaseState<AppStateMachineContext> public class GameplayState : BaseState<AppStateMachineContext>
{ {
private PlayerHealth _playerHealthPoints; // DEBUG
private Transform _testSFXTransform;
// DEBUG
private PlayerHealth _playerHealth;
private PlayerMoney _playerMoney;
private CharacterProvider _playerCharacter;
public GameplayState(AppStateMachineContext context) : base(context) public GameplayState(AppStateMachineContext context) : base(context)
{ {
} }
public override void EnterState() public override void EnterState()
{ {
Context.SceneLoaderService.LoadScene((int)SceneIds.MovementDev, SceneLoaded); Context.UIService.LoadingScreen.Show();
_playerMoney = Context.PlayerFactory.CreatePlayerMoney();
//_playerHealth = Context.PlayerProvider.Health;
//_playerHealth.OnDeath += EndGame;
Context.AudioService.TransitToSnapshot(AudioSnapshotState.Peaceful);
Context.InputService.EnableMap(ActionMappings.Gameplay); Context.InputService.EnableMap(ActionMappings.Gameplay);
Context.UIService.LoadingScreen.Show(); Context.InputService.DebugBindAction((int)ActionMappings.Debug, (int)DebugActions.Debug1, GiveHealth);
Context.UIService.SwitchPanelTo(UIPanel.Gameplay); Context.InputService.DebugBindAction((int)ActionMappings.Debug, (int)DebugActions.Debug3, GiveMoney);
Context.InputService.DebugBindAction((int)ActionMappings.Debug, (int)DebugActions.Debug2, LooseHealth);
Context.CameraStateMachine.SwitchState(CameraMode.FirstPerson); Context.InputService.DebugBindAction((int)ActionMappings.Debug, (int)DebugActions.Debug4, LooseMoney);
Context.InputService.DebugBindAction((int)ActionMappings.Debug, (int)DebugActions.Debug5, PlayDebugSFX);
Context.InputService.DebugBindAction((int)ActionMappings.Debug, (int)DebugActions.Debug6, PlayDebugDispatcher);
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
WaitForSceneToLoad();
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
} }
private void SceneLoaded() private async UniTask WaitForPlayerCharacterLoad() => _playerCharacter = await Context.PlayerFactory.CreatePlayer();
private async UniTaskVoid WaitForSceneToLoad()
{ {
Context.UIService.LoadingScreen.Hide(); await UniTask.WhenAll(
Context.SceneLoaderService.LoadSceneAsyncAwaitable((int)SceneIds.Gameplay),
Context.UIService.AsyncSwitchPanelTo(UIPanel.Gameplay),
Context.AudioFactory.WarmGameplayMusic(),
Context.AudioFactory.WarmSFX(),
WaitForPlayerCharacterLoad()
);
base.EnterState(); _playerHealth = Context.PlayerProvider.Health;
_playerHealth.OnDeath += EndGame;
Context.AudioService.PlayRandomMusicUntilHumanityWillBeDestroyedForTheySins(MusicType.Peaceful);
Context.AudioService.PlayRandomMusicUntilHumanityWillBeDestroyedForTheySins(MusicType.Action);
var sceneEnvironment = ((GameplaySceneEnvironment)SceneManager.GetActiveScene()
.GetSceneContainer().Resolve<CurrentSceneEnvironment>());
_testSFXTransform = sceneEnvironment.debugSfxTarget;
_playerCharacter.Teleport(sceneEnvironment.playerSpawnPoint.position, sceneEnvironment.playerSpawnPoint.rotation);
((GameplayPanel)Context.UIService.CurrentPanel).Initialize(_playerMoney, _playerHealth);
Context.CameraStateMachine.SwitchState(CameraMode.FirstPerson);
((BaseCameraState)Context.CameraStateMachine.CurrentBaseState).SetFollowTransform(_playerCharacter.GetFollowTransform());
Context.InputService.EnableMap(ActionMappings.Gameplay);
Context.UIService.LoadingScreen.Hide();
} }
private void OnCharacterSpawn(CharacterProvider characterProvider)
{
((BaseCameraState)Context.CameraStateMachine.CurrentBaseState).SetFollowTransform(_playerCharacter.GetFollowTransform());
}
private void PlayDebugSFX(InputAction.CallbackContext context)
{
Context.AudioService.PlaySFX(SFXSounds.Crowbar, _testSFXTransform.position);
}
private void PlayDebugDispatcher(InputAction.CallbackContext context)
{
Context.AudioService.PlayDispatcher(DispatcherSounds.AhFreeman);
}
private void GiveHealth(InputAction.CallbackContext context)
{
_playerHealth.GetDamage(-1);
}
private void LooseHealth(InputAction.CallbackContext context)
{
_playerHealth.GetDamage(1);
}
private void GiveMoney(InputAction.CallbackContext context)
{
_playerMoney.GiveMoney(100);
((GameplayPanel)(Context.UIService.CurrentPanel)).SummonDispatcher();
}
private void LooseMoney(InputAction.CallbackContext context)
{
_playerMoney.LooseMoney(100);
((GameplayPanel)(Context.UIService.CurrentPanel)).CloseDispatcher();
}
// DEBUG
public override void Tick() public override void Tick()
{ {
} }
private void EndGame()
{
Context.StateMachine.SwitchState(AppState.GameOver)
.AllowStateTransition(AppState.MainMenu);
}
public override void ExitState() public override void ExitState()
{ {
_playerHealth = null;
_playerMoney = null;
Context.PlayerFactory.ReleasePlayer(_playerCharacter);
_playerCharacter = null;
Context.InputService.DisableMap(ActionMappings.Gameplay); Context.InputService.DisableMap(ActionMappings.Gameplay);
Context.AudioService.GladlyAskToStopMusic(MusicType.Action, callback: Context.AudioFactory.ReleaseActionMusic);
Context.AudioService.GladlyAskToStopMusic(MusicType.Peaceful, callback: Context.AudioFactory.ReleasePeacefulMusic);
Context.InputService.DebugUnbindAction((int)ActionMappings.Debug, (int)DebugActions.Debug1, GiveHealth);
Context.InputService.DebugUnbindAction((int)ActionMappings.Debug, (int)DebugActions.Debug3, GiveHealth);
Context.InputService.DebugUnbindAction((int)ActionMappings.Debug, (int)DebugActions.Debug2, LooseHealth);
Context.InputService.DebugUnbindAction((int)ActionMappings.Debug, (int)DebugActions.Debug4, LooseMoney);
Context.InputService.DebugUnbindAction((int)ActionMappings.Debug, (int)DebugActions.Debug5, PlayDebugSFX);
Context.InputService.DebugUnbindAction((int)ActionMappings.Debug, (int)DebugActions.Debug6, PlayDebugDispatcher);
base.ExitState(); base.ExitState();
} }
@@ -1,44 +1,76 @@
using Code.App.StateMachine; using Code.App.StateMachine;
using Code.Audio;
using Code.Gameplay.Camera; using Code.Gameplay.Camera;
using Code.Gameplay.Camera.CameraStates;
using Code.Gameplay.Door;
using Code.Input; using Code.Input;
using Code.Scenes; using Code.Scenes;
using Code.Scenes.SceneEnvironments;
using Code.StateMachine; using Code.StateMachine;
using Code.UI; using Code.UI;
using Code.UI.Panels;
using Cysharp.Threading.Tasks;
using Reflex.Extensions;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement;
namespace Code.App.AppStates namespace Code.App.AppStates
{ {
public class MainMenuState : BaseState<AppStateMachineContext> public class MainMenuState : BaseState<AppStateMachineContext>
{ {
public MainMenuState(AppStateMachineContext context) : base(context) public MainMenuState(AppStateMachineContext context) : base(context)
{ } {
}
public override void EnterState() public override void EnterState()
{ {
Context.SceneLoaderService.LoadScene((int)SceneIds.MainMenu, SceneLoaded);
Context.InputService.EnableMap(ActionMappings.MainMenu);
Context.UIService.LoadingScreen.Show(); Context.UIService.LoadingScreen.Show();
Context.UIService.SwitchPanelTo(UIPanel.MainMenu);
((MainMenuPanel)Context.UIService.CurrentPanel).startButton.onClick.AddListener(StartGame); Context.CameraStateMachine.SwitchState(CameraMode.UltraStatic);
((MainMenuPanel)Context.UIService.CurrentPanel).exitButton.onClick.AddListener(ExitGame);
Context.CameraStateMachine.SwitchState(CameraMode.Static);
}
private void SceneLoaded() #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
WaitForSceneToLoad();
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
private CinematicDoorOpening _damnDoor = new();
private async UniTaskVoid WaitForSceneToLoad()
{ {
await UniTask.WhenAll(
Context.SceneLoaderService.LoadSceneAsyncAwaitable((int)SceneIds.MainMenu),
Context.UIService.AsyncSwitchPanelTo(UIPanel.MainMenu),
Context.AudioFactory.WarmMenuMusic()
);
Context.AudioService.TransitToSnapshot(AudioSnapshotState.Menu);
Context.AudioService.PlayRandomMusicUntilHumanityWillBeDestroyedForTheySins(MusicType.Menu);
var hui = ((MainMenuEnvironment)SceneManager.GetActiveScene()
.GetSceneContainer().Resolve<CurrentSceneEnvironment>());
_damnDoor.DamnDoor = hui.damnDoor;
_damnDoor.DamnDoor.SetPhysics(false);
_damnDoor.DoorLight = hui.doorLight;
var menuPanel = (MainMenuPanel)Context.UIService.CurrentPanel;
menuPanel.startButton.button.onClick.AddListener(StartGame);
menuPanel.exitButton.button.onClick.AddListener(ExitGame);
menuPanel.CinematicDoorOpening = _damnDoor;
((UltraStaticState)Context.CameraStateMachine.CurrentBaseState).SetFollowTransform(hui.cameraPosition);
Context.UIService.LoadingScreen.Hide(); Context.UIService.LoadingScreen.Hide();
base.EnterState(); Context.InputService.EnableMap(ActionMappings.MainMenu);
} }
public override void Tick() public override void Tick()
{ } {
}
private void StartGame() private void StartGame()
{ {
Context.StateMachine.SwitchState(AppState.Gameplay) Context.StateMachine.SwitchState(AppState.Gameplay)
.AllowStateTransition(AppState.MainMenu)
.AllowStateTransition(AppState.GameOver); .AllowStateTransition(AppState.GameOver);
} }
@@ -46,10 +78,11 @@ namespace Code.App.AppStates
{ {
Application.Quit(); Application.Quit();
} }
public override void ExitState() public override void ExitState()
{ {
Context.InputService.DisableMap(ActionMappings.MainMenu); Context.InputService.DisableMap(ActionMappings.MainMenu);
Context.AudioService.GladlyAskToStopMusic(MusicType.Menu, callback: Context.AudioFactory.ReleaseMenuMusic);
base.ExitState(); base.ExitState();
} }
@@ -1,4 +1,5 @@
using Code.App.StateMachine; using Code.App.StateMachine;
using Code.Gameplay.Camera;
using Code.Input; using Code.Input;
using Code.Scenes; using Code.Scenes;
using Code.StateMachine; using Code.StateMachine;
@@ -14,8 +15,10 @@ namespace Code.App.AppStates
public override void EnterState() public override void EnterState()
{ {
Context.SceneLoaderService.LoadScene((int)SceneIds.MovementDev, SceneLoaded); // Context.SceneLoaderService.LoadScene((int)SceneIds.MovementDev, SceneLoaded);
Context.InputService.EnableMap(ActionMappings.Gameplay); Context.InputService.EnableMap(ActionMappings.Gameplay);
Context.CameraStateMachine.SwitchState(CameraMode.FirstPerson);
} }
private void SceneLoaded() private void SceneLoaded()
@@ -1,11 +1,15 @@
using Code.App.StateMachine; using Code.App.StateMachine;
using Code.Audio;
using Code.Configs; using Code.Configs;
using Code.Constants;
using Code.Gameplay.Camera; using Code.Gameplay.Camera;
using Code.Gameplay.Character.Camera; using Code.Gameplay.Player;
using Code.Input; using Code.Input;
using Code.Scenes; using Code.Scenes;
using Code.StaticData; using Code.StaticData;
using Code.UI; using Code.UI;
using Code.UI.Panels;
using Cysharp.Threading.Tasks;
using Reflex.Core; using Reflex.Core;
using Reflex.Injectors; using Reflex.Injectors;
using UnityEngine; using UnityEngine;
@@ -15,7 +19,6 @@ namespace Code.App
{ {
public class BootstrapInstaller : MonoBehaviour, IInstaller public class BootstrapInstaller : MonoBehaviour, IInstaller
{ {
public void InstallBindings(ContainerBuilder containerBuilder) public void InstallBindings(ContainerBuilder containerBuilder)
{ {
InstallStaticData(containerBuilder, out IStaticDataService staticDataService); InstallStaticData(containerBuilder, out IStaticDataService staticDataService);
@@ -23,10 +26,16 @@ namespace Code.App
InstallInfrastructureUnit(containerBuilder, staticDataService); InstallInfrastructureUnit(containerBuilder, staticDataService);
containerBuilder.AddSingleton(typeof(InputService), typeof(IInputService)); containerBuilder.AddSingleton(typeof(InputService), typeof(IInputService));
containerBuilder.AddSingleton(typeof(UIService), typeof(IUIService)); containerBuilder.AddSingleton(typeof(PlayerFactory), typeof(IPlayerFactory));
containerBuilder.AddSingleton(typeof(PlayerProvider), typeof(IPlayerProvider));
InitializeCamera(containerBuilder, staticDataService); InitializeCamera(containerBuilder, staticDataService);
containerBuilder.AddSingleton(typeof(UIFactory), typeof(IUIFactory));
containerBuilder.AddSingleton(typeof(UIService), typeof(IUIService));
containerBuilder.AddSingleton(typeof(AudioFactory), typeof(IAudioFactory));
containerBuilder.AddSingleton(typeof(AudioService), typeof(IAudioService));
containerBuilder.AddSingleton(typeof(SceneLoaderService), typeof(ISceneLoaderService)); containerBuilder.AddSingleton(typeof(SceneLoaderService), typeof(ISceneLoaderService));
containerBuilder.OnContainerBuilt += Bootstrap; containerBuilder.OnContainerBuilt += Bootstrap;
@@ -34,11 +43,8 @@ namespace Code.App
private void InitializeCamera(ContainerBuilder containerBuilder, IStaticDataService staticDataService) private void InitializeCamera(ContainerBuilder containerBuilder, IStaticDataService staticDataService)
{ {
// CameraService cameraService = Instantiate(staticDataService.CameraConfig.cameraPrefab); Camera camera = Instantiate(staticDataService.CameraConfig.cameraPrefab);
// containerBuilder.AddSingleton(cameraService);
// DontDestroyOnLoad(cameraService);
Camera camera = Instantiate(staticDataService.CameraConfig.cameraPrefab2);
DontDestroyOnLoad(camera); DontDestroyOnLoad(camera);
containerBuilder.AddSingleton(camera); containerBuilder.AddSingleton(camera);
} }
@@ -50,7 +56,7 @@ namespace Code.App
containerBuilder.AddSingleton(infrastructureUnit); containerBuilder.AddSingleton(infrastructureUnit);
containerBuilder.AddSingleton(infrastructureUnit.AddComponent<CoroutineRunner>(), containerBuilder.AddSingleton(infrastructureUnit.AddComponent<CoroutineRunner>(),
typeof(ICoroutineRunner)); typeof(ICoroutineRunner));
containerBuilder.AddSingleton(infrastructureUnit.AddComponent<AppStateMachine>()); containerBuilder.AddSingleton(infrastructureUnit.AddComponent<AppStateMachine>());
containerBuilder.AddSingleton(infrastructureUnit.AddComponent<CameraStateMachine>()); containerBuilder.AddSingleton(infrastructureUnit.AddComponent<CameraStateMachine>());
} }
@@ -61,26 +67,56 @@ namespace Code.App
staticDataService.Load(); staticDataService.Load();
containerBuilder.AddSingleton(staticDataService, typeof(IStaticDataService)); containerBuilder.AddSingleton(staticDataService, typeof(IStaticDataService));
containerBuilder.AddSingleton(staticDataService.InputConfig.inputActionAsset, typeof(InputActionAsset)); containerBuilder.AddSingleton(staticDataService.InputConfig.inputActionAsset, typeof(InputActionAsset));
containerBuilder.AddSingleton(staticDataService.UIConfig.canvasPrefab, typeof(Canvas)); containerBuilder.AddSingleton(staticDataService.InputConfig, typeof(InputConfig));
containerBuilder.AddSingleton(staticDataService.CameraConfig, typeof(CameraConfig)); containerBuilder.AddSingleton(staticDataService.CameraConfig, typeof(CameraConfig));
containerBuilder.AddSingleton(staticDataService.UIConfig.uiElementsPrefabs, typeof(UIElement[]));
containerBuilder.AddSingleton(staticDataService.PlayerConfig, typeof(PlayerConfig));
containerBuilder.AddSingleton(staticDataService.CharacterConfig, typeof(CharacterConfig));
containerBuilder.AddSingleton(staticDataService.UIConfig, typeof(UIConfig));
containerBuilder.AddSingleton(staticDataService.DrunkenGrandpaConfig, typeof(DrunkenGrandpaConfig));
containerBuilder.AddSingleton(staticDataService.CockroachConfig, typeof(CockroachConfig));
containerBuilder.AddSingleton(staticDataService.FogConfig, typeof(FogConfig));
containerBuilder.AddSingleton(staticDataService.AudioMixerConfig, typeof(AudioMixerConfig));
containerBuilder.AddSingleton(staticDataService.SoundtrackListConfig, typeof(SoundtrackListConfig));
containerBuilder.AddSingleton(staticDataService.SFXListConfig, typeof(SFXListConfig));
containerBuilder.AddSingleton(staticDataService.DispatcherSoundListConfig, typeof(DispatcherSoundListConfig));
containerBuilder.AddSingleton(staticDataService.EnemiesSoundListConfig, typeof(EnemiesSoundListConfig));
} }
private AppStateMachine _appStateMachine;
private void Bootstrap(Container container) private void Bootstrap(Container container)
{ {
AppStateMachine appStateMachine = container.Resolve<AppStateMachine>(); _appStateMachine = container.Resolve<AppStateMachine>();
appStateMachine.Initialize(); _appStateMachine.Initialize();
AttributeInjector.Inject(_appStateMachine.StateMachineContext, container);
AttributeInjector.Inject(appStateMachine.StateMachineContext, container);
CameraStateMachine cameraStateMachine = container.Resolve<CameraStateMachine>(); CameraStateMachine cameraStateMachine = container.Resolve<CameraStateMachine>();
AttributeInjector.Inject(cameraStateMachine.StateMachineContext, container); AttributeInjector.Inject(cameraStateMachine.StateMachineContext, container);
cameraStateMachine.Initialize(); cameraStateMachine.Initialize();
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
BootstrapFinish(container);
// TODO: поменяно для теста #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
appStateMachine }
private async UniTaskVoid BootstrapFinish(Container container)
{
var loadingScreen = GameObject.FindWithTag(Tags.LoadingScreen);
DontDestroyOnLoad(loadingScreen);
await UniTask.WhenAll(
container.Resolve<IUIService>().Initialize(loadingScreen.GetComponent<LoadingScreen>()),
container.Resolve<IAudioService>().Initialize()
);
_appStateMachine
.SwitchState(AppState.MainMenu) .SwitchState(AppState.MainMenu)
.AllowStateTransition(AppState.Gameplay); .AllowStateTransition(AppState.Gameplay);
} }
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c7a271c6319540abb78d561f392e1396
timeCreated: 1761743570
@@ -0,0 +1,26 @@
using Code.Scenes;
using Code.Scenes.SceneEnvironments;
using Reflex.Core;
using UnityEngine;
namespace Code.App.SceneInstallers
{
public class GameplaySceneInstaller : MonoBehaviour, IInstaller
{
[SerializeField] private GameplaySceneEnvironment _gameplaySceneEnvironment;
public void InstallBindings(ContainerBuilder containerBuilder)
{
containerBuilder.AddScoped(typeof(GameplaySceneEnvironment), typeof(CurrentSceneEnvironment));
containerBuilder.OnContainerBuilt += Initialize;
}
private void Initialize(Container container)
{
GameplaySceneEnvironment env = (GameplaySceneEnvironment)container.Resolve<CurrentSceneEnvironment>();
env.TransitVariables(_gameplaySceneEnvironment);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 88584fd4361b46f6a7326440cd95ce69
timeCreated: 1763144580
@@ -0,0 +1,27 @@
using Code.Scenes;
using Code.Scenes.SceneEnvironments;
using Reflex.Core;
using UnityEngine;
using UnityEngine.Serialization;
namespace Code.App.SceneInstallers
{
public class MainMenuSceneInstaller : MonoBehaviour, IInstaller
{
[SerializeField] private MainMenuEnvironment _mainMenuEnvironment;
public void InstallBindings(ContainerBuilder containerBuilder)
{
containerBuilder.AddScoped(typeof(MainMenuEnvironment), typeof(CurrentSceneEnvironment));
containerBuilder.OnContainerBuilt += Initialize;
}
private void Initialize(Container container)
{
MainMenuEnvironment env = (MainMenuEnvironment)container.Resolve<CurrentSceneEnvironment>();
env.TransitVariables(_mainMenuEnvironment);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 04145a046c584b66818ca25745652e66
timeCreated: 1761743587
@@ -7,6 +7,7 @@ namespace Code.App.StateMachine
{ {
public override void Initialize() public override void Initialize()
{ {
States.Add(AppState.Bootstrap, new BootstrapState(StateMachineContext));
States.Add(AppState.MainMenu, new MainMenuState(StateMachineContext)); States.Add(AppState.MainMenu, new MainMenuState(StateMachineContext));
States.Add(AppState.Gameplay, new GameplayState(StateMachineContext)); States.Add(AppState.Gameplay, new GameplayState(StateMachineContext));
States.Add(AppState.GameOver, new GameOverState(StateMachineContext)); States.Add(AppState.GameOver, new GameOverState(StateMachineContext));
@@ -16,6 +17,8 @@ namespace Code.App.StateMachine
AllowedStates |= AppState.MainMenu; AllowedStates |= AppState.MainMenu;
AllowedStates |= AppState.Gameplay; AllowedStates |= AppState.Gameplay;
AllowedStates |= AppState.MovementDev; AllowedStates |= AppState.MovementDev;
CurrentBaseState = States[AppState.Bootstrap];
} }
protected override bool CheckTransitionAllowed(AppState nextState) => AllowedStates.HasFlag(nextState); protected override bool CheckTransitionAllowed(AppState nextState) => AllowedStates.HasFlag(nextState);
@@ -1,4 +1,6 @@
using Code.Audio;
using Code.Gameplay.Camera; using Code.Gameplay.Camera;
using Code.Gameplay.Player;
using Code.Input; using Code.Input;
using Code.Scenes; using Code.Scenes;
using Code.UI; using Code.UI;
@@ -8,10 +10,14 @@ namespace Code.App.StateMachine
{ {
public class AppStateMachineContext public class AppStateMachineContext
{ {
[Inject] public ISceneLoaderService SceneLoaderService; [Inject] public ISceneLoaderService SceneLoaderService { get; private set; }
[Inject] public IInputService InputService; [Inject] public IInputService InputService { get; private set; }
[Inject] public IUIService UIService; [Inject] public AppStateMachine StateMachine { get; private set; }
[Inject] public AppStateMachine StateMachine; [Inject] public CameraStateMachine CameraStateMachine { get; private set; }
[Inject] public CameraStateMachine CameraStateMachine; [Inject] public IUIService UIService { get; private set; }
[Inject] public IPlayerFactory PlayerFactory { get; private set; }
[Inject] public IPlayerProvider PlayerProvider { get; private set; }
[Inject] public IAudioFactory AudioFactory { get; private set; }
[Inject] public IAudioService AudioService { get; private set; }
} }
} }
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a1937581573e47844bb593bd62310b89
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,23 @@
namespace Code.Audio
{
public enum SFXSounds
{
Crowbar,
Shot357
}
public enum UISounds
{
Crowbar
}
public enum EnemiesSounds
{
Bastard,
Bravo
}
public enum DispatcherSounds
{
Absolutely,
AbsolutelyNot,
AhFreeman
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 907b506ceac843ebb509a8e5298af540
timeCreated: 1762888870
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using Code.Configs;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Audio;
using UnityEngine.ResourceManagement.AsyncOperations;
using Object = UnityEngine.Object;
namespace Code.Audio
{
public class AudioFactory : IAudioFactory
{
public AudioMixer AudioMixer { get; private set; }
private AssetReference _audioSourcePrefab;
private AssetReference[] _peacefulSoundtrack;
private AssetReference[] _actionSoundtrack;
private AssetReference[] _menuSoundtrack;
private AssetReference[] _SFX;
private AssetReference[] _enemies;
private AssetReference[] _dispatcher;
private AssetReference[] _character;
private AsyncOperationHandle<IList<AudioClip>> _menuMusicHandle;
private AsyncOperationHandle<IList<AudioClip>> _actionMusicHandle;
private AsyncOperationHandle<IList<AudioClip>> _peacefulMusicHandle;
private AsyncOperationHandle<IList<AudioClip>> _SFXHandle;
private AsyncOperationHandle<IList<AudioClip>> _enemiesHandle;
private AsyncOperationHandle<IList<AudioClip>> _dispatcherHandle;
public Dictionary<MusicType, List<AudioClip>> MusicDictionary { get; private set; } = new();
public List<AudioClip> SFXAudios { get; private set; } = new();
public List<AudioClip> EnemiesAudios { get; private set; } = new();
public List<AudioClip> DispatcherAudios { get; private set; } = new();
public AudioFactory(AudioMixerConfig audioMixerConfig,
SoundtrackListConfig soundtrackListConfig,
SFXListConfig sfxListConfig,
EnemiesSoundListConfig enemiesSoundListConfig,
DispatcherSoundListConfig dispatcherSoundListConfig
)
{
AudioMixer = audioMixerConfig.audioMixerAsset;
_audioSourcePrefab = audioMixerConfig.audioSourcePrefab;
_peacefulSoundtrack = soundtrackListConfig.peacefulSoundtrack;
_actionSoundtrack = soundtrackListConfig.actionSoundtrack;
_menuSoundtrack = soundtrackListConfig.menuSoundtrack;
_SFX = sfxListConfig.SFX;
_enemies = enemiesSoundListConfig.enemies;
_dispatcher = dispatcherSoundListConfig.dispatcher;
MusicDictionary.Add(MusicType.Menu, new ());
MusicDictionary.Add(MusicType.Peaceful, new ());
MusicDictionary.Add(MusicType.Action, new ());
}
public async UniTask LoadAudioSourcePrefab(Action<GameObject> callback) =>
callback?.Invoke(await Addressables.LoadAssetAsync<GameObject>(_audioSourcePrefab));
public async UniTask WarmGameplayMusic( )
{
_actionMusicHandle = Addressables.LoadAssetsAsync<AudioClip>(_actionSoundtrack, OnActionTrackLoaded, Addressables.MergeMode.None);
_peacefulMusicHandle = Addressables.LoadAssetsAsync<AudioClip>(_peacefulSoundtrack, OnPeacefulTrackLoaded, Addressables.MergeMode.None);
await UniTask.WhenAll(_actionMusicHandle.ToUniTask(), _peacefulMusicHandle.ToUniTask());
}
public void ReleaseActionMusic()
{
MusicDictionary[MusicType.Action].Clear();
_actionMusicHandle.Release();
}
public void ReleasePeacefulMusic()
{
MusicDictionary[MusicType.Peaceful].Clear();
_peacefulMusicHandle.Release();
}
public async UniTask WarmMenuMusic( )
{
_menuMusicHandle = Addressables.LoadAssetsAsync<AudioClip>(_menuSoundtrack, OnMenuTrackLoaded, Addressables.MergeMode.None);
// foreach (var assetReference in _menuSoundtrack) MenuMusic.Add(await assetReference.LoadAssetAsync<AudioClip>());
await _menuMusicHandle.ToUniTask();
}
public void ReleaseMenuMusic()
{
MusicDictionary[MusicType.Menu].Clear();
// foreach (var assetReference in _menuSoundtrack) assetReference.ReleaseAsset();
_menuMusicHandle.Release();
}
public async UniTask WarmSFX( )
{
_SFXHandle = Addressables.LoadAssetsAsync<AudioClip>(_SFX, OnSFXLoaded, Addressables.MergeMode.None);
await _SFXHandle.ToUniTask();
}
public void ReleaseSFX()
{
SFXAudios.Clear();
_SFXHandle.Release();
}
public async UniTask WarmEnemies( )
{
_enemiesHandle = Addressables.LoadAssetsAsync<AudioClip>(_enemies, OnEnemiesLoaded, Addressables.MergeMode.None);
await _enemiesHandle.ToUniTask();
}
public void ReleaseEnemies()
{
EnemiesAudios.Clear();
_enemiesHandle.Release();
}
public async UniTask WarmDispatcher( )
{
_dispatcherHandle = Addressables.LoadAssetsAsync<AudioClip>(_dispatcher, OnDispatcherLoaded, Addressables.MergeMode.None);
await _dispatcherHandle.ToUniTask();
}
public void ReleaseDispatcher()
{
DispatcherAudios.Clear();
_dispatcherHandle.Release();
}
private void OnMenuTrackLoaded(AudioClip audioClip)
{
MusicDictionary[MusicType.Menu].Add(audioClip);
}
private void OnActionTrackLoaded(AudioClip audioClip)
{
MusicDictionary[MusicType.Action].Add(audioClip);
}
private void OnPeacefulTrackLoaded(AudioClip audioClip)
{
MusicDictionary[MusicType.Peaceful].Add(audioClip);
}
private void OnSFXLoaded(AudioClip audioClip)
{
SFXAudios.Add(audioClip);
}
private void OnEnemiesLoaded(AudioClip audioClip)
{
EnemiesAudios.Add(audioClip);
}
private void OnDispatcherLoaded(AudioClip audioClip)
{
DispatcherAudios.Add(audioClip);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5a4b48740d194d4dafa1732f96d71687
timeCreated: 1762626915
@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Code.Configs;
using Code.Constants;
using Cysharp.Threading.Tasks;
using Reflex.Attributes;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Audio;
using Random = UnityEngine.Random;
namespace Code.Audio
{
public class AudioService : IAudioService
{
private IAudioFactory _audioFactory;
private AudioMixer _mixer;
private AudioSource _actionMusicSource;
private AudioSource _peacefulMusicSource;
private AudioMixerGroup _masterGroup,
// _musicGroup,
// _actionMusicGroup,
// _peacefulMusicGroup,
_SFXGroup,
_creaturesGroup,
_dispatcherGroup,
_courierGroup,
_enemiesGroup;
private readonly AudioSourcePool _audioSourcePool = new();
private Dictionary<AudioSnapshotState, AudioMixerSnapshot> _snapshots = new();
public AudioService(IAudioFactory audioFactory)
{
_audioFactory = audioFactory;
}
public async UniTask Initialize()
{
// _mixer = await _audioFactory.CreateMixer();
_mixer = _audioFactory.AudioMixer;
_masterGroup = _mixer.FindMatchingGroups(AudioGroups.MasterGroup)[0];
// _actionMusicGroup = _mixer.FindMatchingGroups(AudioGroups.ActionMusicGroup)[0];
// _peacefulMusicGroup = _mixer.FindMatchingGroups(AudioGroups.PeacefulMusoup)[0];
// _musicGroup = _mixer.FindMatchingGroups(AudioGroups.MusicGroup)[0];
_musicAudioGroups.Add(MusicType.Menu, _mixer.FindMatchingGroups(AudioGroups.MenuMusicGroup)[0]);
_musicAudioGroups.Add(MusicType.Action, _mixer.FindMatchingGroups(AudioGroups.ActionMusicGroup)[0]);
_musicAudioGroups.Add(MusicType.Peaceful, _mixer.FindMatchingGroups(AudioGroups.PeacefulMusicGroup)[0]);
_cancellationTokenSources.Add(MusicType.Menu, new());
_cancellationTokenSources.Add(MusicType.Action, new());
_cancellationTokenSources.Add(MusicType.Peaceful, new());
_musicAudioSources.Add(MusicType.Menu, null);
_musicAudioSources.Add(MusicType.Action, null);
_musicAudioSources.Add(MusicType.Peaceful, null);
_SFXGroup = _mixer.FindMatchingGroups(AudioGroups.SFXGroup)[0];
_creaturesGroup = _mixer.FindMatchingGroups(AudioGroups.CreaturesGroup)[0];
_dispatcherGroup = _mixer.FindMatchingGroups(AudioGroups.DispatcherMusicGroup)[0];
_courierGroup = _mixer.FindMatchingGroups(AudioGroups.CourierMusicGroup)[0];
_enemiesGroup = _mixer.FindMatchingGroups(AudioGroups.EnemiesMusicGroup)[0];
_snapshots[AudioSnapshotState.None] = _mixer.FindSnapshot(AudioGroups.NoneSnapshot);
_snapshots[AudioSnapshotState.Peaceful] = _mixer.FindSnapshot(AudioGroups.PeacefulSnapshot);
_snapshots[AudioSnapshotState.Action] = _mixer.FindSnapshot(AudioGroups.ActionSnapshot);
_snapshots[AudioSnapshotState.Menu] = _mixer.FindSnapshot(AudioGroups.MenuSnapshot);
await _audioFactory.LoadAudioSourcePrefab(_audioSourcePool.Initialize);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
private Dictionary<MusicType, CancellationTokenSource> _cancellationTokenSources = new();
private Dictionary<MusicType, AudioSource> _musicAudioSources = new();
private Dictionary<MusicType, AudioMixerGroup> _musicAudioGroups = new();
public async UniTaskVoid PlayRandomMusicUntilHumanityWillBeDestroyedForTheySins(MusicType musicType)
{
if(!_musicAudioSources[musicType])
{
_musicAudioSources[musicType] = _audioSourcePool.GetSource();
_musicAudioSources[musicType].spatialBlend = 0f;
_musicAudioSources[musicType].outputAudioMixerGroup = _musicAudioGroups[musicType];
}
_musicAudioSources[musicType].resource =
_audioFactory.MusicDictionary[musicType][
Random.Range(0, _audioFactory.MusicDictionary[musicType].Count)];
_musicAudioSources[musicType].Play();
// await UniTask.WaitUntilValueChanged(_musicAudioSources[musicType],
// _ => !_musicAudioSources[musicType].isPlaying,
// cancellationToken: _cancellationTokenSources[musicType].Token);
await UniTask.NextFrame();
// Debug.Log($"Playing {musicType}");
var (isCanceled, _) = await UniTask.WaitUntilValueChanged(_musicAudioSources[musicType],
_ => !_musicAudioSources[musicType].isPlaying,
cancellationToken: _cancellationTokenSources[musicType].Token).SuppressCancellationThrow();
// Debug.Log($"Finished {musicType}");
if (isCanceled)
{
// Debug.Log($"Cancelled {musicType}");
_cancellationTokenSources[musicType].Dispose();
_cancellationTokenSources[musicType] = new();
_audioSourcePool.ReleaseSource(_musicAudioSources[musicType]);
_musicAudioSources[musicType] = null;
return;
}
PlayRandomMusicUntilHumanityWillBeDestroyedForTheySins(musicType);
}
public async UniTaskVoid GladlyAskToStopMusic(MusicType musicType, AudioSnapshotState audioSnapshotState = AudioSnapshotState.None, float time = 1f, Action callback = null)
{
TransitToSnapshot(audioSnapshotState, time);
await UniTask.WaitForSeconds(time);
_cancellationTokenSources[musicType].Cancel();
callback?.Invoke();
}
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
public void StopMusic()
{
_peacefulMusicSource.Stop();
_actionMusicSource.Stop();
}
public void GladlyAskToStopMusic(Action callback = null)
{
TransitToSnapshot(AudioSnapshotState.None);
_peacefulMusicSource.Stop();
_actionMusicSource.Stop();
callback?.Invoke();
}
public void TransitToSnapshot(AudioSnapshotState state, float time = 1f)
{
_snapshots[state].TransitionTo(time);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
public void PlaySFX(SFXSounds sfxId, Vector3 position)
{
_audioSourcePool.PlayOneShot(_audioFactory.SFXAudios[(int)sfxId], position, _SFXGroup);
}
public void PlayEnemy(EnemiesSounds enemySoundId, Vector3 position)
{
_audioSourcePool.PlayOneShot(_audioFactory.EnemiesAudios[(int)enemySoundId], position, _enemiesGroup);
}
public void PlayDispatcher(DispatcherSounds dispatcherSoundId)
{
_audioSourcePool.PlayShizo(_audioFactory.DispatcherAudios[(int)dispatcherSoundId], _dispatcherGroup);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bf44cb051308463494bd930f2a3d31e4
timeCreated: 1762625073
@@ -0,0 +1,119 @@
using System.Collections.Generic;
using Code.Constants;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Serialization;
namespace Code.Audio
{
public class AudioServiceDebug : MonoBehaviour
{
[SerializeField] private AudioMixer mixer;
private AudioSource _actionMusicSource;
private AudioSource _peacefulMusicSource;
[SerializeField] private AudioSource prefab;
[SerializeField] private AudioResource[] peacefulAudio;
[SerializeField] private AudioResource[] actionAudio;
private AudioMixerGroup _masterGroup,
_musicGroup,
_actionMusicGroup,
_peacefulMusicGroup,
_SFXGroup,
_creaturesGroup,
_dispatcherGroup,
_courierGroup,
_enemiesGroup;
private readonly AudioSourcePool _audioSourcePool = new();
private Dictionary<AudioSnapshotState, AudioMixerSnapshot> _snapshots = new();
[FormerlySerializedAs("sfxAudio")] [SerializeField] private AudioClip[] sfxAudios;
[SerializeField] private AudioClip[] enemiesAudios;
[SerializeField] private AudioClip[] dispatcherAudios;
private void MixerCreated()
{
_masterGroup = mixer.FindMatchingGroups(AudioGroups.MasterGroup)[0];
_actionMusicGroup = mixer.FindMatchingGroups(AudioGroups.ActionMusicGroup)[0];
_peacefulMusicGroup = mixer.FindMatchingGroups(AudioGroups.PeacefulMusicGroup)[0];
_SFXGroup = mixer.FindMatchingGroups(AudioGroups.SFXGroup)[0];
_musicGroup = mixer.FindMatchingGroups(AudioGroups.MusicGroup)[0];
_creaturesGroup = mixer.FindMatchingGroups(AudioGroups.CreaturesGroup)[0];
_dispatcherGroup = mixer.FindMatchingGroups(AudioGroups.DispatcherMusicGroup)[0];
_courierGroup = mixer.FindMatchingGroups(AudioGroups.CourierMusicGroup)[0];
_enemiesGroup = mixer.FindMatchingGroups(AudioGroups.EnemiesMusicGroup)[0];
_snapshots[AudioSnapshotState.Peaceful] = mixer.FindSnapshot(AudioGroups.PeacefulSnapshot);
_snapshots[AudioSnapshotState.Action] = mixer.FindSnapshot(AudioGroups.ActionSnapshot);
// _audioSourcePool.Initialize(10,prefab);
InitializeMusicSources();
}
private void InitializeMusicSources()
{
_actionMusicSource = _audioSourcePool.GetSource();
_actionMusicSource.outputAudioMixerGroup = _actionMusicGroup;
_actionMusicSource.spatialBlend = 0f;
_peacefulMusicSource = _audioSourcePool.GetSource();
_peacefulMusicSource.outputAudioMixerGroup = _peacefulMusicGroup;
_peacefulMusicSource.spatialBlend = 0f;
}
private void Start()
{
MixerCreated();
}
public void PlayRandomGameplayMusic()
{
_peacefulMusicSource.resource = peacefulAudio[Random.Range(0, peacefulAudio.Length)];
_actionMusicSource.resource = actionAudio[Random.Range(0, actionAudio.Length)];
_peacefulMusicSource.Play();
_actionMusicSource.Play();
}
public void TransitToSnapshot(AudioSnapshotState state)
{
_snapshots[state].TransitionTo(1f);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
public void PlaySFX(SFXSounds sfxId, Vector3 position)
{
_audioSourcePool.PlayOneShot(sfxAudios[(int)sfxId], position, _SFXGroup);
}
public void PlayDebugSFX()
{
_audioSourcePool.PlayOneShot(sfxAudios[(int)SFXSounds.Crowbar], transform.position, _SFXGroup);
}
public void PlayEnemy(EnemiesSounds enemySoundId, Vector3 position)
{
_audioSourcePool.PlayOneShot(enemiesAudios[(int)enemySoundId], position, _enemiesGroup);
}
public void PlayDebugEnemySFX()
{
_audioSourcePool.PlayOneShot(enemiesAudios[(int)EnemiesSounds.Bastard], transform.position, _enemiesGroup);
}
public void PlayDispatcher(DispatcherSounds dispatcherSoundId)
{
_audioSourcePool.PlayShizo(dispatcherAudios[(int)dispatcherSoundId], _dispatcherGroup);
}
public void PlayDebugDispatcherSFX()
{
_audioSourcePool.PlayShizo(dispatcherAudios[(int)DispatcherSounds.AhFreeman], _dispatcherGroup);
}
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a42cad8f28f54a53bef23d9313014abd
timeCreated: 1762805867
@@ -0,0 +1,10 @@
namespace Code.Audio
{
public enum AudioSnapshotState
{
None,
Peaceful,
Action,
Menu
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 29adaf8e912649d9b91c09735e36840a
timeCreated: 1762808301
@@ -0,0 +1,84 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Pool;
namespace Code.Audio
{
public class AudioSourcePool
{
// The pool holds plain GameObjects (you can swap this for any component type).
private IObjectPool<AudioSource> _pool;
private GameObject _sourcePrefab;
public void Initialize(GameObject sourcePrefab)
{
_sourcePrefab = sourcePrefab;
// Create a pool with the four core callbacks.
_pool = new ObjectPool<AudioSource>(
createFunc: CreateItem,
// actionOnGet: OnGet,
actionOnRelease: OnRelease,
actionOnDestroy: OnDestroyItem,
collectionCheck: true, // helps catch double-release mistakes
defaultCapacity: 10,
maxSize: 20
);
}
// Creates a new pooled GameObject the first time (and whenever the pool needs more).
private AudioSource CreateItem()
{
AudioSource audioSource = Object.Instantiate(_sourcePrefab).GetComponent<AudioSource>();
audioSource.name = "PooledAudioSource";
Object.DontDestroyOnLoad(audioSource);
return audioSource;
}
// Called when an item is taken from the pool.
private void OnGet(AudioSource source)
{
//source.gameObject.SetActive(true);
}
// Called when an item is returned to the pool.
private void OnRelease(AudioSource source)
{
source.Stop();
//source.gameObject.SetActive(false);
}
// Called when the pool decides to destroy an item (e.g., above max size).
private void OnDestroyItem(AudioSource source)
{
Object.Destroy(source.gameObject);
}
public async UniTaskVoid PlayOneShot(AudioClip clip, Vector3 position, AudioMixerGroup audioMixerGroup)
{
AudioSource source = _pool.Get();
source.PlayOneShot(clip);
source.spatialBlend = 1f;
source.transform.position = position;
source.outputAudioMixerGroup = audioMixerGroup;
await UniTask.WaitUntilValueChanged(source, x => x.isPlaying);
_pool.Release(source);
}
public async UniTaskVoid PlayShizo(AudioClip clip, AudioMixerGroup audioMixerGroup)
{
AudioSource source = _pool.Get();
source.PlayOneShot(clip);
source.spatialBlend = 0f;
source.outputAudioMixerGroup = audioMixerGroup;
await UniTask.WaitUntilValueChanged(source, x => x.isPlaying);
_pool.Release(source);
}
public AudioSource GetSource() => _pool.Get();
public void ReleaseSource(AudioSource source) => _pool.Release(source);
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 370b6a43a74744ff9d90a99991b104f7
timeCreated: 1762852042
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Audio;
namespace Code.Audio
{
public interface IAudioFactory
{
List<AudioClip> SFXAudios { get; }
List<AudioClip> EnemiesAudios { get; }
List<AudioClip> DispatcherAudios { get; }
AudioMixer AudioMixer { get; }
Dictionary<MusicType, List<AudioClip>> MusicDictionary { get; }
UniTask LoadAudioSourcePrefab(Action<GameObject> callback);
UniTask WarmGameplayMusic();
void ReleaseActionMusic();
UniTask WarmMenuMusic();
void ReleaseMenuMusic();
UniTask WarmSFX();
void ReleaseSFX();
UniTask WarmEnemies();
void ReleaseEnemies();
UniTask WarmDispatcher();
void ReleaseDispatcher();
void ReleasePeacefulMusic();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea837e1ca08a4846b7f4ce2df771bf65
timeCreated: 1762804793
@@ -0,0 +1,19 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace Code.Audio
{
public interface IAudioService
{
UniTask Initialize();
UniTaskVoid PlayRandomMusicUntilHumanityWillBeDestroyedForTheySins(MusicType musicType);
UniTaskVoid GladlyAskToStopMusic(MusicType musicType, AudioSnapshotState audioSnapshotState = AudioSnapshotState.None, float time = 1f, Action callback = null);
void StopMusic();
void GladlyAskToStopMusic(Action callback = null);
void TransitToSnapshot(AudioSnapshotState state, float time = 1f);
void PlaySFX(SFXSounds sfxId, Vector3 position);
void PlayEnemy(EnemiesSounds enemySoundId, Vector3 position);
void PlayDispatcher(DispatcherSounds dispatcherSoundId);
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 30521fadcce84848ae88363e915bceb4
timeCreated: 1762804728
@@ -0,0 +1,9 @@
namespace Code.Audio
{
public enum MusicType
{
Menu,
Peaceful,
Action
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 74ff08d5f1b741f69e37fc06fb07ae8d
timeCreated: 1763151897
@@ -0,0 +1,13 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Audio;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Audio/Mixer", fileName = "AudioMixerConfig")]
public class AudioMixerConfig : ScriptableObject
{
public AudioMixer audioMixerAsset;
public AssetReference audioSourcePrefab;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 353cee8946ca43efbbcda905d7b32ae1
timeCreated: 1762624627
@@ -1,11 +1,9 @@
using Code.Gameplay.Camera;
using Code.Gameplay.Character.Camera;
using Code.Gameplay.Character.Core;
using UnityEngine; using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs namespace Code.Configs
{ {
[CreateAssetMenu(fileName = "CharacterCameraConfig", menuName = "Config/CharacterCameraConfig")] [CreateAssetMenu(fileName = "CameraConfig", menuName = "Config/CameraConfig")]
public class CameraConfig : ScriptableObject public class CameraConfig : ScriptableObject
{ {
public float public float
@@ -31,13 +29,10 @@ namespace Code.Configs
maxVerticalAngle = 70f, // Максимальный угол наклона камеры (вверх) maxVerticalAngle = 70f, // Максимальный угол наклона камеры (вверх)
defaultVerticalAngle = 10f; // Стандартный угол наклона камеры defaultVerticalAngle = 10f; // Стандартный угол наклона камеры
// Текущий режим камеры // Позиция следования по умолчанию
public CameraMode defaultCameraMode = CameraMode.Bogdan;
// Позиция следования по умолчанию (для меню)
public Vector3 defaultFollowPosition = Vector3.zero; public Vector3 defaultFollowPosition = Vector3.zero;
public CameraService cameraPrefab; // Префаб камеры
public Camera cameraPrefab2; public Camera cameraPrefab;
} }
} }
@@ -0,0 +1,41 @@
using UnityEngine;
namespace Code.Configs
{
[CreateAssetMenu(fileName = "CharacterConfig", menuName = "Config/CharacterConfig")]
public class CharacterConfig : ScriptableObject
{
// Основное движение по земле
public float
maxStableMoveSpeed = 8f, // Максимальная скорость движения по устойчивой поверхности
stableMovementSharpness = 15f, // Резкость изменения скорости движения (чем выше - тем резче)
orientationSharpness = 10f, // Резкость изменения вращения персонажа (чем выше - тем резче)
jumpSpeed = 10f; // Скорость прыжка
// Спринт
public float
sprintMoveSpeed = 12f, // Максимальная скорость спринта
sprintAngleThreshold = 90f; // Максимальный угол (в градусах) от направления вперёд, при котором разрешён спринт
// Слайд
public float
slideSpeed = 20f, // Скорость слайда
slideDuration = 0.8f, // Длительность слайда в секундах
slideCooldown = 1.5f; // Время перезарядки слайда в секундах
// Залазание на стены
public float
wallClimbSpeedFactor = 0.8f, // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену
minWallClimbSpeed = 9f, // Минимальная горизонтальная скорость для начала залазания на стену
wallDetectionAngleThreshold = 80f; // Максимальный угол (в градусах) от вертикали, при котором поверхность считается "стеной"
// Наклон при залазании на стены
public float
maxWallLeanAngle = 45f, // Максимальный угол наклона от стены (в градусах)
wallLeanSharpness = 50f, // Скорость наклона/выпрямления (чем выше — тем резче)
wallLeanHeightScale = 0.1f; // Множитель: на сколько метров подъёма = 1 единица "наклона"
// Гравитация
public Vector3 gravity = new(0f, -30f, 0f); // Вектор, отвечающий за применение гравитации
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 10cfaf8e57f64eddb8c2eda147e4739a
timeCreated: 1761723608
@@ -0,0 +1,11 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Audio/CharacterSoundList", fileName = "CharacterSoundListConfig")]
public class CharacterSoundListConfig : ScriptableObject
{
public AssetReference[] character;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 83304a98bbdc4e7c9c7c643653ae1368
timeCreated: 1762625438
@@ -0,0 +1,29 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Enemies/Cockroach", fileName = "CockroachConfig")]
public class CockroachConfig : ScriptableObject
{
public AssetReference cockroachPrefab;
public int damage = 2;
public float attackCooldown = 1.5f;
public float detectionRange = 20f;
public float attackRange = 1.3f;
public float speed = 6f;
public bool isStunnable;
public float stunDuration;
public bool usesIdleMovement = true;
public float idleWanderRadius = 3f;
public float idleMoveDurationMin = 0.3f;
public float idleMoveDurationMax = 1f;
public float idlePauseDurationMin = 0.1f;
public float idlePauseDurationMax = 0.8f;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7bc162d3127949aa950d0738beee99a4
timeCreated: 1763571220
@@ -0,0 +1,12 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Audio/DispatcherSoundList", fileName = "DispatcherSoundListConfig")]
public class DispatcherSoundListConfig : ScriptableObject
{
public AssetReference[] dispatcher;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8a6f6ffb8f5242899791719801791945
timeCreated: 1762625431
@@ -0,0 +1,29 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Enemies/DrunkenGrandpa", fileName = "DrunkenGrandpaConfig")]
public class DrunkenGrandpaConfig : ScriptableObject
{
public AssetReference drunkenGrandpaPrefab;
public int damage = 2;
public float attackCooldown = 5f;
public float detectionRange = 10f;
public float attackRange = 1.5f;
public float speed = 3f;
public bool isStunnable = true;
public float stunDuration = 3f;
public bool usesIdleMovement = true;
public float idleWanderRadius = 5f;
public float idleMoveDurationMin = 1f;
public float idleMoveDurationMax = 5f;
public float idlePauseDurationMin = 3f;
public float idlePauseDurationMax = 5f;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4d157e8891074b90957a7434cff0ae71
timeCreated: 1763569096
@@ -0,0 +1,11 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Audio/EnemiesSoundList", fileName = "EnemiesSoundListConfig")]
public class EnemiesSoundListConfig : ScriptableObject
{
public AssetReference[] enemies;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e73ac7f7e3cd4db8ada95f63d73c2d1d
timeCreated: 1762625425
@@ -0,0 +1,29 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Enemies/Fog", fileName = "FogConfig")]
public class FogConfig : ScriptableObject
{
public AssetReference fogPrefab;
public int damage = int.MaxValue;
public float attackCooldown;
public float detectionRange;
public float attackRange = 5f;
public float speed = 1.5f;
public bool isStunnable;
public float stunDuration;
public bool usesIdleMovement;
public float idleWanderRadius;
public float idleMoveDurationMin;
public float idleMoveDurationMax;
public float idlePauseDurationMin;
public float idlePauseDurationMax;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aa4dd2a5cce24902a2918488b5751f2f
timeCreated: 1763581410
@@ -1,4 +1,5 @@
using UnityEngine; using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs namespace Code.Configs
{ {
@@ -6,5 +7,7 @@ namespace Code.Configs
public class PlayerConfig : ScriptableObject public class PlayerConfig : ScriptableObject
{ {
public int maxHealth; public int maxHealth;
public AssetReference characterPrefab;
} }
} }
@@ -0,0 +1,11 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Audio/SFXList", fileName = "SFXListConfig")]
public class SFXListConfig : ScriptableObject
{
public AssetReference[] SFX;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: be49de18297645009c1e2092f382a670
timeCreated: 1762625417
@@ -0,0 +1,13 @@
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Code.Configs
{
[CreateAssetMenu(menuName = "Config/Audio/SoundtrackList", fileName = "SoundtrackListConfig")]
public class SoundtrackListConfig : ScriptableObject
{
public AssetReference[] peacefulSoundtrack;
public AssetReference[] actionSoundtrack;
public AssetReference[] menuSoundtrack;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6d4b28e4552d456d99550b6d91467345
timeCreated: 1762624980
@@ -1,13 +1,12 @@
using Code.UI;
using UnityEngine; using UnityEngine;
using UnityEngine.Serialization; using UnityEngine.AddressableAssets;
namespace Code.Configs namespace Code.Configs
{ {
[CreateAssetMenu(menuName = "Config/UI", fileName = "UIConfig")] [CreateAssetMenu(menuName = "Config/UI", fileName = "UIConfig")]
public class UIConfig : ScriptableObject public class UIConfig : ScriptableObject
{ {
[FormerlySerializedAs("uiElements")] public UIElement[] uiElementsPrefabs; public AssetReference[] uiPanelsAssetReferences;
[FormerlySerializedAs("canvas")] public Canvas canvasPrefab; public AssetReference canvasAssetReference;
} }
} }
@@ -6,4 +6,26 @@ namespace Code.Constants
public const string ConfigMultiple = "Configs/{0}"; public const string ConfigMultiple = "Configs/{0}";
} }
public static class Tags
{
public const string LoadingScreen = "LoadingScreen";
}
public static class AudioGroups
{
public const string MasterGroup = "Master";
public const string SFXGroup = "SFX";
public const string CreaturesGroup = "Creatures";
public const string MusicGroup = "Music";
public const string PeacefulMusicGroup = "Peaceful";
public const string ActionMusicGroup = "Action";
public const string DispatcherMusicGroup = "Dispatcher";
public const string CourierMusicGroup = "Courier";
public const string EnemiesMusicGroup = "Enemies";
public const string PeacefulSnapshot = "Peaceful";
public const string ActionSnapshot = "Action";
public const string NoneSnapshot = "None";
public const string MenuSnapshot = "Menu";
public const string MenuMusicGroup = "Menu";
}
} }
@@ -12,6 +12,7 @@ namespace Code.Gameplay.Camera
{ {
Bogdan, Bogdan,
Static, Static,
UltraStatic,
FirstPerson, FirstPerson,
ThirdPerson, ThirdPerson,
Cinematic Cinematic
@@ -10,8 +10,11 @@ namespace Code.Gameplay.Camera
public override void Initialize() public override void Initialize()
{ {
States.Add(CameraMode.Static, new StaticState(StateMachineContext)); States.Add(CameraMode.Static, new StaticState(StateMachineContext));
States.Add(CameraMode.UltraStatic, new UltraStaticState(StateMachineContext));
States.Add(CameraMode.FirstPerson, new FirstPersonState(StateMachineContext)); States.Add(CameraMode.FirstPerson, new FirstPersonState(StateMachineContext));
States.Add(CameraMode.ThirdPerson, new ThirdPersonState(StateMachineContext)); States.Add(CameraMode.ThirdPerson, new ThirdPersonState(StateMachineContext));
CurrentBaseState = States[CameraMode.UltraStatic];
} }
@@ -0,0 +1,152 @@
using Code.Configs;
using Code.Input;
using Code.StateMachine;
using UnityEngine;
namespace Code.Gameplay.Camera.CameraStates
{
public abstract class BaseCameraState : BaseState<CameraStateMachineContext>
{
// DI сервиса ввода
private readonly IInputService _inputService;
// Конфиг камеры
private readonly CameraConfig _config;
// Используемая камера
protected UnityEngine.Camera Camera { get; private set; }
// Трансформ объекта, за которым следует камера
protected Transform FollowTransform;
// Позиция следования: текущая сглаженная позиция и горизонтальное направление камеры
protected Vector3 CurrentFollowPosition;
// Fallback-позиция для режимов без цели
private Vector3 _defaultFollowPosition;
// Целевой вертикальный угол камеры (наклон вверх/вниз)
private float _targetVerticalAngle;
// Текущая и целевая дистанция камеры от цели
protected float CurrentDistance, TargetDistance;
// Исходные значения дистанции для восстановления
protected float OriginalDefaultDistance, OriginalMinDistance, OriginalMaxDistance;
// Горизонтальное направление камеры (проекция forward на плоскость)
private Vector3 PlanarDirection { get; set; }
// Параметры загружаются из CameraConfig
protected float
DefaultDistance,
MinDistance,
MaxDistance,
DistanceMovementSpeed,
RotationSpeed,
RotationSharpness,
FollowSharpness,
MinVerticalAngle,
MaxVerticalAngle,
DefaultVerticalAngle;
// Свойства для доступа к важным данным камеры извне
public Vector3 GetPlanarDirection() => PlanarDirection; // Горизонтальное направление камеры
public Quaternion GetCameraRotation() => Camera.transform.rotation; // Текущее вращение камеры
// Конструктор базового состояния
protected BaseCameraState(CameraStateMachineContext context) : base(context)
{
_inputService = context.InputService;
_config = context.Config;
LoadConfig();
}
// Загрузка конфига для камеры
private void LoadConfig()
{
// Загрузка значений из конфигов
DefaultDistance = _config.defaultDistance;
MinDistance = _config.minDistance;
MaxDistance = _config.maxDistance;
DistanceMovementSpeed = _config.distanceMovementSpeed;
RotationSpeed = _config.rotationSpeed;
RotationSharpness = _config.rotationSharpness;
FollowSharpness = _config.followSharpness;
MinVerticalAngle = _config.minVerticalAngle;
MaxVerticalAngle = _config.maxVerticalAngle;
DefaultVerticalAngle = _config.defaultVerticalAngle;
_defaultFollowPosition = _config.defaultFollowPosition;
// Обеспечиваем корректные значения дистанции и угла
DefaultDistance = Mathf.Clamp(DefaultDistance, MinDistance, MaxDistance);
DefaultVerticalAngle = Mathf.Clamp(DefaultVerticalAngle, MinVerticalAngle, MaxVerticalAngle);
// Сохраняем оригинальные значения для восстановления
OriginalDefaultDistance = DefaultDistance;
OriginalMinDistance = MinDistance;
OriginalMaxDistance = MaxDistance;
CurrentDistance = DefaultDistance;
TargetDistance = CurrentDistance;
_targetVerticalAngle = DefaultVerticalAngle;
PlanarDirection = Vector3.forward; // Начальное направление - вперед по оси Z
CurrentFollowPosition = _defaultFollowPosition;
}
// Переход в состояние
public override void EnterState()
{
Camera = Context.Camera;
}
public override void LateTick()
{
// Обновляем камеру только если есть цель для следования
if (FollowTransform)
{
UpdateWithInput(Time.deltaTime, _inputService.ZoomInput, _inputService.LookInput);
}
}
// Абстрактные методы для специфики режима
protected abstract void HandlePosition(float deltaTime, float zoomInput, Quaternion targetRotation);
// Обработка вращения камеры на основе пользовательского ввода
// Теперь виртуальный, чтобы статический режим мог работать как хочет
protected virtual void HandleRotationInput(float deltaTime, Vector3 rotationInput, out Quaternion targetRotation)
{
// Вращение вокруг вертикальной оси (горизонтальный поворот)
var rotationFromInput = Quaternion.Euler(Vector3.up * (rotationInput.x * RotationSpeed));
PlanarDirection = rotationFromInput * PlanarDirection;
var planarRotation = Quaternion.LookRotation(PlanarDirection, FollowTransform.up);
// Вращение вокруг горизонтальной оси (вертикальный наклон)
_targetVerticalAngle -= rotationInput.y * RotationSpeed;
_targetVerticalAngle = Mathf.Clamp(_targetVerticalAngle, MinVerticalAngle, MaxVerticalAngle);
var verticalRotation = Quaternion.Euler(_targetVerticalAngle, 0, 0);
// Интерполяция к целевому вращению для плавности
targetRotation = Quaternion.Slerp(Camera.transform.rotation, planarRotation * verticalRotation, deltaTime * RotationSharpness);
// Применение финального вращения
Camera.transform.rotation = targetRotation;
}
// Основной метод обновления камеры, вызываемый извне каждый кадр
private void UpdateWithInput(float deltaTime, float zoomInput, Vector3 rotationInput)
{
// Обработка ввода и позиционирования
HandleRotationInput(deltaTime, rotationInput, out var targetRotation);
HandlePosition(deltaTime, zoomInput, targetRotation);
}
// Установка цели для следования камеры
public void SetFollowTransform(Transform t)
{
FollowTransform = t;
CurrentFollowPosition = t ? t.position : _defaultFollowPosition;
if (t) PlanarDirection = t.forward; // Начальное направление совпадает с направлением цели
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 92ff312b40a647b392af24877e699bd5
timeCreated: 1761685916
@@ -1,153 +1,43 @@
using System;
using Code.Input;
using Code.StateMachine;
using UnityEngine; using UnityEngine;
namespace Code.Gameplay.Camera.CameraStates namespace Code.Gameplay.Camera.CameraStates
{ {
public class FirstPersonState : BaseState<CameraStateMachineContext> /*
* Состояние камеры от первого лица
* Камера размещается непосредственно на позиции цели (например, на уровне глаз персонажа)
* Дистанция до цели принудительно устанавливается в 0
* Вращение управляется мышью, позиция следует за целью без отступа
*/
public class FirstPersonState : BaseCameraState, ICameraState
{ {
// Параметры загружаются из CameraConfig public FirstPersonState(CameraStateMachineContext context) : base(context) { }
private float
_defaultDistance,
_minDistance,
_maxDistance,
_distanceMovementSpeed,
_rotationSpeed,
_rotationSharpness,
_followSharpness,
_minVerticalAngle,
_maxVerticalAngle,
_defaultVerticalAngle;
// Трансформ объекта, за которым следует камера (для 1st-person: уровень глаз)
private Transform _followTransform;
// Позиция следования: текущая сглаженная позиция и горизонтальное направление камеры
private Vector3 _currentFollowPosition;
// Fallback-позиция для режимов без цели (например, MainMenu)
private Vector3 _defaultFollowPosition;
// Целевой вертикальный угол камеры (наклон вверх/вниз)
private float _targetVerticalAngle;
// Текущая и целевая дистанция камеры от цели
private float _currentDistance, _targetDistance;
// Исходные значения дистанции для восстановления после FirstPerson режима
private float _originalDefaultDistance, _originalMinDistance, _originalMaxDistance;
// Горизонтальное направление камеры (проекция forward на плоскость)
public Vector3 PlanarDirection { get; private set; }
private IInputService _inputService;
private UnityEngine.Camera _camera;
public FirstPersonState(CameraStateMachineContext context) : base(context)
{
_inputService = context.InputService;
// Загрузка значений из конфига
_defaultDistance = context.Config.defaultDistance;
_minDistance = context.Config.minDistance;
_maxDistance = context.Config.maxDistance;
_distanceMovementSpeed = context.Config.distanceMovementSpeed;
_rotationSpeed = context.Config.rotationSpeed;
_rotationSharpness = context.Config.rotationSharpness;
_followSharpness = context.Config.followSharpness;
_minVerticalAngle = context.Config.minVerticalAngle;
_maxVerticalAngle = context.Config.maxVerticalAngle;
_defaultVerticalAngle = context.Config.defaultVerticalAngle;
_defaultFollowPosition = context.Config.defaultFollowPosition;
// Валидация значений (на случай ручного редактирования конфига)
_defaultDistance = Mathf.Clamp(_defaultDistance, _minDistance, _maxDistance);
_defaultVerticalAngle = Mathf.Clamp(_defaultVerticalAngle, _minVerticalAngle, _maxVerticalAngle);
// Сохраняем оригинальные значения для восстановления
_originalDefaultDistance = _defaultDistance;
_originalMinDistance = _minDistance;
_originalMaxDistance = _maxDistance;
// Инициализация состояния камеры
_currentDistance = _defaultDistance;
_targetDistance = _currentDistance;
_targetVerticalAngle = _defaultVerticalAngle;
PlanarDirection = Vector3.forward; // Начальное направление - вперед по оси Z
_currentFollowPosition = _defaultFollowPosition;
}
public override void EnterState() public override void EnterState()
{ {
// В FirstPerson режиме дистанция всегда равна 0 // В FirstPerson режиме дистанция всегда равна 0
_minDistance = 0f; MinDistance = 0f;
_maxDistance = 0f; MaxDistance = 0f;
_defaultDistance = 0f; DefaultDistance = 0f;
_targetDistance = 0f; TargetDistance = 0f;
_currentDistance = 0f; CurrentDistance = 0f;
_camera = Context.Camera; base.EnterState();
} }
public override void LateTick() protected override void HandlePosition(float deltaTime, float zoomInput, Quaternion targetRotation)
{
UpdateWithInput(Time.deltaTime,_inputService.ZoomInput, _inputService.LookInput);
}
// Обработка вращения камеры на основе пользовательского ввода
private void HandleRotationInput(float deltaTime, Vector3 rotationInput, out Quaternion targetRotation)
{
// Вращение вокруг вертикальной оси (горизонтальный поворот)
var rotationFromInput = Quaternion.Euler(_followTransform.up * (rotationInput.x * _rotationSpeed));
PlanarDirection = rotationFromInput * PlanarDirection;
var planarRotation = Quaternion.LookRotation(PlanarDirection, _followTransform.up);
// Вращение вокруг горизонтальной оси (вертикальный наклон)
_targetVerticalAngle -= rotationInput.y * _rotationSpeed;
_targetVerticalAngle = Math.Clamp(_targetVerticalAngle, _minVerticalAngle, _maxVerticalAngle);
var verticalRotation = Quaternion.Euler(_targetVerticalAngle, 0, 0);
// Интерполяция к целевому вращению для плавности
targetRotation = Quaternion.Slerp(_camera.transform.rotation, planarRotation * verticalRotation, deltaTime * _rotationSharpness);
Debug.Log(_camera.transform.rotation.eulerAngles);
// Применение финального вращения
_camera.transform.rotation = targetRotation;
Debug.Log(_camera.transform.rotation.eulerAngles);
}
// Обработка позиционирования камеры и зума с учетом текущего режима
private void HandlePosition(float deltaTime, float zoomInput, Quaternion targetRotation)
{ {
// Плавное следование за целью с использованием экспоненциального сглаживания // Плавное следование за целью с использованием экспоненциального сглаживания
_currentFollowPosition = Vector3.Lerp(_currentFollowPosition, _followTransform.position, 1f - Mathf.Exp(-_followSharpness * deltaTime)); CurrentFollowPosition = Vector3.Lerp(CurrentFollowPosition, FollowTransform.position, 1f - Mathf.Exp(-FollowSharpness * deltaTime));
// Вычисление целевой позиции камеры с учетом текущего режима // В FirstPerson режиме камера находится прямо на позиции цели
Vector3 targetPosition; var targetPosition = CurrentFollowPosition;
// В FirstPerson режиме камера находится прямо на позиции цели
targetPosition = _currentFollowPosition;
// Установка финальной позиции камеры // Установка финальной позиции камеры
_camera.transform.position = targetPosition; Camera.transform.position = targetPosition;
} }
public CameraMode GetCameraMode()
// Основной метод обновления камеры, вызываемый извне каждый кадр
public void UpdateWithInput(float deltaTime, float zoomInput, Vector3 rotationInput)
{ {
// Проверка наличия цели для следования и проверка на режим главного меню return CameraMode.FirstPerson;
if (!_followTransform) return;
// Обработка ввода и позиционирования
HandleRotationInput(deltaTime, rotationInput, out var targetRotation);
HandlePosition(deltaTime, zoomInput, targetRotation);
}
// Установка цели для следования камеры
public void SetFollowTransform(Transform t)
{
_followTransform = t;
_currentFollowPosition = t.position;
PlanarDirection = t.forward; // Начальное направление совпадает с направлением цели
} }
} }
} }
@@ -0,0 +1,7 @@
namespace Code.Gameplay.Camera.CameraStates
{
public interface ICameraState
{
CameraMode GetCameraMode();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4f25670e547242e8a91e19894cf6889c
timeCreated: 1761687204
@@ -1,23 +1,30 @@
using Code.StateMachine;
using UnityEngine; using UnityEngine;
namespace Code.Gameplay.Camera.CameraStates namespace Code.Gameplay.Camera.CameraStates
{ {
public class StaticState : BaseState<CameraStateMachineContext> public class StaticState : BaseCameraState, ICameraState
{ {
private Transform _target; public Transform LookTarget;
private Transform _position; public StaticState(CameraStateMachineContext context) : base(context) { }
private UnityEngine.Camera _camera;
public StaticState(CameraStateMachineContext context) : base(context)
{
}
public override void EnterState() public override void EnterState()
{ {
_camera = Context.Camera; Debug.Log("Static State Enter");
base.EnterState(); base.EnterState();
} }
protected override void HandlePosition(float deltaTime, float zoomInput, Quaternion targetRotation)
{
Camera.transform.position = FollowTransform.position;
}
public CameraMode GetCameraMode() => CameraMode.Static;
protected override void HandleRotationInput(float deltaTime, Vector3 rotationInput,
out Quaternion targetRotation)
{
if(LookTarget) Camera.transform.LookAt(LookTarget);
targetRotation = Camera.transform.rotation;
}
} }
} }
@@ -1,164 +1,58 @@
using System;
using Code.Input;
using Code.StateMachine;
using UnityEngine; using UnityEngine;
namespace Code.Gameplay.Camera.CameraStates namespace Code.Gameplay.Camera.CameraStates
{ {
public class ThirdPersonState : BaseState<CameraStateMachineContext> /*
* Состояние камеры от третьего лица
* Камера следует за персонажем на заданном расстоянии
* Поддерживает зум, плавное следование и вращение с учётом угла наклона
* Дистанция и углы настраиваются через конфигурацию
*/
public class ThirdPersonState : BaseCameraState, ICameraState
{ {
// Параметры загружаются из CameraConfig public ThirdPersonState(CameraStateMachineContext context) : base(context) { }
private float
_defaultDistance,
_minDistance,
_maxDistance,
_distanceMovementSpeed,
_rotationSpeed,
_rotationSharpness,
_followSharpness,
_minVerticalAngle,
_maxVerticalAngle,
_defaultVerticalAngle;
// Трансформ объекта, за которым следует камера (для 1st-person: уровень глаз)
private Transform _followTransform;
// Позиция следования: текущая сглаженная позиция и горизонтальное направление камеры
private Vector3 _currentFollowPosition;
// Fallback-позиция для режимов без цели (например, MainMenu)
private Vector3 _defaultFollowPosition;
// Целевой вертикальный угол камеры (наклон вверх/вниз)
private float _targetVerticalAngle;
// Текущая и целевая дистанция камеры от цели
private float _currentDistance, _targetDistance;
// Исходные значения дистанции для восстановления после FirstPerson режима
private float _originalDefaultDistance, _originalMinDistance, _originalMaxDistance;
// Горизонтальное направление камеры (проекция forward на плоскость)
public Vector3 PlanarDirection { get; private set; }
private IInputService _inputService;
private UnityEngine.Camera _camera;
public ThirdPersonState(CameraStateMachineContext context) : base(context)
{
_inputService = context.InputService;
// Загрузка значений из конфига
_defaultDistance = context.Config.defaultDistance;
_minDistance = context.Config.minDistance;
_maxDistance = context.Config.maxDistance;
_distanceMovementSpeed = context.Config.distanceMovementSpeed;
_rotationSpeed = context.Config.rotationSpeed;
_rotationSharpness = context.Config.rotationSharpness;
_followSharpness = context.Config.followSharpness;
_minVerticalAngle = context.Config.minVerticalAngle;
_maxVerticalAngle = context.Config.maxVerticalAngle;
_defaultVerticalAngle = context.Config.defaultVerticalAngle;
_defaultFollowPosition = context.Config.defaultFollowPosition;
// Валидация значений (на случай ручного редактирования конфига)
_defaultDistance = Mathf.Clamp(_defaultDistance, _minDistance, _maxDistance);
_defaultVerticalAngle = Mathf.Clamp(_defaultVerticalAngle, _minVerticalAngle, _maxVerticalAngle);
// Сохраняем оригинальные значения для восстановления
_originalDefaultDistance = _defaultDistance;
_originalMinDistance = _minDistance;
_originalMaxDistance = _maxDistance;
// Инициализация состояния камеры
_currentDistance = _defaultDistance;
_targetDistance = _currentDistance;
_targetVerticalAngle = _defaultVerticalAngle;
PlanarDirection = Vector3.forward; // Начальное направление - вперед по оси Z
_currentFollowPosition = _defaultFollowPosition;
}
public override void EnterState() public override void EnterState()
{ {
// Восстанавливаем исходные значения дистанции // Восстанавливаем исходные значения дистанции
_minDistance = _originalMinDistance; MinDistance = OriginalMinDistance;
_maxDistance = _originalMaxDistance; MaxDistance = OriginalMaxDistance;
_defaultDistance = _originalDefaultDistance; DefaultDistance = OriginalDefaultDistance;
_targetDistance = _defaultDistance; TargetDistance = DefaultDistance;
_camera = Context.Camera; base.EnterState();
} }
public override void LateTick() protected override void HandlePosition(float deltaTime, float zoomInput, Quaternion targetRotation)
{
UpdateWithInput(Time.deltaTime, _inputService.ZoomInput, _inputService.LookInput);
}
// Основной метод обновления камеры, вызываемый извне каждый кадр
public void UpdateWithInput(float deltaTime, float zoomInput, Vector3 rotationInput)
{
// Проверка наличия цели для следования и проверка на режим главного меню
if (!_followTransform) return;
// Обработка ввода и позиционирования
HandleRotationInput(deltaTime, rotationInput, out var targetRotation);
HandlePosition(deltaTime, zoomInput, targetRotation);
}
// Обработка вращения камеры на основе пользовательского ввода
private void HandleRotationInput(float deltaTime, Vector3 rotationInput, out Quaternion targetRotation)
{
// Вращение вокруг вертикальной оси (горизонтальный поворот)
var rotationFromInput = Quaternion.Euler(_followTransform.up * (rotationInput.x * _rotationSpeed));
PlanarDirection = rotationFromInput * PlanarDirection;
var planarRotation = Quaternion.LookRotation(PlanarDirection, _followTransform.up);
// Вращение вокруг горизонтальной оси (вертикальный наклон)
_targetVerticalAngle -= rotationInput.y * _rotationSpeed;
_targetVerticalAngle = Math.Clamp(_targetVerticalAngle, _minVerticalAngle, _maxVerticalAngle);
var verticalRotation = Quaternion.Euler(_targetVerticalAngle, 0, 0);
// Интерполяция к целевому вращению для плавности
targetRotation = Quaternion.Slerp(_camera.transform.rotation, planarRotation * verticalRotation,
deltaTime * _rotationSharpness);
// Применение финального вращения
_camera.transform.rotation = targetRotation;
}
// Обработка позиционирования камеры и зума с учетом текущего режима
private void HandlePosition(float deltaTime, float zoomInput, Quaternion targetRotation)
{ {
// Обновление целевой дистанции на основе ввода зума // Обновление целевой дистанции на основе ввода зума
_targetDistance += zoomInput * _distanceMovementSpeed; TargetDistance += zoomInput * DistanceMovementSpeed;
_targetDistance = Mathf.Clamp(_targetDistance, _minDistance, _maxDistance); TargetDistance = Mathf.Clamp(TargetDistance, MinDistance, MaxDistance);
// Плавное следование за целью с использованием экспоненциального сглаживания // Плавное следование за целью с использованием экспоненциального сглаживания
_currentFollowPosition = Vector3.Lerp(_currentFollowPosition, _followTransform.position, CurrentFollowPosition = Vector3.Lerp(
1f - Mathf.Exp(-_followSharpness * deltaTime)); CurrentFollowPosition,
FollowTransform.position,
// Вычисление целевой позиции камеры с учетом текущего режима 1f - Mathf.Exp(-FollowSharpness * deltaTime)
Vector3 targetPosition; );
// В ThirdPerson режиме камера отодвинута на расстояние от цели // В ThirdPerson режиме камера отодвинута на расстояние от цели
targetPosition = _currentFollowPosition - targetRotation * Vector3.forward * _currentDistance; var targetPosition = CurrentFollowPosition - targetRotation * Vector3.forward * CurrentDistance;
// Плавное изменение текущей дистанции к целевой (только для ThirdPerson)
_currentDistance = Mathf.Lerp(_currentDistance, _targetDistance,
1f - Mathf.Exp(-_distanceMovementSpeed * deltaTime));
// Плавное изменение текущей дистанции к целевой
CurrentDistance = Mathf.Lerp(
CurrentDistance,
TargetDistance,
1f - Mathf.Exp(-DistanceMovementSpeed * deltaTime)
);
// Установка финальной позиции камеры // Установка финальной позиции камеры
_camera.transform.position = targetPosition; Camera.transform.position = targetPosition;
} }
// Установка цели для следования камеры public CameraMode GetCameraMode()
public void SetFollowTransform(Transform t)
{ {
_followTransform = t; return CameraMode.ThirdPerson;
_currentFollowPosition = t.position;
PlanarDirection = t.forward; // Начальное направление совпадает с направлением цели
} }
} }
} }
@@ -0,0 +1,29 @@
using UnityEngine;
namespace Code.Gameplay.Camera.CameraStates
{
public class UltraStaticState : BaseCameraState, ICameraState
{
public UltraStaticState(CameraStateMachineContext context) : base(context) { }
public override void EnterState()
{
Debug.Log("Ultra static State Enter");
base.EnterState();
}
protected override void HandlePosition(float deltaTime, float zoomInput, Quaternion targetRotation)
{
Camera.transform.position = FollowTransform.position;
}
public CameraMode GetCameraMode() => CameraMode.UltraStatic;
protected override void HandleRotationInput(float deltaTime, Vector3 rotationInput,
out Quaternion targetRotation)
{
if(FollowTransform) Camera.transform.rotation = FollowTransform.rotation;
targetRotation = Camera.transform.rotation;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8e4b2fa3af4b4ba78ab79ca154f05fb4
timeCreated: 1763547110
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: b36ec6a31fb74d608232394fd5df657c
timeCreated: 1761570832
@@ -1,215 +0,0 @@
using System;
using Code.Configs;
using Code.Gameplay.Camera;
using Code.Gameplay.Character.Core;
using Code.StaticData;
using Reflex.Attributes;
using UnityEngine;
namespace Code.Gameplay.Character.Camera
{
/*
* Скрипт, отвечающий за поведение камеры при передвижении
* Поддерживает как 3rd-person, так и 1st-person режимы камеры
* Реализует плавное следование за персонажем, вращение и зум камеры
*
* Источник: Tvtig, https://www.youtube.com/watch?v=9ibpY64wBYM
*/
public class CameraService : MonoBehaviour
{
// Параметры загружаются из CameraConfig
private float
_defaultDistance,
_minDistance,
_maxDistance,
_distanceMovementSpeed,
_rotationSpeed,
_rotationSharpness,
_followSharpness,
_minVerticalAngle,
_maxVerticalAngle,
_defaultVerticalAngle;
// Трансформ объекта, за которым следует камера (для 1st-person: уровень глаз)
private Transform _followTransform;
// Позиция следования: текущая сглаженная позиция и горизонтальное направление камеры
private Vector3 _currentFollowPosition;
// Fallback-позиция для режимов без цели (например, MainMenu)
private Vector3 _defaultFollowPosition;
// Целевой вертикальный угол камеры (наклон вверх/вниз)
private float _targetVerticalAngle;
// Текущая и целевая дистанция камеры от цели
private float _currentDistance, _targetDistance;
// Исходные значения дистанции для восстановления после FirstPerson режима
private float _originalDefaultDistance, _originalMinDistance, _originalMaxDistance;
// Горизонтальное направление камеры (проекция forward на плоскость)
public Vector3 PlanarDirection { get; private set; }
// Текущий режим работы камеры
public CameraMode CurrentMode { get; private set; }
// Загрузка конфига для камеры
[Inject]
private void LoadConfig(CameraConfig config)
{
// Загрузка значений из конфига
_defaultDistance = config.defaultDistance;
_minDistance = config.minDistance;
_maxDistance = config.maxDistance;
_distanceMovementSpeed = config.distanceMovementSpeed;
_rotationSpeed = config.rotationSpeed;
_rotationSharpness = config.rotationSharpness;
_followSharpness = config.followSharpness;
_minVerticalAngle = config.minVerticalAngle;
_maxVerticalAngle = config.maxVerticalAngle;
_defaultVerticalAngle = config.defaultVerticalAngle;
CurrentMode = config.defaultCameraMode;
_defaultFollowPosition = config.defaultFollowPosition;
// Валидация значений (на случай ручного редактирования конфига)
_defaultDistance = Mathf.Clamp(_defaultDistance, _minDistance, _maxDistance);
_defaultVerticalAngle = Mathf.Clamp(_defaultVerticalAngle, _minVerticalAngle, _maxVerticalAngle);
// Сохраняем оригинальные значения для восстановления
_originalDefaultDistance = _defaultDistance;
_originalMinDistance = _minDistance;
_originalMaxDistance = _maxDistance;
// Инициализация состояния камеры
_currentDistance = _defaultDistance;
_targetDistance = _currentDistance;
_targetVerticalAngle = _defaultVerticalAngle;
PlanarDirection = Vector3.forward; // Начальное направление - вперед по оси Z
_currentFollowPosition = _defaultFollowPosition;
}
// Установка цели для следования камеры
public void SetFollowTransform(Transform t)
{
_followTransform = t;
_currentFollowPosition = t.position;
PlanarDirection = t.forward; // Начальное направление совпадает с направлением цели
}
// Обработка вращения камеры на основе пользовательского ввода
private void HandleRotationInput(float deltaTime, Vector3 rotationInput, out Quaternion targetRotation)
{
// Вращение вокруг вертикальной оси (горизонтальный поворот)
var rotationFromInput = Quaternion.Euler(_followTransform.up * (rotationInput.x * _rotationSpeed));
PlanarDirection = rotationFromInput * PlanarDirection;
var planarRotation = Quaternion.LookRotation(PlanarDirection, _followTransform.up);
// Вращение вокруг горизонтальной оси (вертикальный наклон)
_targetVerticalAngle -= rotationInput.y * _rotationSpeed;
_targetVerticalAngle = Math.Clamp(_targetVerticalAngle, _minVerticalAngle, _maxVerticalAngle);
var verticalRotation = Quaternion.Euler(_targetVerticalAngle, 0, 0);
// Интерполяция к целевому вращению для плавности
targetRotation = Quaternion.Slerp(transform.rotation, planarRotation * verticalRotation, deltaTime * _rotationSharpness);
// Применение финального вращения
transform.rotation = targetRotation;
}
// Обработка позиционирования камеры и зума с учетом текущего режима
private void HandlePosition(float deltaTime, float zoomInput, Quaternion targetRotation)
{
// В FirstPerson режиме зум отключается
if (CurrentMode != CameraMode.FirstPerson)
{
// Обновление целевой дистанции на основе ввода зума
_targetDistance += zoomInput * _distanceMovementSpeed;
_targetDistance = Mathf.Clamp(_targetDistance, _minDistance, _maxDistance);
}
// Плавное следование за целью с использованием экспоненциального сглаживания
_currentFollowPosition = Vector3.Lerp(_currentFollowPosition, _followTransform.position, 1f - Mathf.Exp(-_followSharpness * deltaTime));
// Вычисление целевой позиции камеры с учетом текущего режима
Vector3 targetPosition;
if (CurrentMode == CameraMode.FirstPerson)
{
// В FirstPerson режиме камера находится прямо на позиции цели
targetPosition = _currentFollowPosition;
}
else
{
// В ThirdPerson режиме камера отодвинута на расстояние от цели
targetPosition = _currentFollowPosition - targetRotation * Vector3.forward * _currentDistance;
}
// Плавное изменение текущей дистанции к целевой (только для ThirdPerson)
if (CurrentMode != CameraMode.FirstPerson)
{
_currentDistance = Mathf.Lerp(_currentDistance, _targetDistance, 1f - Mathf.Exp(-_distanceMovementSpeed * deltaTime));
}
// Установка финальной позиции камеры
transform.position = targetPosition;
}
// Основной метод обновления камеры, вызываемый извне каждый кадр
public void UpdateWithInput(float deltaTime, float zoomInput, Vector3 rotationInput)
{
// Проверка наличия цели для следования и проверка на режим главного меню
if (!_followTransform || CurrentMode == CameraMode.Bogdan) return;
// Обработка ввода и позиционирования
HandleRotationInput(deltaTime, rotationInput, out var targetRotation);
HandlePosition(deltaTime, zoomInput, targetRotation);
}
// Смена режима камеры
public void SetCameraMode(CameraMode newMode)
{
if (CurrentMode == newMode) return;
CurrentMode = newMode;
// Применяем настройки специфичные для режима
ApplyModeSpecificSettings();
}
// Применение настроек специфичных для текущего режима
private void ApplyModeSpecificSettings()
{
switch (CurrentMode)
{
case CameraMode.FirstPerson:
// В FirstPerson режиме дистанция всегда равна 0
_minDistance = 0f;
_maxDistance = 0f;
_defaultDistance = 0f;
_targetDistance = 0f;
_currentDistance = 0f;
break;
case CameraMode.ThirdPerson:
// Восстанавливаем исходные значения дистанции
_minDistance = _originalMinDistance;
_maxDistance = _originalMaxDistance;
_defaultDistance = _originalDefaultDistance;
_targetDistance = _defaultDistance;
break;
// Заготовка на будущее (если понадобится)
case CameraMode.Bogdan:
_minDistance = 0f;
_maxDistance = 0f;
_defaultDistance = 0f;
_targetDistance = 0f;
_currentDistance = 0f;
break;
case CameraMode.Static:
case CameraMode.Cinematic:
default:
break;
}
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f1d98e6c5419411098391c07861e627e
timeCreated: 1760627986
@@ -1,7 +1,6 @@
using Code.Gameplay.Camera; using Code.Gameplay.Camera;
using KinematicCharacterController; using KinematicCharacterController;
using UnityEngine; using UnityEngine;
using Code.Gameplay.Character.Core;
namespace Code.Gameplay.Character.Controller.Abilities namespace Code.Gameplay.Character.Controller.Abilities
{ {
@@ -13,30 +13,51 @@ namespace Code.Gameplay.Character.Controller.Abilities
private readonly float _wallClimbSpeedFactor; private readonly float _wallClimbSpeedFactor;
// Минимальная горизонтальная скорость для начала залазания на стену // Минимальная горизонтальная скорость для начала залазания на стену
private readonly float _minWallClimbSpeed; private readonly float _minWallClimbSpeed;
// Пороговый косинус стены
public float WallNormalThreshold { get; }
private bool _hasClimbedThisWallSession;
private float _wallClimbCooldownTimer;
private readonly float _wallClimbCooldownDuration = 1.5f;
public bool IsClimbingActive { get; private set; }
// Конструктор механики залазания на стену // Конструктор механики залазания на стену
public WallClimbMechanic(float wallClimbSpeedFactor, float minWallClimbSpeed) public WallClimbMechanic(float wallClimbSpeedFactor, float minWallClimbSpeed, float wallDetectionAngleThreshold)
{ {
_wallClimbSpeedFactor = wallClimbSpeedFactor; _wallClimbSpeedFactor = wallClimbSpeedFactor;
_minWallClimbSpeed = minWallClimbSpeed; _minWallClimbSpeed = minWallClimbSpeed;
// Преобразуем угол в косинус порога: если |dot| > cos(threshold), то это НЕ стена
WallNormalThreshold = Mathf.Cos(Mathf.Deg2Rad * wallDetectionAngleThreshold);
} }
// Обработка столкновения при движении (вызывается при ударе о препятствие) // Обработка столкновения при движении (вызывается при ударе о препятствие)
public void OnMovementHit(KinematicCharacterMotor motor, Vector3 hitNormal) public bool TryClimbWall(KinematicCharacterMotor motor, Vector3 hitNormal)
{ {
// Проверяем, что это вертикальная или почти вертикальная поверхность (стена) // Проверяем, что это вертикальная или почти вертикальная поверхность (стена)
var dotWithUp = Vector3.Dot(hitNormal, motor.CharacterUp); var dotWithUp = Vector3.Dot(hitNormal, motor.CharacterUp);
if (Mathf.Abs(dotWithUp) > 0.1f) return; // Не стена — это пол или потолок
if (Mathf.Abs(dotWithUp) > WallNormalThreshold)
{
// Сбрасываем сессию, если коснулись пола/потолка → выход из "режима стены"
_hasClimbedThisWallSession = false;
return false;
}
// Запрещаем повторное залазание в рамках одной сессии
if (_hasClimbedThisWallSession || _wallClimbCooldownTimer > 0f)
return false;
// Получаем текущую скорость персонажа // Получаем текущую скорость персонажа
var currentVelocity = motor.Velocity; var currentVelocity = motor.Velocity;
// Проекция скорости на направление к стене (отрицательная, если летим в стену) // Проекция скорости на направление к стене (отрицательная, если летим в стену)
var velocityIntoWall = Vector3.Dot(currentVelocity, -hitNormal); var velocityIntoWall = Vector3.Dot(currentVelocity, -hitNormal);
// Если движемся в стену с достаточной скоростью // Если движемся в стену с достаточной скоростью
if (velocityIntoWall > _minWallClimbSpeed) if (velocityIntoWall > _minWallClimbSpeed)
{ {
_wallClimbCooldownTimer = _wallClimbCooldownDuration;
// Преобразуем часть горизонтальной скорости в вертикальную // Преобразуем часть горизонтальной скорости в вертикальную
var verticalBoost = velocityIntoWall * _wallClimbSpeedFactor; var verticalBoost = velocityIntoWall * _wallClimbSpeedFactor;
@@ -45,6 +66,26 @@ namespace Code.Gameplay.Character.Controller.Abilities
// Предотвращаем заземление и повторное применение в этом кадре // Предотвращаем заземление и повторное применение в этом кадре
motor.ForceUnground(); motor.ForceUnground();
_hasClimbedThisWallSession = true;
IsClimbingActive = true;
return true;
}
return false;
}
public void ResetWallClimbSession()
{
_hasClimbedThisWallSession = false;
IsClimbingActive = false;
}
public void TickCooldown(float deltaTime)
{
if (_wallClimbCooldownTimer > 0f)
{
_wallClimbCooldownTimer -= deltaTime;
} }
} }
} }
@@ -0,0 +1,147 @@
using UnityEngine;
using KinematicCharacterController;
namespace Code.Gameplay.Character.Controller.Abilities
{
/*
* Механика наклона персонажа при контакте со стеной
* Управляет визуальным наклоном модели в зависимости от высоты подъёма по стене
*/
public class WallLeanMechanic
{
private readonly float _maxLeanAngle;
private readonly float _leanSharpness;
private readonly float _heightScale;
private Transform _characterTransform;
private Vector3 _lastWallNormal;
private float _lastWallContactHeight;
private float _leanFactor;
private float _leanSign;
private Vector3 _leanDirection;
private Vector3 _lastWallContactPoint;
private bool _isLeaningOnWall;
private const float WallProximityThreshold = 0.6f;
public WallLeanMechanic(float maxLeanAngle, float leanSharpness, float heightScale)
{
_maxLeanAngle = maxLeanAngle;
_leanSharpness = leanSharpness;
_heightScale = heightScale;
}
// Назначаем трансформ визуальной модели один раз
public void SetCharacterTransform(Transform transform)
{
_characterTransform = transform;
}
// Вызывается из OnMovementHit
public void OnWallContact(KinematicCharacterMotor motor, Vector3 wallNormal, float contactHeight)
{
// Только если это действительно стена (не пол/потолок)
var dotWithUp = Vector3.Dot(wallNormal, motor.CharacterUp);
if (Mathf.Abs(dotWithUp) > 0.7f) // ~45°, можно настроить
return;
_lastWallNormal = wallNormal;
_lastWallContactHeight = contactHeight;
_lastWallContactPoint = motor.TransientPosition;
// Пересчитываем направление наклона
_leanDirection = -_lastWallNormal;
_leanDirection = Vector3.ProjectOnPlane(_leanDirection, motor.CharacterUp).normalized;
_leanSign = 1;
// what the fuck
if (_leanDirection.x != 0)
{
switch (_leanDirection.x)
{
case -1 or 1 when motor.CharacterRight.x > 0.1f:
_leanSign = -1;
break;
case -1 or 1 when Mathf.Abs(motor.CharacterRight.x) < 0.1f:
_leanDirection = new Vector3(0, 0, -1);
break;
}
}
else
{
switch (_leanDirection.z)
{
case 1 when Mathf.Abs(motor.CharacterRight.z) < 0.1f:
_leanSign = -1;
break;
case -1 when motor.CharacterRight.z < -0.1f:
case 1 when motor.CharacterRight.z > 0.1f:
_leanDirection = new Vector3(-1, 0, 0);
break;
case -1 when motor.CharacterRight.z > 0.1f:
case 1 when motor.CharacterRight.z < -0.1f:
_leanDirection = new Vector3(1, 0, 0);
break;
}
}
_isLeaningOnWall = true;
}
// Обновление каждый кадр
public void UpdateLean(KinematicCharacterMotor motor, float deltaTime)
{
if (!_characterTransform) return;
var currentPosition = motor.TransientPosition;
var isStillNearWall = false;
if (_isLeaningOnWall && _lastWallNormal != Vector3.zero)
{
var toWall = currentPosition - _lastWallContactPoint;
var distanceToWallPlane = Mathf.Abs(Vector3.Dot(toWall, _lastWallNormal));
isStillNearWall = distanceToWallPlane <= WallProximityThreshold;
}
if (_isLeaningOnWall)
{
var isBelowContactHeight = currentPosition.y < _lastWallContactHeight;
if (!isStillNearWall || isBelowContactHeight)
{
_isLeaningOnWall = false;
}
else
{
var heightDelta = currentPosition.y - _lastWallContactHeight;
var targetLeanFactor = Mathf.Clamp01(heightDelta * _heightScale);
_leanFactor = Mathf.Lerp(_leanFactor, targetLeanFactor, 1f - Mathf.Exp(-_leanSharpness * deltaTime));
}
}
if (!_isLeaningOnWall)
{
_leanFactor = Mathf.Lerp(_leanFactor, 0f, 1f - Mathf.Exp(-_leanSharpness * deltaTime));
}
ApplyLeanRotation(motor);
}
private void ApplyLeanRotation(KinematicCharacterMotor motor)
{
if (_leanFactor <= 0.01f)
{
_characterTransform.localRotation = Quaternion.identity;
return;
}
if (_leanDirection.sqrMagnitude < 0.01f) return;
var leanAngle = _leanSign * _leanFactor * _maxLeanAngle;
var leanAxis = Vector3.Cross(motor.CharacterUp, _leanDirection);
var leanRotation = Quaternion.AngleAxis(leanAngle, leanAxis);
_characterTransform.localRotation = leanRotation;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dfaefcb035124edb84b7874c5938defc
timeCreated: 1761738483
@@ -1,9 +1,11 @@
using Code.Configs;
using Code.Gameplay.Camera; using Code.Gameplay.Camera;
using Code.Gameplay.Character.Controller.Abilities; using Code.Gameplay.Character.Controller.Abilities;
using Code.Gameplay.Character.Controller.Input; using Code.Gameplay.Character.Controller.Input;
using Code.Gameplay.Character.Controller.Movement; using Code.Gameplay.Character.Controller.Movement;
using Code.Gameplay.Character.Core; using Code.Gameplay.Character.Core;
using KinematicCharacterController; using KinematicCharacterController;
using Reflex.Attributes;
using UnityEngine; using UnityEngine;
namespace Code.Gameplay.Character.Controller namespace Code.Gameplay.Character.Controller
@@ -17,58 +19,114 @@ namespace Code.Gameplay.Character.Controller
*/ */
public class CustomCharacterController : MonoBehaviour, ICharacterController public class CustomCharacterController : MonoBehaviour, ICharacterController
{ {
[SerializeField] [Inject] private CharacterConfig _characterConfig; // Конфиг персонажа через DI
private KinematicCharacterMotor motor; // Движок физического перемещения персонажа
[SerializeField] [SerializeField] private KinematicCharacterMotor motor; // Движок физического перемещения персонажа
private float
maxStableMoveSpeed = 8f, // Максимальная скорость движения по устойчивой поверхности [SerializeField] private Transform characterTransform; // Трансформ персонажа (с точкой отслеживания камеры)
sprintMoveSpeed = 12f, // Максимальная скорость спринта
slideSpeed = 20f, // Скорость слайда
slideDuration = 0.8f, // Длительность слайда в секундах
slideCooldown = 1.5f, // Время перезарядки слайда
stableMovementSharpness = 15f, // Резкость изменения скорости движения (чем выше - тем резче)
orientationSharpness = 10f, // Резкость изменения вращения персонажа (чем выше - тем резче)
jumpSpeed = 10f, // Скорость прыжка
wallClimbSpeedFactor = 0.8f, // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену
minWallClimbSpeed = 9f; // Минимальная горизонтальная скорость для начала залазания на стену
[SerializeField]
private Vector3 gravity = new(0, -30f, 0); // Вектор, отвечающий за применение гравитации
// Подсистемы // Подсистемы
private readonly CharacterInputHandler _inputHandler; private CharacterInputHandler _inputHandler;
private readonly SlideMechanic _slideMechanic; private SlideMechanic _slideMechanic;
private readonly WallClimbMechanic _wallClimbMechanic; private WallClimbMechanic _wallClimbMechanic;
private readonly GroundMovementSystem _groundMovement; private WallLeanMechanic _wallLeanMechanic;
private readonly AirMovementSystem _airMovement; private GroundMovementSystem _groundMovement;
private AirMovementSystem _airMovement;
// Конструктор без параметров, инициализирующий контроллер public bool IsSliding => _slideMechanic.IsSliding;
public CustomCharacterController()
private float abilityBlockTimer;
private bool isAbilitiesBlocked;
public void SetPosition(Vector3 position) => motor.SetPosition(position);
public void SetRotation(Quaternion rotation) => motor.SetRotation(rotation);
// Инициализация контроллера
// private void Awake()
// {
// // Инициализация подсистем с параметрами из конфига
// _inputHandler = new CharacterInputHandler(
// _characterConfig.sprintAngleThreshold
// );
// _slideMechanic = new SlideMechanic(
// _characterConfig.slideSpeed,
// _characterConfig.slideDuration,
// _characterConfig.slideCooldown
// );
// _wallClimbMechanic = new WallClimbMechanic(
// _characterConfig.wallClimbSpeedFactor,
// _characterConfig.minWallClimbSpeed,
// _characterConfig.wallDetectionAngleThreshold
// );
// _wallLeanMechanic = new WallLeanMechanic(
// _characterConfig.maxWallLeanAngle,
// _characterConfig.wallLeanSharpness,
// _characterConfig.wallLeanHeightScale
// );
// _groundMovement = new GroundMovementSystem(
// _characterConfig.maxStableMoveSpeed,
// _characterConfig.sprintMoveSpeed,
// _characterConfig.stableMovementSharpness,
// _characterConfig.jumpSpeed
// );
// _airMovement = new AirMovementSystem(_characterConfig.gravity);
// }
[Inject]
private void Construct(CharacterConfig characterConfig)
{ {
_inputHandler = new CharacterInputHandler(); _characterConfig = characterConfig;
_slideMechanic = new SlideMechanic(slideSpeed, slideDuration, slideCooldown); // Инициализация подсистем с параметрами из конфига
_wallClimbMechanic = new WallClimbMechanic(wallClimbSpeedFactor, minWallClimbSpeed); _inputHandler = new CharacterInputHandler(
_groundMovement = new GroundMovementSystem(maxStableMoveSpeed, sprintMoveSpeed, stableMovementSharpness, jumpSpeed); _characterConfig.sprintAngleThreshold
_airMovement = new AirMovementSystem(gravity); );
_slideMechanic = new SlideMechanic(
_characterConfig.slideSpeed,
_characterConfig.slideDuration,
_characterConfig.slideCooldown
);
_wallClimbMechanic = new WallClimbMechanic(
_characterConfig.wallClimbSpeedFactor,
_characterConfig.minWallClimbSpeed,
_characterConfig.wallDetectionAngleThreshold
);
_wallLeanMechanic = new WallLeanMechanic(
_characterConfig.maxWallLeanAngle,
_characterConfig.wallLeanSharpness,
_characterConfig.wallLeanHeightScale
);
_groundMovement = new GroundMovementSystem(
_characterConfig.maxStableMoveSpeed,
_characterConfig.sprintMoveSpeed,
_characterConfig.stableMovementSharpness,
_characterConfig.jumpSpeed
);
_airMovement = new AirMovementSystem(_characterConfig.gravity);
motor.CharacterController = this; // Регистрация этого контроллера в движке
_inputHandler.FirstPersonCameraRotation =
motor.Transform.rotation; // Начальное вращение совпадает с персонажемс
_wallLeanMechanic.SetCharacterTransform(characterTransform);
} }
// Текущий режим камеры для адаптации поведения // Текущий режим камеры для адаптации поведения
private CameraMode _currentCameraMode = CameraMode.ThirdPerson; private CameraMode _currentCameraMode = CameraMode.FirstPerson;
// Инициализация контроллера // Инициализация контроллера
private void Start() // private void Start()
{ // {
motor.CharacterController = this; // Регистрация этого контроллера в движке // motor.CharacterController = this; // Регистрация этого контроллера в движке
_inputHandler.FirstPersonCameraRotation = motor.Transform.rotation; // Начальное вращение совпадает с персонажем // _inputHandler.FirstPersonCameraRotation =
} // motor.Transform.rotation; // Начальное вращение совпадает с персонажемс
// _wallLeanMechanic.SetCharacterTransform(characterTransform);
// }
// Обработка и преобразование входных данных от персонажа // Обработка и преобразование входных данных от персонажа
public void SetInputs(ref CharacterInputs inputs) public void SetInputs(ref CharacterInputs inputs)
{ {
// Обновляем таймер перезарядки слайда // Обновляем таймер перезарядки слайда
_slideMechanic.TickCooldown(Time.deltaTime); _slideMechanic.TickCooldown(Time.deltaTime);
_wallClimbMechanic.TickCooldown(Time.deltaTime);
// Если активен слайд - игнорируем весь остальной ввод // Если активен слайд - игнорируем весь остальной ввод
if (_slideMechanic.IsSliding) if (_slideMechanic.IsSliding)
{ {
@@ -79,16 +137,30 @@ namespace Code.Gameplay.Character.Controller
_groundMovement.RequestJump(inputs.JumpPressed); _groundMovement.RequestJump(inputs.JumpPressed);
return; return;
} }
// Сохраняем текущий режим камеры // Сохраняем текущий режим камеры
_currentCameraMode = inputs.CameraMode; _currentCameraMode = inputs.CameraMode;
if (isAbilitiesBlocked)
{
abilityBlockTimer -= Time.deltaTime;
if (abilityBlockTimer <= 0)
{
isAbilitiesBlocked = false;
}
else
{
inputs.SprintPressed = false;
inputs.SlidePressed = false;
}
}
// Обработка слайда // Обработка слайда
_slideMechanic.RequestSlide(inputs.SlidePressed); _slideMechanic.RequestSlide(inputs.SlidePressed);
// Обработка остального ввода // Обработка остального ввода
_inputHandler.ProcessInputs(ref inputs, motor); _inputHandler.ProcessInputs(ref inputs, motor);
// Передаём запрос прыжка напрямую в систему движения по земле // Передаём запрос прыжка напрямую в систему движения по земле
_groundMovement.RequestJump(inputs.JumpPressed); _groundMovement.RequestJump(inputs.JumpPressed);
} }
@@ -96,8 +168,7 @@ namespace Code.Gameplay.Character.Controller
// Обновление вращения персонажа на основе входных данных // Обновление вращения персонажа на основе входных данных
public void UpdateRotation(ref Quaternion currentRotation, float deltaTime) public void UpdateRotation(ref Quaternion currentRotation, float deltaTime)
{ {
// ВО ВРЕМЯ СЛАЙДА НЕ ОБНОВЛЯЕМ ВРАЩЕНИЕ - ПЕРСОНАЖ СМОТРИТ ПРЯМО if (_slideMechanic.IsSliding || _wallClimbMechanic.IsClimbingActive) return;
if (_slideMechanic.IsSliding) return;
switch (_currentCameraMode) switch (_currentCameraMode)
{ {
@@ -110,13 +181,13 @@ namespace Code.Gameplay.Character.Controller
case CameraMode.ThirdPerson: case CameraMode.ThirdPerson:
{ {
// Вращение применяется только если есть направление движения и разрешена резкость вращения // Вращение применяется только если есть направление движения и разрешена резкость вращения
if (_inputHandler.LookInputVector.sqrMagnitude > 0 && orientationSharpness > 0) if (_inputHandler.LookInputVector.sqrMagnitude > 0 && _characterConfig.orientationSharpness > 0)
{ {
// Плавная интерполяция текущего направления к целевому с экспоненциальным сглаживанием // Плавная интерполяция текущего направления к целевому с экспоненциальным сглаживанием
var smoothedLookInputDirection = Vector3.Slerp( var smoothedLookInputDirection = Vector3.Slerp(
motor.CharacterForward, motor.CharacterForward,
_inputHandler.LookInputVector, _inputHandler.LookInputVector,
1f - Mathf.Exp(-orientationSharpness * deltaTime) 1f - Mathf.Exp(-_characterConfig.orientationSharpness * deltaTime)
).normalized; ).normalized;
// Установка нового вращения смотря в сглаженном направлении // Установка нового вращения смотря в сглаженном направлении
@@ -142,11 +213,12 @@ namespace Code.Gameplay.Character.Controller
{ {
_slideMechanic.EndSlide(); _slideMechanic.EndSlide();
} }
// Проверяем запрос слайда и запускаем его если можно // Проверяем запрос слайда и запускаем его если можно
if (!_slideMechanic.IsSliding) if (!_slideMechanic.IsSliding)
{ {
_slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, _inputHandler.LookInputVector); _slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation,
_inputHandler.LookInputVector);
} }
// Если активен слайд - обрабатываем отдельно // Если активен слайд - обрабатываем отдельно
else else
@@ -154,7 +226,7 @@ namespace Code.Gameplay.Character.Controller
_slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor); _slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor);
return; return;
} }
// Если персонаж находится на земле // Если персонаж находится на земле
if (motor.GroundingStatus.IsStableOnGround) if (motor.GroundingStatus.IsStableOnGround)
{ {
@@ -168,6 +240,12 @@ namespace Code.Gameplay.Character.Controller
_groundMovement.CancelJumpRequest(); _groundMovement.CancelJumpRequest();
} }
} }
public void BlockAbilities(float duration)
{
isAbilitiesBlocked = true;
abilityBlockTimer = duration;
}
// Метод вызывается перед обновлением персонажа (можно использовать для подготовки) // Метод вызывается перед обновлением персонажа (можно использовать для подготовки)
public void BeforeCharacterUpdate(float deltaTime) public void BeforeCharacterUpdate(float deltaTime)
@@ -182,11 +260,17 @@ namespace Code.Gameplay.Character.Controller
{ {
_slideMechanic.EndSlide(); _slideMechanic.EndSlide();
} }
if (motor.GroundingStatus.IsStableOnGround)
{
_wallClimbMechanic.ResetWallClimbSession();
}
} }
// Метод вызывается после обновления персонажа (можно использовать для пост-обработки) // Метод вызывается после обновления персонажа
public void AfterCharacterUpdate(float deltaTime) public void AfterCharacterUpdate(float deltaTime)
{ {
_wallLeanMechanic.UpdateLean(motor, deltaTime);
} }
// Проверка валидности коллайдера для обработки столкновений // Проверка валидности коллайдера для обработки столкновений
@@ -196,7 +280,8 @@ namespace Code.Gameplay.Character.Controller
} }
// Обработка столкновения с землей (вызывается при приземлении) // Обработка столкновения с землей (вызывается при приземлении)
public void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) public void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint,
ref HitStabilityReport hitStabilityReport)
{ {
} }
@@ -204,11 +289,13 @@ namespace Code.Gameplay.Character.Controller
public void OnMovementHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, public void OnMovementHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint,
ref HitStabilityReport hitStabilityReport) ref HitStabilityReport hitStabilityReport)
{ {
_wallClimbMechanic.OnMovementHit(motor, hitNormal); if (_wallClimbMechanic.TryClimbWall(motor, hitNormal))
_wallLeanMechanic.OnWallContact(motor, hitNormal, motor.TransientPosition.y);
} }
// Обработка отчета о стабильности столкновения // Обработка отчета о стабильности столкновения
public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint,
Vector3 atCharacterPosition,
Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport)
{ {
} }
@@ -11,6 +11,9 @@ namespace Code.Gameplay.Character.Controller.Input
*/ */
public class CharacterInputHandler public class CharacterInputHandler
{ {
// Косинус порогового угла для спринта (предвычислен)
private readonly float _sprintAngleThresholdCos;
// Векторы движения и направления взгляда // Векторы движения и направления взгляда
public Vector3 MoveInputVector { get; set; } public Vector3 MoveInputVector { get; set; }
public Vector3 LookInputVector { get; set; } public Vector3 LookInputVector { get; set; }
@@ -23,7 +26,16 @@ namespace Code.Gameplay.Character.Controller.Input
// Флаг-свойство указывающий, что нужно применить вращение камеры к персонажу // Флаг-свойство указывающий, что нужно применить вращение камеры к персонажу
public bool ShouldApplyCameraRotation { get; private set; } public bool ShouldApplyCameraRotation { get; private set; }
// Конструктор с пороговым углом
public CharacterInputHandler(float sprintAngleThreshold)
{
// Предвычисляем косинус порогового угла для эффективности
// Угол в градусах → радианы → косинус
// Чем меньше угол — тем строже условие
_sprintAngleThresholdCos = Mathf.Cos(Mathf.Deg2Rad * sprintAngleThreshold);
}
// Обработка и преобразование входных данных от персонажа // Обработка и преобразование входных данных от персонажа
public void ProcessInputs(ref CharacterInputs inputs, KinematicCharacterMotor motor) public void ProcessInputs(ref CharacterInputs inputs, KinematicCharacterMotor motor)
{ {
@@ -31,7 +43,7 @@ namespace Code.Gameplay.Character.Controller.Input
var currentCameraMode = inputs.CameraMode; var currentCameraMode = inputs.CameraMode;
// Обработка спринта // Обработка спринта
IsSprinting = inputs is { SprintPressed: true, MoveAxisForward: > 0.1f }; // Спринт только при движении вперёд IsSprinting = inputs.SprintPressed && inputs.MoveAxisForward > _sprintAngleThresholdCos; // Спринт только при движении на заданный в конфиге угол
// Нормализация вектора движения и ограничение его величины до 1 // Нормализация вектора движения и ограничение его величины до 1
var moveInputVector = var moveInputVector =
@@ -101,7 +113,7 @@ namespace Code.Gameplay.Character.Controller.Input
var cameraForward = cameraRotation * Vector3.forward; var cameraForward = cameraRotation * Vector3.forward;
// Проецируем на горизонтальную плоскость // Проецируем на горизонтальную плоскость
var horizontalForward = Vector3.ProjectOnPlane(cameraForward, motor.CharacterUp).normalized; var horizontalForward = Vector3.ProjectOnPlane(cameraForward, Vector3.up).normalized;
// Если проекция вырождена, используем текущее направление персонажа // Если проекция вырождена, используем текущее направление персонажа
if (horizontalForward.sqrMagnitude < 0.01f) if (horizontalForward.sqrMagnitude < 0.01f)
@@ -1,11 +1,9 @@
using Code.Gameplay.Camera; using Code.Gameplay.Camera;
using Code.Gameplay.Camera.CameraStates; using Code.Gameplay.Camera.CameraStates;
using Code.Gameplay.Character.Camera;
using Code.Gameplay.Character.Controller; using Code.Gameplay.Character.Controller;
using Code.Input; using Code.Input;
using Reflex.Attributes; using Reflex.Attributes;
using UnityEngine; using UnityEngine;
using UnityEngine.Serialization;
namespace Code.Gameplay.Character.Core namespace Code.Gameplay.Character.Core
{ {
@@ -17,7 +15,7 @@ namespace Code.Gameplay.Character.Core
*/ */
public class Character : MonoBehaviour public class Character : MonoBehaviour
{ {
[Inject] private CameraStateMachine _cameraService; // Компонент камеры персонажа [Inject] private CameraStateMachine _cameraStateMachine; // Стейт-машина камеры (нужна для транзакций и т.п.)
[SerializeField] private CustomCharacterController customCharacterController; // Компонент управления персонажем [SerializeField] private CustomCharacterController customCharacterController; // Компонент управления персонажем
[SerializeField] private Transform cameraFollowPoint; // Точка следования камеры (уровень глаз для FirstPerson) [SerializeField] private Transform cameraFollowPoint; // Точка следования камеры (уровень глаз для FirstPerson)
@@ -30,7 +28,7 @@ namespace Code.Gameplay.Character.Core
private void Start() private void Start()
{ {
// Настройка камеры для следования за точкой камеры // Настройка камеры для следования за точкой камеры
((FirstPersonState)_cameraService.CurrentBaseState).SetFollowTransform(cameraFollowPoint); ((BaseCameraState)_cameraStateMachine.CurrentBaseState).SetFollowTransform(cameraFollowPoint);
// Включение Gameplay мапа ввода // Включение Gameplay мапа ввода
_inputService.EnableMap(ActionMappings.Gameplay); _inputService.EnableMap(ActionMappings.Gameplay);
@@ -49,9 +47,11 @@ namespace Code.Gameplay.Character.Core
inputs.JumpPressed = _inputService.JumpPressed; // Прыжки (Space) inputs.JumpPressed = _inputService.JumpPressed; // Прыжки (Space)
inputs.SprintPressed = _inputService.SprintPressed; // Спринт (Shift) inputs.SprintPressed = _inputService.SprintPressed; // Спринт (Shift)
inputs.SlidePressed = _inputService.SlidePressed; // Слайд (Ctrl) inputs.SlidePressed = _inputService.SlidePressed; // Слайд (Ctrl)
inputs.CameraMode = ((ICameraState)_cameraStateMachine.CurrentBaseState).GetCameraMode();
// Передаем информацию о камере для адаптивного поведения // Передаем информацию о камере для адаптивного поведения
inputs.CameraRotation = _cameraService.transform.rotation; inputs.CameraRotation = ((BaseCameraState)_cameraStateMachine.CurrentBaseState).GetCameraRotation();
inputs.CameraPlanarDirection = ((BaseCameraState)_cameraStateMachine.CurrentBaseState).GetPlanarDirection();
// Передаем ввод в контроллер персонажа // Передаем ввод в контроллер персонажа
customCharacterController.SetInputs(ref inputs); customCharacterController.SetInputs(ref inputs);
@@ -67,7 +67,7 @@ namespace Code.Gameplay.Character.Core
{ {
// Обрабатываем нажатие только один раз при первом обнаружении // Обрабатываем нажатие только один раз при первом обнаружении
case true when !_cameraModeToggleHandled: case true when !_cameraModeToggleHandled:
// ToggleCameraMode(); ToggleCameraMode();
_cameraModeToggleHandled = true; _cameraModeToggleHandled = true;
break; break;
// Сбрасываем флаг когда кнопка отпущена // Сбрасываем флаг когда кнопка отпущена
@@ -76,6 +76,17 @@ namespace Code.Gameplay.Character.Core
break; break;
} }
} }
// Метод для переключения режимов камеры
private void ToggleCameraMode()
{
var currentMode = ((ICameraState)_cameraStateMachine.CurrentBaseState).GetCameraMode();
var newMode = currentMode == CameraMode.FirstPerson ? CameraMode.ThirdPerson : CameraMode.FirstPerson;
_cameraStateMachine.SwitchState(newMode);
((BaseCameraState)_cameraStateMachine.CurrentBaseState).SetFollowTransform(cameraFollowPoint);
}
// Обновление логики каждый кадр // Обновление логики каждый кадр
private void Update() private void Update()
@@ -86,10 +97,5 @@ namespace Code.Gameplay.Character.Core
// Обработка переключения режима камеры // Обработка переключения режима камеры
HandleCameraModeInput(); HandleCameraModeInput();
} }
// Обновление после всех вычислений (идеально для камеры)
private void LateUpdate()
{
}
} }
} }
@@ -0,0 +1,56 @@
using Code.Gameplay.Camera;
using Code.Gameplay.Camera.CameraStates;
using Code.Gameplay.Character.Controller;
using Code.Input;
using Reflex.Attributes;
using UnityEngine;
namespace Code.Gameplay.Character.Core
{
public class CharacterProvider : MonoBehaviour
{
[Inject] private CameraStateMachine _cameraStateMachine; // Стейт-машина камеры (нужна для транзакций и т.п.)
[SerializeField] private CustomCharacterController customCharacterController; // Компонент управления персонажем
public CustomCharacterController CustomCharacterController => customCharacterController;
[SerializeField] private Transform cameraFollowPoint; // Точка следования камеры (уровень глаз для FirstPerson)
public Transform GetFollowTransform() => cameraFollowPoint;
[Inject] private IInputService _inputService; // Сервис ввода через DI
public void Teleport(Vector3 position, Quaternion rotation)
{
customCharacterController.SetPosition(position);
customCharacterController.SetRotation(rotation);
}
// Обработка ввода для движения персонажа
private void HandleCharacterInput()
{
// Создаем структуру ввода с полной информацией о камере
var inputs = new CharacterInputs();
// Получаем данные движения из Input System
var moveInput = _inputService.MoveInput;
inputs.MoveAxisForward = moveInput.y; // Vertical ось (W/S или Up/Down)
inputs.MoveAxisRight = moveInput.x; // Horizontal ось (A/D или Left/Right)
inputs.JumpPressed = _inputService.JumpPressed; // Прыжки (Space)
inputs.SprintPressed = true;//_inputService.SprintPressed; // Спринт (Shift)
inputs.SlidePressed = _inputService.SlidePressed; // Слайд (Ctrl)
inputs.CameraMode = ((ICameraState)_cameraStateMachine.CurrentBaseState).GetCameraMode();
// Передаем информацию о камере для адаптивного поведения
inputs.CameraRotation = ((BaseCameraState)_cameraStateMachine.CurrentBaseState).GetCameraRotation();
inputs.CameraPlanarDirection = ((BaseCameraState)_cameraStateMachine.CurrentBaseState).GetPlanarDirection();
// Передаем ввод в контроллер персонажа
customCharacterController.SetInputs(ref inputs);
}
// Обновление логики каждый кадр
private void Update()
{
// Обработка ввода для движения в Update (физика)
HandleCharacterInput();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b2d4b8256c1f4b3facf6b8d0a7d5256b
timeCreated: 1763217123
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b7ba7b62b1aa47c48dc82ac8d8e8530e
timeCreated: 1763548306
@@ -0,0 +1,23 @@
using DG.Tweening;
using UnityEngine;
namespace Code.Gameplay.Door
{
public class CinematicDoorOpening
{
public DamnDoor DamnDoor;
public Light DoorLight;
public void PreOpen(bool start)
{
DoorLight.DOColor(start ? Color.yellow : Color.red, 0.2f);
DamnDoor.Petlya.DORotate(new Vector3(0, -40, 0), 1f);
}
public void Close()
{
DamnDoor.Petlya.DORotate(new Vector3(0, 0, 0), 1f);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: deda0339570d44609dd62eafd9c6215f
timeCreated: 1763548908
@@ -0,0 +1,16 @@
using UnityEngine;
namespace Code.Gameplay.Door
{
public class DamnDoor : MonoBehaviour
{
[SerializeField] Rigidbody rigidbody;
[SerializeField] private Transform petlya;
public Transform Petlya => petlya;
public void SetPhysics(bool active)
{
rigidbody.isKinematic = active;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 50a6b5e332704611b19f5a209d4dbffe
timeCreated: 1763548314
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3b36108558824d748620db29707cd2b1
timeCreated: 1763228467
@@ -0,0 +1,202 @@
using UnityEngine;
using UnityEngine.AI;
using Code.Gameplay.Player;
using Code.Gameplay.Character.Controller;
using Code.Gameplay.Character.Core;
using Reflex.Attributes;
using Reflex.Core;
using Reflex.Injectors;
namespace Code.Gameplay.Enemies
{
public abstract class BaseEnemy : MonoBehaviour
{
[SerializeField] protected NavMeshAgent agent;
[SerializeField] protected Collider enemyCollider;
protected int Damage;
protected float AttackCooldown;
protected float DetectionRange;
protected float AttackRange;
protected float Speed;
protected bool IsStunnable;
protected float StunDuration;
protected bool UsesIdleMovement;
protected float IdleWanderRadius;
protected float IdleMoveDurationMin;
protected float IdleMoveDurationMax;
protected float IdlePauseDurationMin;
protected float IdlePauseDurationMax;
private bool _isStunned;
private float _currentStunTimer;
private bool _canAttack = true;
private float _attackCooldownTimer;
private enum IdleState { Moving, Paused }
private IdleState _currentIdleState = IdleState.Paused;
private float _idleTimer;
private Vector3 _idleTargetPosition;
[Inject] private IPlayerProvider _playerProvider;
protected CharacterProvider Player => _playerProvider?.Character;
protected PlayerHealth PlayerHealth => _playerProvider?.Health;
protected CustomCharacterController PlayerController => Player?.CustomCharacterController;
protected virtual void Awake()
{
if (_playerProvider == null)
{
AttributeInjector.Inject(this, Container.ProjectContainer);
}
}
protected virtual void Update()
{
if (!Player) return;
HandleStun();
HandleAttackCooldown();
if (_isStunned) return;
HandleMovement();
HandleAttack();
}
protected virtual void HandleMovement()
{
if (!Player || !agent) return;
var distanceToPlayer = Vector3.Distance(transform.position, Player.transform.position);
if (distanceToPlayer <= DetectionRange)
{
agent.isStopped = false;
agent.SetDestination(Player.transform.position);
}
else if (UsesIdleMovement)
{
HandleIdleMovement();
}
else
{
agent.isStopped = true;
}
}
protected virtual void HandleIdleMovement()
{
_idleTimer -= Time.deltaTime;
switch (_currentIdleState)
{
case IdleState.Paused:
if (_idleTimer <= 0f)
{
// Start moving to a new random point
_currentIdleState = IdleState.Moving;
SetNewIdleDestination();
_idleTimer = Random.Range(IdleMoveDurationMin, IdleMoveDurationMax);
agent.isStopped = false;
}
break;
case IdleState.Moving:
if (_idleTimer <= 0f || HasReachedIdleDestination())
{
// Stop and pause
_currentIdleState = IdleState.Paused;
_idleTimer = Random.Range(IdlePauseDurationMin, IdlePauseDurationMax);
agent.isStopped = true;
}
break;
}
}
private void SetNewIdleDestination()
{
Vector3 randomPoint = transform.position + Random.insideUnitSphere * IdleWanderRadius;
NavMeshHit hit;
if (NavMesh.SamplePosition(randomPoint, out hit, 3f, agent.areaMask))
{
_idleTargetPosition = hit.position;
agent.SetDestination(_idleTargetPosition);
}
else
{
// Fallback: stay
_idleTargetPosition = transform.position;
}
}
private bool HasReachedIdleDestination()
{
return agent.remainingDistance <= agent.stoppingDistance;
}
protected virtual void HandleAttack() { }
protected virtual void HandleStun()
{
if (!_isStunned || !IsStunnable) return;
_currentStunTimer -= Time.deltaTime;
if (!(_currentStunTimer <= 0)) return;
_isStunned = false;
agent.isStopped = false;
enemyCollider.enabled = true;
}
protected virtual void ApplyStun()
{
_isStunned = true;
_currentStunTimer = StunDuration;
agent.isStopped = true;
enemyCollider.enabled = false;
}
protected virtual void HandleAttackCooldown()
{
if (!_canAttack)
{
_attackCooldownTimer -= Time.deltaTime;
if (_attackCooldownTimer <= 0)
{
_canAttack = true;
}
}
}
protected virtual void DealDamage()
{
if (!_canAttack || PlayerHealth == null) return;
PlayerHealth.GetDamage(Damage);
ApplyEffectsToPlayer();
_canAttack = false;
_attackCooldownTimer = AttackCooldown;
}
protected virtual void ApplyEffectsToPlayer()
{
PlayerController.BlockAbilities(3f);
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, DetectionRange);
Gizmos.color = new Color(0.3f, 0.3f, 0.8f, 0.4f);
Gizmos.DrawSphere(transform.position, AttackRange);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 691006ba0f69448787db8f350ecf32f4
timeCreated: 1763228480
@@ -0,0 +1,55 @@
using Code.Configs;
using Reflex.Attributes;
using UnityEngine;
using UnityEngine.AI;
namespace Code.Gameplay.Enemies
{
public class Cockroach : BaseEnemy
{
[Inject]
private void Construct(CockroachConfig cockroachConfig)
{
Damage = cockroachConfig.damage;
AttackCooldown = cockroachConfig.attackCooldown;
DetectionRange = cockroachConfig.detectionRange;
AttackRange = cockroachConfig.attackRange;
Speed = cockroachConfig.speed;
IsStunnable = cockroachConfig.isStunnable;
StunDuration = cockroachConfig.stunDuration;
UsesIdleMovement = cockroachConfig.usesIdleMovement;
IdleWanderRadius = cockroachConfig.idleWanderRadius;
IdleMoveDurationMin = cockroachConfig.idleMoveDurationMin;
IdleMoveDurationMax = cockroachConfig.idleMoveDurationMax;
IdleMoveDurationMin = cockroachConfig.idleMoveDurationMin;
IdleMoveDurationMax = cockroachConfig.idleMoveDurationMax;
IdlePauseDurationMin = cockroachConfig.idlePauseDurationMin;
IdlePauseDurationMax = cockroachConfig.idlePauseDurationMax;
}
protected override void Awake()
{
base.Awake();
if (agent == null) return;
var walkableArea = NavMesh.GetAreaFromName("Cockroach");
agent.areaMask = 1 << walkableArea;
}
protected override void HandleAttack()
{
if (!Player) return;
var distance = Vector3.Distance(transform.position, Player.transform.position);
if (!(distance <= AttackRange)) return;
DealDamage();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 119c0b675c944ed7ada6cdc8db4b04aa
timeCreated: 1763228933
@@ -0,0 +1,58 @@
using Code.Configs;
using Reflex.Attributes;
using UnityEngine;
using UnityEngine.AI;
namespace Code.Gameplay.Enemies
{
public class DrunkenGrandpa : BaseEnemy
{
[Inject]
private void Construct(DrunkenGrandpaConfig drunkenGrandpaConfig)
{
Damage = drunkenGrandpaConfig.damage;
AttackCooldown = drunkenGrandpaConfig.attackCooldown;
DetectionRange = drunkenGrandpaConfig.detectionRange;
AttackRange = drunkenGrandpaConfig.attackRange;
Speed = drunkenGrandpaConfig.speed;
IsStunnable = drunkenGrandpaConfig.isStunnable;
StunDuration = drunkenGrandpaConfig.stunDuration;
UsesIdleMovement = drunkenGrandpaConfig.usesIdleMovement;
IdleWanderRadius = drunkenGrandpaConfig.idleWanderRadius;
IdleMoveDurationMin = drunkenGrandpaConfig.idleMoveDurationMin;
IdleMoveDurationMax = drunkenGrandpaConfig.idleMoveDurationMax;
IdleMoveDurationMin = drunkenGrandpaConfig.idleMoveDurationMin;
IdleMoveDurationMax = drunkenGrandpaConfig.idleMoveDurationMax;
IdlePauseDurationMin = drunkenGrandpaConfig.idlePauseDurationMin;
IdlePauseDurationMax = drunkenGrandpaConfig.idlePauseDurationMax;
}
protected override void Awake()
{
base.Awake();
if (agent == null) return;
var walkableArea = NavMesh.GetAreaFromName("Walkable");
agent.areaMask = 1 << walkableArea;
}
protected override void HandleAttack()
{
if (!Player) return;
var distance = Vector3.Distance(transform.position, Player.transform.position);
if (!(distance <= AttackRange)) return;
if (Player.CustomCharacterController.IsSliding)
ApplyStun();
else
DealDamage();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5e77164596e249bca15e09cdfc9ddac2
timeCreated: 1763228910
Loaded 100 of 621 files, more files were not shown because too many files have changed in this diff. Show more