feat(platforms): add new platform types and enhance player interactions

This commit is contained in:
Данил Ярош 2025-11-19 16:59:50 +03:00
parent abe90fcfce
commit 47b43cf122
54 changed files with 51932 additions and 5438 deletions

View File

@ -2,6 +2,7 @@
<dictionary name="project">
<words>
<w>event'ов</w>
<w>renderer'ы</w>
</words>
</dictionary>
</component>

View File

@ -4478,4 +4478,18 @@ AnimationClip:
flags: 0
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
m_Events:
- time: 0
functionName: OnDeathBegin
data:
objectReferenceParameter: {fileID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
- time: 0.6666667
functionName: OnDeathEnd
data:
objectReferenceParameter: {fileID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0

View File

@ -387,7 +387,7 @@ AnimationClip:
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 1
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0

View File

@ -1003,7 +1003,7 @@ AnimationClip:
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 1
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0

View File

@ -328,7 +328,7 @@ AnimationClip:
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 1
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0

View File

@ -724,7 +724,7 @@ AnimationClip:
m_Level: 0
m_CycleOffset: 0
m_HasAdditiveReferencePose: 0
m_LoopTime: 1
m_LoopTime: 0
m_LoopBlend: 0
m_LoopBlendOrientation: 0
m_LoopBlendPositionY: 0
@ -2934,4 +2934,11 @@ AnimationClip:
flags: 0
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
m_Events:
- time: 0
functionName: Jump
data:
objectReferenceParameter: {fileID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0

View File

@ -25,34 +25,6 @@ AnimatorStateTransition:
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &-6839088151083392861
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 3
m_ConditionEvent: VSpeed
m_EventTreshold: 10
- m_ConditionMode: 1
m_ConditionEvent: Jump
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 2683883220764761602}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0.75
m_HasExitTime: 0
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &-6437098828268326780
AnimatorStateTransition:
m_ObjectHideFlags: 1
@ -70,11 +42,11 @@ AnimatorStateTransition:
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0.25
m_HasExitTime: 1
m_HasFixedDuration: 1
m_ExitTime: 0
m_HasExitTime: 0
m_HasFixedDuration: 0
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
@ -86,7 +58,7 @@ AnimatorState:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Jump_Fall
m_Speed: 1
m_Speed: 2
m_CycleOffset: 0
m_Transitions:
- {fileID: 5962895747233769529}
@ -105,6 +77,31 @@ AnimatorState:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1101 &-5665309061377477553
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 1
m_ConditionEvent: Grounded
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: -3989795846899303455}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0.25
m_HasExitTime: 0
m_HasFixedDuration: 0
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &-3989795846899303455
AnimatorState:
serializedVersion: 6
@ -145,6 +142,7 @@ AnimatorState:
m_Transitions:
- {fileID: 7699157278345549200}
- {fileID: -9048563795446365488}
- {fileID: -1366041084176649225}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
@ -160,6 +158,34 @@ AnimatorState:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1101 &-2496720488816828561
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 3
m_ConditionEvent: VSpeed
m_EventTreshold: 10
- m_ConditionMode: 1
m_ConditionEvent: Jump
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 2683883220764761602}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0.75
m_HasExitTime: 0
m_HasFixedDuration: 0
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &-2059045626719700312
AnimatorState:
serializedVersion: 6
@ -201,34 +227,86 @@ AnimatorStateMachine:
m_Position: {x: 890, y: 220, z: 0}
- serializedVersion: 1
m_State: {fileID: -3208104225031276435}
m_Position: {x: 890, y: 340, z: 0}
m_Position: {x: 900, y: 320, z: 0}
- serializedVersion: 1
m_State: {fileID: -2059045626719700312}
m_Position: {x: 100, y: 50, z: 0}
m_Position: {x: 560, y: -160, z: 0}
- serializedVersion: 1
m_State: {fileID: 2683883220764761602}
m_Position: {x: 330, y: 100, z: 0}
m_Position: {x: 330, y: 170, z: 0}
- serializedVersion: 1
m_State: {fileID: 3056242929772600144}
m_Position: {x: 330, y: 180, z: 0}
m_Position: {x: 190, y: 320, z: 0}
- serializedVersion: 1
m_State: {fileID: -6196007508092385579}
m_Position: {x: 330, y: 260, z: 0}
m_Position: {x: 500, y: 320, z: 0}
- serializedVersion: 1
m_State: {fileID: -3989795846899303455}
m_Position: {x: 330, y: 340, z: 0}
m_Position: {x: 330, y: 450, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions:
- {fileID: 2884326239320347019}
- {fileID: -6839088151083392861}
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 350, y: -60, z: 0}
m_AnyStatePosition: {x: 580, y: -260, z: 0}
m_EntryPosition: {x: 1140, y: 220, z: 0}
m_ExitPosition: {x: 640, y: 50, z: 0}
m_ExitPosition: {x: 870, y: -150, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 8507286312630441258}
--- !u!1101 &-1366041084176649225
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 3
m_ConditionEvent: VSpeed
m_EventTreshold: 10
- m_ConditionMode: 1
m_ConditionEvent: Jump
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 2683883220764761602}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0.75
m_HasExitTime: 0
m_HasFixedDuration: 0
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &-505540794586266171
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 3
m_ConditionEvent: VSpeed
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: -6196007508092385579}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0
m_HasExitTime: 0
m_HasFixedDuration: 0
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &-426421497963076622
AnimatorStateTransition:
m_ObjectHideFlags: 1
@ -328,6 +406,31 @@ AnimatorStateTransition:
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1564265989192692461
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 3
m_ConditionEvent: VSpeed
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 3056242929772600144}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0
m_HasExitTime: 0
m_HasFixedDuration: 0
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &2683883220764761602
AnimatorState:
serializedVersion: 6
@ -339,8 +442,8 @@ AnimatorState:
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 5820879251909814290}
- {fileID: 3010298919337203651}
- {fileID: 1564265989192692461}
- {fileID: -505540794586266171}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
@ -381,34 +484,6 @@ AnimatorStateTransition:
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &3010298919337203651
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 2
m_ConditionEvent: Jump
m_EventTreshold: 0
- m_ConditionMode: 1
m_ConditionEvent: Grounded
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 0}
m_Solo: 0
m_Mute: 0
m_IsExit: 1
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &3056242929772600144
AnimatorState:
serializedVersion: 6
@ -417,10 +492,11 @@ AnimatorState:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Jump_Mid-Air
m_Speed: 1
m_Speed: 2
m_CycleOffset: 0
m_Transitions:
- {fileID: -6437098828268326780}
- {fileID: -5665309061377477553}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
@ -453,36 +529,11 @@ AnimatorStateTransition:
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0.25
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &5820879251909814290
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name:
m_Conditions:
- m_ConditionMode: 3
m_ConditionEvent: VSpeed
m_EventTreshold: 0
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 3056242929772600144}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0
m_HasExitTime: 1
m_HasFixedDuration: 1
m_HasExitTime: 0
m_HasFixedDuration: 0
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
@ -503,11 +554,11 @@ AnimatorStateTransition:
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionDuration: 0
m_TransitionOffset: 0
m_ExitTime: 0.5
m_HasExitTime: 1
m_HasFixedDuration: 1
m_HasExitTime: 0
m_HasFixedDuration: 0
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
@ -548,6 +599,7 @@ AnimatorState:
m_CycleOffset: 0
m_Transitions:
- {fileID: 144947006081674025}
- {fileID: -2496720488816828561}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0

View File

@ -9065,4 +9065,25 @@ AnimationClip:
flags: 0
m_HasGenericRootTransform: 0
m_HasMotionFloatCurves: 0
m_Events: []
m_Events:
- time: 0.16666667
functionName: Step
data:
objectReferenceParameter: {fileID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
- time: 0.5
functionName: Step
data:
objectReferenceParameter: {fileID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
- time: 0.8333333
functionName: Step
data:
objectReferenceParameter: {fileID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2625,8 +2625,8 @@ GameObject:
- component: {fileID: 6550761235523395915}
- component: {fileID: -592641019219869902}
- component: {fileID: 683509761572695280}
- component: {fileID: 5723917688524288437}
- component: {fileID: 9068111837403598740}
- component: {fileID: 5723917688524288437}
- component: {fileID: 6106630833600239313}
- component: {fileID: -953906007136484429}
m_Layer: 0
@ -2762,6 +2762,20 @@ PolygonCollider2D:
- {x: 0.922113, y: 0.029278636}
- {x: 1.135083, y: 3.0278835}
m_UseDelaunayMesh: 0
--- !u!114 &9068111837403598740
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6550761235523395909}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1a9fe08f70754d1fa12c13ac4cd190e3, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.Health
maxHealth: 3
currentHealth: 0
--- !u!114 &5723917688524288437
MonoBehaviour:
m_ObjectHideFlags: 0
@ -2791,20 +2805,6 @@ MonoBehaviour:
stepSounds: {fileID: 11400000, guid: 37fe07ddcac109a46a86ba44df16d551, type: 2}
hurtSounds: {fileID: 11400000, guid: 89e7f713913aecb40ada8c2f27c69303, type: 2}
jumpSounds: {fileID: 11400000, guid: 9a63566742cc2d84c89928c8b988e4eb, type: 2}
--- !u!114 &9068111837403598740
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6550761235523395909}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1a9fe08f70754d1fa12c13ac4cd190e3, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.Health
maxHealth: 3
currentHealth: 0
--- !u!114 &6106630833600239313
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@ -1284,8 +1284,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 1a9fe08f70754d1fa12c13ac4cd190e3, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Game2D.Behaviour.Health
maxHealth: 1
currentHealth: 1
maxHealth: 3
currentHealth: 0
--- !u!114 &8121578573349881196
MonoBehaviour:
m_ObjectHideFlags: 0
@ -1311,7 +1311,7 @@ MonoBehaviour:
serializedVersion: 2
m_Bits: 64
animator: {fileID: 6400385618584986155}
maxHealth: 3
maxHealth: 1
stepSounds: {fileID: 11400000, guid: 37fe07ddcac109a46a86ba44df16d551, type: 2}
hurtSounds: {fileID: 11400000, guid: 89e7f713913aecb40ada8c2f27c69303, type: 2}
jumpSounds: {fileID: 11400000, guid: 9a63566742cc2d84c89928c8b988e4eb, type: 2}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -214,7 +214,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!224 &28542763
RectTransform:
m_ObjectHideFlags: 0
@ -1591,9 +1591,9 @@ RectTransform:
m_Father: {fileID: 2054206196}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 1, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: -17}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 1, y: 1}
--- !u!114 &273493570
MonoBehaviour:
@ -1883,9 +1883,9 @@ RectTransform:
m_Father: {fileID: 2054206196}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -17, y: 20}
m_SizeDelta: {x: 0, y: 20}
m_Pivot: {x: 0, y: 0}
--- !u!114 &328533400
MonoBehaviour:
@ -2156,7 +2156,7 @@ RectTransform:
m_Father: {fileID: 112549352}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
@ -3285,9 +3285,9 @@ RectTransform:
m_Father: {fileID: 265801313}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -17, y: 20}
m_SizeDelta: {x: 0, y: 20}
m_Pivot: {x: 0, y: 0}
--- !u!114 &787002084
MonoBehaviour:
@ -4106,7 +4106,7 @@ RectTransform:
m_Father: {fileID: 38989454}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@ -4583,7 +4583,7 @@ RectTransform:
m_Father: {fileID: 1819180688}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
@ -4954,7 +4954,7 @@ RectTransform:
m_Father: {fileID: 96234448}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 40, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@ -5029,7 +5029,7 @@ RectTransform:
m_Father: {fileID: 47570575}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@ -5273,7 +5273,7 @@ RectTransform:
m_Father: {fileID: 837630039}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
@ -6345,9 +6345,9 @@ RectTransform:
m_Father: {fileID: 265801313}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -17, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1461740380
MonoBehaviour:
@ -6433,7 +6433,7 @@ RectTransform:
m_Father: {fileID: 462618672}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@ -6680,7 +6680,7 @@ RectTransform:
m_Father: {fileID: 244828024}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@ -6948,9 +6948,9 @@ RectTransform:
m_Father: {fileID: 2054206196}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -17, y: -17}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1549457647
MonoBehaviour:
@ -8536,7 +8536,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0.0005065672}
m_AnchoredPosition: {x: 0, y: 0.00004450919}
m_SizeDelta: {x: 0, y: 300}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1792218764
@ -9686,7 +9686,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!224 &1932206569
RectTransform:
m_ObjectHideFlags: 0
@ -10329,7 +10329,7 @@ RectTransform:
m_Father: {fileID: 1071857129}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
@ -10549,7 +10549,7 @@ RectTransform:
m_Father: {fileID: 1638546234}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 40, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@ -10624,7 +10624,7 @@ RectTransform:
m_Father: {fileID: 2055356562}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 40, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@ -10835,7 +10835,7 @@ RectTransform:
m_Father: {fileID: 1411940924}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 40, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@ -15316,8 +15316,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 27
m_Sprite: {fileID: -3144633359940803573, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -15721,8 +15721,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 15
m_Sprite: {fileID: 4013115251250039491, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -15833,8 +15833,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 8
m_Sprite: {fileID: 5688498731606456266, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -15927,8 +15927,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 13
m_Sprite: {fileID: -7314516758093373832, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -16166,8 +16166,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 29
m_Sprite: {fileID: -5122409871508779338, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -16224,8 +16224,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 19
m_Sprite: {fileID: -1334230109140272425, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -16300,8 +16300,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 26
m_Sprite: {fileID: 3129511523439360540, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -16711,8 +16711,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 2
m_Sprite: {fileID: -8320496513077812493, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -16871,8 +16871,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 23
m_Sprite: {fileID: -4410586660626731876, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -17343,8 +17343,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 6
m_Sprite: {fileID: -962209174622309214, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -17886,8 +17886,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 10
m_Sprite: {fileID: -164839347033225330, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -24756,8 +24756,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 3
m_Sprite: {fileID: 4915747386952914504, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -24889,8 +24889,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 31
m_Sprite: {fileID: -6961950492574641117, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -25066,8 +25066,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 20
m_Sprite: {fileID: -1549096108764380935, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -33172,8 +33172,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 7
m_Sprite: {fileID: -5661695156105813953, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -33375,8 +33375,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 5
m_Sprite: {fileID: 3846603968152241411, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -33512,8 +33512,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 28
m_Sprite: {fileID: 150988668789867834, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -37628,8 +37628,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 24
m_Sprite: {fileID: -921071596155124139, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -38243,8 +38243,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 17
m_Sprite: {fileID: 5546884723567340426, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -38317,8 +38317,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 30
m_Sprite: {fileID: 1727280087122091534, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -38556,8 +38556,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 11
m_Sprite: {fileID: 9202984404932711791, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -41034,8 +41034,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 18
m_Sprite: {fileID: 4343387480515338132, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -41257,8 +41257,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 21
m_Sprite: {fileID: 3753856189432605465, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@ -41484,8 +41484,8 @@ SpriteRenderer:
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 1159882347
m_SortingLayer: -1
m_SortingOrder: 14
m_Sprite: {fileID: 6783752571112214151, guid: c0941254ff0f017419c681f7d6d44584, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}

View File

@ -0,0 +1,13 @@
using System;
using UnityEngine;
namespace RR.Game2D.Core
{
public enum Direction2D
{
Up,
Down,
Left,
Right
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3a5aaacda43707540828f858a46a6a6d

View File

@ -3,31 +3,34 @@ using UnityEngine;
using RR.Game2D.Core.Managers;
public class CinemachineFindPlayer : MonoBehaviour
namespace RR.Game2D.Behaviour.Camera
{
private GameObject _tPlayer;
private Transform _tFollowTarget;
private CinemachineCamera _virtualCamera;
void Start()
public class CinemachineFindPlayer : MonoBehaviour
{
_virtualCamera = GetComponent<CinemachineCamera>();
if (_tPlayer != null) return;
_tPlayer = GameManager.Instance.PlayerObj;
if (_tPlayer == null) _tPlayer = GameObject.FindWithTag("Player");
private GameObject _tPlayer;
private Transform _tFollowTarget;
private CinemachineCamera _virtualCamera;
}
void Start()
{
_virtualCamera = GetComponent<CinemachineCamera>();
void FixedUpdate()
{
if (_tPlayer == null) return;
_tFollowTarget = _tPlayer.transform;
_virtualCamera.LookAt = _tFollowTarget;
_virtualCamera.Follow = _tFollowTarget;
if (_tPlayer != null) return;
_tPlayer = GameManager.Instance.PlayerObj;
if (_tPlayer == null) _tPlayer = GameObject.FindWithTag("Player");
}
void FixedUpdate()
{
if (_tPlayer == null) return;
_tFollowTarget = _tPlayer.transform;
_virtualCamera.LookAt = _tFollowTarget;
_virtualCamera.Follow = _tFollowTarget;
}
}
}

View File

@ -26,7 +26,7 @@ namespace RR.Game2D.Entities
private string PrefKey => $"CP_{SceneManager.GetActiveScene().name}";
void Awake()
private void Awake()
{
if (Instance && Instance != this)
{

View File

@ -11,7 +11,7 @@ namespace RR.Game2D.Entities
[Tooltip("Направление/вектор взгляда по умолчанию")]
public Vector2 defaultForward = Vector2.right;
void Start()
private void Start()
{
// Пытаемся получить позицию из CheckpointSystem, если он есть.
Vector3 spawnPos = defaultSpawnPosition;

View File

@ -1,6 +1,7 @@
using UnityEngine;
using RR.Common.Interfaces;
using RR.Common.Utilities;
using RR.Game2D.Core.Managers;
using RR.Game2D.Behaviour.ScriptableObjects;

View File

@ -17,7 +17,7 @@ namespace RR.Game2D.Behaviour
{
[Header("Управление")]
[SerializeField] private InputActionReference moveAction; // Value(Vector2), например "Move" (A/D, ←/→, stick)
[SerializeField] private InputActionReference moveAction;
[SerializeField] private InputActionReference jumpAction;
[Header("Горизонтальное движение")]
@ -64,19 +64,19 @@ namespace RR.Game2D.Behaviour
private static readonly int GroundedAnimator = Animator.StringToHash("Grounded");
private static readonly int Death = Animator.StringToHash("Death");
void OnEnable()
private void OnEnable()
{
moveAction.action.Enable();
jumpAction.action.Enable();
}
void OnDisable()
private void OnDisable()
{
moveAction.action.Disable();
jumpAction.action.Disable();
}
void Awake()
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
//col = GetComponent<Collider2D>();
@ -102,7 +102,7 @@ namespace RR.Game2D.Behaviour
_health.SetMaxHealth(maxHealth);
}
void Update()
private void Update()
{
// --- Ввод ---
Vector2 move = moveAction ? moveAction.action.ReadValue<Vector2>() : Vector2.zero;
@ -196,7 +196,7 @@ namespace RR.Game2D.Behaviour
/// Метод для проверки соприкосновения с землей
/// </summary>
/// <returns>bool isGrounded - на земле ли игрок</returns>
bool Grounded()
private bool Grounded()
{
Collider2D hit = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundMask);
bool isGrounded = hit != null

View File

@ -1,96 +0,0 @@
using System;
using UnityEngine;
namespace RR.Game2D.Behaviour
{
[RequireComponent(typeof(BoxCollider2D))]
public class JumpPad2D : MonoBehaviour
{
[SerializeField] private float verticalBand = 0.5f;
private BoxCollider2D _col;
public Action<GameObject> PlayerJumped;
private void Awake()
{
_col = GetComponent<BoxCollider2D>();
if (!_col.isTrigger)
_col.isTrigger = true;
}
//private void OnTriggerEnter2D(Collider2D other)
//{
// var d = col.Distance(other);
// Vector2 contactMid = (d.pointA + d.pointB) * 0.5f;
// bool topByNormal = IsTopByNormal(col, d.normal, maxAngleDeg: 30f);
// bool topByBox = false;
// if (col is BoxCollider2D box)
// topByBox = IsTopByLocalY(box, contactMid, bandPercent: bandPercent);
// bool isTopContact = topByNormal || topByBox;
// if (isTopContact)
// {
// if (other.CompareTag("Player"))
// {
// PlayerController playerController = other.GetComponent<PlayerController>();
// Rigidbody2D playerRB = other.GetComponent<Rigidbody2D>();
// Vector2 newPlayerVelocity = playerRB.linearVelocity;
// newPlayerVelocity.y = playerController.jumpVelocity;
// playerRB.linearVelocity = newPlayerVelocity;
// }
// }
// //var distance = col.Distance(collision);
// //Vector2 localPoint = col.transform.InverseTransformPoint(distance.pointA);
// //float halfHeight = col.size.y * 0.5f;
// //float topY = col.offset.y * halfHeight;
// //float band = Mathf.Max(0.001f, col.size.y * bandPercent);
//}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
PlayerController playerController = other.GetComponent<PlayerController>();
Rigidbody2D playerRB = other.GetComponent<Rigidbody2D>();
if (Mathf.Abs(other.transform.position.y - _col.bounds.max.y) <= verticalBand)
{
Vector2 newPlayerVelocity = playerRB.linearVelocity;
newPlayerVelocity.y = playerController.jumpVelocity;
playerRB.linearVelocity = newPlayerVelocity;
PlayerJumped?.Invoke(other.gameObject);
}
}
}
//public static bool IsTopByNormal(Collider2D myCol, Vector2 contactNormal, float maxAngleDeg = 30f)
//{
// var up = (Vector2)myCol.transform.up;
// float cos = Vector2.Dot(contactNormal.normalized, up);
// float cosThresh = Mathf.Cos(maxAngleDeg * Mathf.Deg2Rad);
// return cos >= cosThresh;
//}
//public static bool IsTopByLocalY(BoxCollider2D box, Vector2 worldPoint, float bandPercent = 0.1f)
//{
// // Локальная точка
// Vector2 pLocal = box.transform.InverseTransformPoint(worldPoint);
// // Верхняя кромка в локальных координатах с учётом offset
// float halfH = box.size.y * 0.5f;
// float topY = box.offset.y + halfH;
// // Ширина допустимого пояска (процент от высоты коробки)
// float band = Mathf.Max(0.001f, box.size.y * bandPercent);
// return pLocal.y >= (topY - band);
//}
}
}

View File

@ -1,260 +1,263 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
/// <summary>
/// Адаптер между системой ввода COM и Unity Input System.
/// Создаёт виртуальный Gamepad и транслирует состояние кнопок из:
/// - действий игроков (PlayerAction)
/// - физических пинов (int индексы)
/// </summary>
[DefaultExecutionOrder(-100)]
public class ComInputToInputSystemAdapter : MonoBehaviour
namespace RR.Common.Controls
{
private const string VirtualGamepadProduct = "COM Bridge Gamepad";
private const string VirtualGamepadManufacturer = "Robotic Retailers";
[Header("Источник состояния")]
[Tooltip("Если true — использовать In.GetButtonAnyPlayer, иначе — In.GetButtonCurrentPlayer")]
[SerializeField] private bool useAnyPlayer = true;
[Header("Маппинг действий (PlayerAction -> Gamepad)")]
[SerializeField] private List<ActionBinding> actionBindings = new();
[Header("Маппинг пинов (индекс пина -> Gamepad)")]
[SerializeField] private List<PinBinding> pinBindings = new();
[Header("Отладка")]
[SerializeField] private bool logCreateDevice = true;
private static Gamepad _virtualPad;
private static bool _deviceCreated;
private GamepadState _currentState;
#region Структуры маппинга
[Serializable]
public struct ActionBinding
/// <summary>
/// Адаптер между системой ввода COM и Unity Input System.
/// Создаёт виртуальный Gamepad и транслирует состояние кнопок из:
/// - действий игроков (PlayerAction)
/// - физических пинов (int индексы)
/// </summary>
[DefaultExecutionOrder(-100)]
public class ComInputToInputSystemAdapter : MonoBehaviour
{
public PlayerAction action;
public BindingTarget target;
public GamepadButton button;
public DpadDirection dpadDirection;
}
private const string VirtualGamepadProduct = "COM Bridge Gamepad";
private const string VirtualGamepadManufacturer = "Robotic Retailers";
[Serializable]
public struct PinBinding
{
public int pinIndex;
public BindingTarget target;
public GamepadButton button;
public DpadDirection dpadDirection;
}
[Header("Источник состояния")]
[Tooltip("Если true — использовать In.GetButtonAnyPlayer, иначе — In.GetButtonCurrentPlayer")]
[SerializeField] private bool useAnyPlayer = true;
public enum BindingTarget { Button, Dpad }
public enum DpadDirection { Up, Down, Left, Right }
[Header("Маппинг действий (PlayerAction -> Gamepad)")]
[SerializeField] private List<ActionBinding> actionBindings = new();
#endregion
[Header("Маппинг пинов (индекс пина -> Gamepad)")]
[SerializeField] private List<PinBinding> pinBindings = new();
#region Unity Lifecycle
[Header("Отладка")]
[SerializeField] private bool logCreateDevice = true;
// Если нет общего инициализатора, можно раскомментировать Awake
// private void Awake()
// {
// Initialize();
// }
private static Gamepad _virtualPad;
private static bool _deviceCreated;
private void OnEnable()
{
EnsureVirtualGamepadExists();
}
private GamepadState _currentState;
private void Update()
{
if (_virtualPad == null) return;
#region Структуры маппинга
UpdateGamepadState();
InputSystem.QueueStateEvent(_virtualPad, _currentState);
}
#endregion
#region Initialization
public void Initialize()
{
EnsureVirtualGamepadExists();
InitializeDefaultBindings();
}
#endregion
#region State Update
private void UpdateGamepadState()
{
_currentState = default;
ProcessActionBindings();
ProcessPinBindings();
}
private void ProcessActionBindings()
{
foreach (ActionBinding binding in actionBindings)
[Serializable]
public struct ActionBinding
{
bool isPressed = useAnyPlayer
? In.GetButtonAnyPlayer(binding.action)
: In.GetButtonCurrentPlayer(binding.action);
public PlayerAction action;
public BindingTarget target;
public GamepadButton button;
public DpadDirection dpadDirection;
}
if (isPressed)
[Serializable]
public struct PinBinding
{
public int pinIndex;
public BindingTarget target;
public GamepadButton button;
public DpadDirection dpadDirection;
}
public enum BindingTarget { Button, Dpad }
public enum DpadDirection { Up, Down, Left, Right }
#endregion
#region Unity Lifecycle
// Если нет общего инициализатора, можно раскомментировать Awake
// private void Awake()
// {
// Initialize();
// }
private void OnEnable()
{
EnsureVirtualGamepadExists();
}
private void Update()
{
if (_virtualPad == null) return;
UpdateGamepadState();
InputSystem.QueueStateEvent(_virtualPad, _currentState);
}
#endregion
#region Initialization
public void Initialize()
{
EnsureVirtualGamepadExists();
InitializeDefaultBindings();
}
#endregion
#region State Update
private void UpdateGamepadState()
{
_currentState = default;
ProcessActionBindings();
ProcessPinBindings();
}
private void ProcessActionBindings()
{
foreach (ActionBinding binding in actionBindings)
{
ApplyBinding(ref _currentState, binding.target, binding.button, binding.dpadDirection);
bool isPressed = useAnyPlayer
? In.GetButtonAnyPlayer(binding.action)
: In.GetButtonCurrentPlayer(binding.action);
if (isPressed)
{
ApplyBinding(ref _currentState, binding.target, binding.button, binding.dpadDirection);
}
}
}
}
private void ProcessPinBindings()
{
foreach (PinBinding binding in pinBindings)
private void ProcessPinBindings()
{
if (In.GetButton(binding.pinIndex))
foreach (PinBinding binding in pinBindings)
{
ApplyBinding(ref _currentState, binding.target, binding.button, binding.dpadDirection);
if (In.GetButton(binding.pinIndex))
{
ApplyBinding(ref _currentState, binding.target, binding.button, binding.dpadDirection);
}
}
}
}
#endregion
#region Binding Logic
#endregion
private static void ApplyBinding(
ref GamepadState state,
BindingTarget target,
GamepadButton button,
DpadDirection direction)
{
if (target == BindingTarget.Button)
#region Binding Logic
private static void ApplyBinding(
ref GamepadState state,
BindingTarget target,
GamepadButton button,
DpadDirection direction)
{
state = state.WithButton(button);
}
else // Dpad
{
GamepadButton dpadButton = direction switch
if (target == BindingTarget.Button)
{
DpadDirection.Up => GamepadButton.DpadUp,
DpadDirection.Down => GamepadButton.DpadDown,
DpadDirection.Left => GamepadButton.DpadLeft,
DpadDirection.Right => GamepadButton.DpadRight,
_ => GamepadButton.DpadUp
state = state.WithButton(button);
}
else // Dpad
{
GamepadButton dpadButton = direction switch
{
DpadDirection.Up => GamepadButton.DpadUp,
DpadDirection.Down => GamepadButton.DpadDown,
DpadDirection.Left => GamepadButton.DpadLeft,
DpadDirection.Right => GamepadButton.DpadRight,
_ => GamepadButton.DpadUp
};
state = state.WithButton(dpadButton);
}
}
#endregion
#region Virtual Gamepad Management
private void EnsureVirtualGamepadExists()
{
if (_deviceCreated && _virtualPad != null)
return;
// Проверяем, не создан ли уже геймпад
if (TryFindExistingVirtualGamepad(out Gamepad existingGamepad))
{
_virtualPad = existingGamepad;
_deviceCreated = true;
return;
}
CreateVirtualGamepad();
}
private static bool TryFindExistingVirtualGamepad(out Gamepad gamepad)
{
gamepad = null;
foreach (InputDevice device in InputSystem.devices)
{
if (device is not Gamepad { description: { product: VirtualGamepadProduct } } gp) continue;
gamepad = gp;
return true;
}
return false;
}
private void CreateVirtualGamepad()
{
InputDeviceDescription description = new()
{
interfaceName = "Virtual",
deviceClass = "Gamepad",
product = VirtualGamepadProduct,
manufacturer = VirtualGamepadManufacturer
};
state = state.WithButton(dpadButton);
}
}
#endregion
#region Virtual Gamepad Management
private void EnsureVirtualGamepadExists()
{
if (_deviceCreated && _virtualPad != null)
return;
// Проверяем, не создан ли уже геймпад
if (TryFindExistingVirtualGamepad(out Gamepad existingGamepad))
{
_virtualPad = existingGamepad;
_virtualPad = (Gamepad)InputSystem.AddDevice(description);
_deviceCreated = true;
return;
}
CreateVirtualGamepad();
}
private static bool TryFindExistingVirtualGamepad(out Gamepad gamepad)
{
gamepad = null;
foreach (InputDevice device in InputSystem.devices)
{
if (device is not Gamepad { description: { product: VirtualGamepadProduct } } gp) continue;
gamepad = gp;
return true;
}
return false;
}
private void CreateVirtualGamepad()
{
InputDeviceDescription description = new()
{
interfaceName = "Virtual",
deviceClass = "Gamepad",
product = VirtualGamepadProduct,
manufacturer = VirtualGamepadManufacturer
};
_virtualPad = (Gamepad)InputSystem.AddDevice(description);
_deviceCreated = true;
if (logCreateDevice)
{
Debug.Log($"[ComInputAdapter] Виртуальный Gamepad создан: {_virtualPad.name}");
}
}
#endregion
#region Default Bindings
private void InitializeDefaultBindings()
{
if (actionBindings.Count > 0)
return;
AddDefaultActionBindings();
}
private void AddDefaultActionBindings()
{
// Маппинг стрелок на D-Pad
TryAddActionBinding(PlayerAction.up, BindingTarget.Dpad, GamepadButton.North, DpadDirection.Up);
TryAddActionBinding(PlayerAction.down, BindingTarget.Dpad, GamepadButton.South, DpadDirection.Down);
TryAddActionBinding(PlayerAction.left, BindingTarget.Dpad, GamepadButton.West, DpadDirection.Left);
TryAddActionBinding(PlayerAction.right, BindingTarget.Dpad, GamepadButton.East, DpadDirection.Right);
// Кнопки действий
TryAddActionBinding(PlayerAction.action, BindingTarget.Button, GamepadButton.South, DpadDirection.Up);
}
private void TryAddActionBinding(
PlayerAction action,
BindingTarget target,
GamepadButton button,
DpadDirection direction)
{
try
{
actionBindings.Add(new ActionBinding
if (logCreateDevice)
{
action = action,
target = target,
button = button,
dpadDirection = direction
});
Debug.Log($"[ComInputAdapter] Виртуальный Gamepad создан: {_virtualPad.name}");
}
}
catch (Exception e)
{
Debug.LogWarning($"[ComInputAdapter] Не удалось добавить привязку для {action}: {e.Message}");
}
}
#endregion
#endregion
#region Default Bindings
private void InitializeDefaultBindings()
{
if (actionBindings.Count > 0)
return;
AddDefaultActionBindings();
}
private void AddDefaultActionBindings()
{
// Маппинг стрелок на D-Pad
TryAddActionBinding(PlayerAction.up, BindingTarget.Dpad, GamepadButton.North, DpadDirection.Up);
TryAddActionBinding(PlayerAction.down, BindingTarget.Dpad, GamepadButton.South, DpadDirection.Down);
TryAddActionBinding(PlayerAction.left, BindingTarget.Dpad, GamepadButton.West, DpadDirection.Left);
TryAddActionBinding(PlayerAction.right, BindingTarget.Dpad, GamepadButton.East, DpadDirection.Right);
// Кнопки действий
TryAddActionBinding(PlayerAction.action, BindingTarget.Button, GamepadButton.South, DpadDirection.Up);
}
private void TryAddActionBinding(
PlayerAction action,
BindingTarget target,
GamepadButton button,
DpadDirection direction)
{
try
{
actionBindings.Add(new ActionBinding
{
action = action,
target = target,
button = button,
dpadDirection = direction
});
}
catch (Exception e)
{
Debug.LogWarning($"[ComInputAdapter] Не удалось добавить привязку для {action}: {e.Message}");
}
}
#endregion
}
}

View File

@ -1,43 +1,46 @@
/// <summary>
/// Соответствие между кнопками игрового ввода и цифровыми выходами периферийной платы (пины 0-15).
/// Содержит mapping для управления двух игроков, служебных кнопок и датчиков хопера.
/// </summary>
public enum ButtonPinMapping
namespace RR.Common.Controls
{
/// <summary>Кнопка движения вверх для первого игрока</summary>
up1 = 0,
/// <summary>Кнопка движения вправо для первого игрока</summary>
right1 = 1,
/// <summary>Кнопка движения вниз для первого игрока</summary>
down1 = 2,
/// <summary>Кнопка движения влево для первого игрока</summary>
left1 = 3,
/// <summary>Действие/огонь для первого игрока</summary>
action1 = 4,
/// <summary>
/// Соответствие между кнопками игрового ввода и цифровыми выходами периферийной платы (пины 0-15).
/// Содержит mapping для управления двух игроков, служебных кнопок и датчиков хопера.
/// </summary>
public enum ButtonPinMapping
{
/// <summary>Кнопка движения вверх для первого игрока</summary>
up1 = 0,
/// <summary>Кнопка движения вправо для первого игрока</summary>
right1 = 1,
/// <summary>Кнопка движения вниз для первого игрока</summary>
down1 = 2,
/// <summary>Кнопка движения влево для первого игрока</summary>
left1 = 3,
/// <summary>Действие/огонь для первого игрока</summary>
action1 = 4,
/// <summary>Кнопка движения вверх для второго игрока</summary>
up2 = 5,
/// <summary>Кнопка движения вправо для второго игрока</summary>
right2 = 6,
/// <summary>Кнопка движения вниз для второго игрока</summary>
down2 = 7,
/// <summary>Кнопка движения влево для второго игрока</summary>
left2 = 8,
/// <summary>Действие/огонь для второго игрока</summary>
action2 = 9,
/// <summary>Кнопка движения вверх для второго игрока</summary>
up2 = 5,
/// <summary>Кнопка движения вправо для второго игрока</summary>
right2 = 6,
/// <summary>Кнопка движения вниз для второго игрока</summary>
down2 = 7,
/// <summary>Кнопка движения влево для второго игрока</summary>
left2 = 8,
/// <summary>Действие/огонь для второго игрока</summary>
action2 = 9,
/// <summary>Запуск игры для одного игрока</summary>
startOnePlayer = 10,
/// <summary>Запуск игры для двух игроков</summary>
startTwoPlayers = 11,
/// <summary>Секретная/служебная кнопка для захода в настройки</summary>
secret = 12,
/// <summary>Запуск игры для одного игрока</summary>
startOnePlayer = 10,
/// <summary>Запуск игры для двух игроков</summary>
startTwoPlayers = 11,
/// <summary>Секретная/служебная кнопка для захода в настройки</summary>
secret = 12,
/// <summary>Дополнительная/резервная кнопка</summary>
extra = 13,
/// <summary>Дополнительная/резервная кнопка</summary>
extra = 13,
/// <summary>Датчик наличия приза в хопере</summary>
hasPrize = 14,
/// <summary>Датчик выдачи приза из хопера</summary>
prizeGiven = 15
}
/// <summary>Датчик наличия приза в хопере</summary>
hasPrize = 14,
/// <summary>Датчик выдачи приза из хопера</summary>
prizeGiven = 15
}
}

View File

@ -2,253 +2,256 @@
using RR.Game2D.Core.Managers;
using UnityEngine;
public class COMInput : InputSource
namespace RR.Common.Controls
{
//Количество = цифровые + 2 датчика.
//На момент написания количество цифровых входов 7*N.
//Настроить количество пинов под свою настройку платы
private const int PIN_COUNT = 16;
private const char POSITIVE = '0';
private const char NEGATIVE = '1';
private bool[] buttonState = new bool[PIN_COUNT];
private bool[] buttonDown = new bool[PIN_COUNT];
private bool[] buttonUp = new bool[PIN_COUNT];
//Свойство для определения текущего игрока
//Должно возвращать 1 или 2
//Используется для получения состояния кнопок с пометкой ...CurrentPlayer
private int currentPlayer => 1;//ПЕРЕОПРЕДЕЛИТЕ ОПРЕДЕЛЕНИЕ ТЕКУЩЕГО ИГРОКА ПОД СВОЮ СИСТЕМУ.
private GameManager gm;//я получаю кто сейчас ходит через этого менеджера. Если у вас другое, допишите.
public override bool Initialize()
public class COMInput : InputSource
{
try
//Количество = цифровые + 2 датчика.
//На момент написания количество цифровых входов 7*N.
//Настроить количество пинов под свою настройку платы
private const int PIN_COUNT = 16;
private const char POSITIVE = '0';
private const char NEGATIVE = '1';
private bool[] buttonState = new bool[PIN_COUNT];
private bool[] buttonDown = new bool[PIN_COUNT];
private bool[] buttonUp = new bool[PIN_COUNT];
//Свойство для определения текущего игрока
//Должно возвращать 1 или 2
//Используется для получения состояния кнопок с пометкой ...CurrentPlayer
private int currentPlayer => 1;//ПЕРЕОПРЕДЕЛИТЕ ОПРЕДЕЛЕНИЕ ТЕКУЩЕГО ИГРОКА ПОД СВОЮ СИСТЕМУ.
private GameManager gm;//я получаю кто сейчас ходит через этого менеджера. Если у вас другое, допишите.
public override bool Initialize()
{
gm = GameManager.Instance;
//ОБЯЗАТЕЛЬНЫЙ БЛОК
ComWrapper.OnInputReceived.AddListener(UpdateInputState);
}
catch (Exception e)
{
GameManager.Logger.Log("COMInput", $"Что то в COMInput {e.ToString()}");
return false;
}
return true;
}
#region PUBLIC_API
//ЭТА ГРУППА МЕТОДОВ ДЛЯ ПОЛУЧЕНИЯ СОСТОЯНИЯ ЛЮБОЙ КНОПКИ ПО ПИНУ (enum ButtonPinMapping)
/// <summary>
/// Получает состояние конкретной кнопки по её физическому пину.
/// Возвращает true если кнопка в данный момент нажата.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка нажата в текущем кадре</returns>
public override bool GetButton(ButtonPinMapping button) => buttonState[(int)button];
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра.
/// Удерживает true до тех пор, пока кто то не проверит, затем сбрасывает.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка была нажата в этом кадре</returns>
public override bool GetButtonDown(ButtonPinMapping button) => ConsumeButtonEvent(buttonDown, button);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра.
/// Удерживает true до тех пор, пока кто то не проверит, затем сбрасывает.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка была отпущена в этом кадре</returns>
public override bool GetButtonUp(ButtonPinMapping button) => ConsumeButtonEvent(buttonUp, button);
/// <summary>
/// Получает состояние кнопки по числовому индексу пина напрямую из буфера состояний.
/// Обходит перечисление ButtonPinMapping для прямого доступа к пинам по индексу.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
/// <remarks>
/// Используется когда необходим прямой доступ к пинам без приведения к перечислению.
/// Подходит для сценариев где количество пинов может превышать размер перечисления.
/// </remarks>
public override bool GetButton(int pinIndex) => buttonState[pinIndex];
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра по числовому индексу пина.
/// Возвращает true только в первом кадре после нажатия и сбрасывает флаг события.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка была нажата в этом кадре</returns>
/// <remarks>
/// Метод "потребляет" событие - после чтения флаг сбрасывается.
/// Использует общий механизм ConsumeButtonEvent для обработки одноразовых событий.
/// </remarks>
public override bool GetButtonDown(int pinIndex) => ConsumeButtonEvent(buttonDown, pinIndex);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра по числовому индексу пина.
/// Возвращает true только в первом кадре после отпускания и сбрасывает флаг события.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка была отпущена в этом кадре</returns>
/// <remarks>
/// Метод "потребляет" событие - после чтения флаг сбрасывается.
/// Использует общий механизм ConsumeButtonEvent для обработки одноразовых событий.
/// </remarks>
public override bool GetButtonUp(int pinIndex) => ConsumeButtonEvent(buttonUp, pinIndex);
// ------------------------------------------------------------------------------------
//ЭТА ГРУППА МЕТОДОВ ПОЗВОЛЯЕТ УЗНАТЬ СОСТОЯНИЯ КНОПОК АКТИВНОГО ИГРОКА (проверка по enum PlayerAction)
//Служебные кнопки типа startOnePlayer, startTwoPlayers, secret, extra, hasPrize, prizeGiven здесь не проверяются
/// <summary>
/// Получает состояние действия для текущего игрока.
/// Возвращает true если действие активно (кнопка нажата) у текущего игрока.
/// </summary>
/// <param name="button">Действие игрока (up, down, left, right, action, extra)</param>
/// <returns>True если действие активно у текущего игрока</returns>
public override bool GetButtonCurrentPlayer(PlayerAction button)
=> GetButton(GetButtonForCurrentPlayer(button));
/// <summary>
/// Проверяет, было ли действие начато текущим игроком в этом кадре.
/// Возвращает true только в первом кадре после начала действия.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было начато в этом кадре</returns>
public override bool GetButtonDownCurrentPlayer(PlayerAction button)
=> GetButtonDown(GetButtonForCurrentPlayer(button));
/// <summary>
/// Проверяет, было ли действие завершено текущим игроком в этом кадре.
/// Возвращает true только в первом кадре после завершения действия.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было завершено в этом кадре</returns>
public override bool GetButtonUpCurrentPlayer(PlayerAction button)
=> GetButtonUp(GetButtonForCurrentPlayer(button));
// ------------------------------------------------------------------------------------
//ЭТА ГРУППА МЕТОДОВ ПОЗВОЛЯЕТ УЗНАТЬ СОСТОЯНИЯ КНОПОК ЛЮБОГО ИГРОКА (проверка по enum PlayerAction)
//Служебные кнопки типа startOnePlayer, startTwoPlayers, secret, extra, hasPrize, prizeGiven здесь не проверяются
/// <summary>
/// Получает состояние действия для любого из игроков.
/// Возвращает true если действие активно у первого ИЛИ второго игрока.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие активно у любого игрока</returns>
public override bool GetButtonAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButton);
/// <summary>
/// Проверяет, было ли действие начато любым игроком в этом кадре.
/// Возвращает true если первый ИЛИ второй игрок начали действие в этом кадре.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было начато любым игроком в этом кадре</returns>
public override bool GetButtonDownAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButtonDown);
/// <summary>
/// Проверяет, было ли действие завершено любым игроком в этом кадре.
/// Возвращает true если первый ИЛИ второй игрок завершили действие в этом кадре.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было завершено любым игроком в этом кадре</returns>
public override bool GetButtonUpAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButtonUp);
#endregion
//==========ХЕЛПЕРЫ==========
// Проверка нажатия с обоих контроллеров игроков
private bool CheckBothPlayers(PlayerAction action, Func<ButtonPinMapping, bool> buttonFunc)
{
var buttons = _buttonByAction(action);
return buttonFunc(buttons.player1button) || buttonFunc(buttons.player2button);
}
// Проверка нажатия с контроллера текущего игрока
private ButtonPinMapping GetButtonForCurrentPlayer(PlayerAction button)
{
var buttons = _buttonByAction(button);
return currentPlayer == 1 ? buttons.player1button : buttons.player2button;
}
// Маппер из действий игрока => пины платы
private (ButtonPinMapping player1button, ButtonPinMapping player2button) _buttonByAction(PlayerAction playerAction) =>
playerAction switch
{
PlayerAction.up => (ButtonPinMapping.up1, ButtonPinMapping.up2),
PlayerAction.down => (ButtonPinMapping.down1, ButtonPinMapping.down2),
PlayerAction.left => (ButtonPinMapping.left1, ButtonPinMapping.left2),
PlayerAction.right => (ButtonPinMapping.right1, ButtonPinMapping.right2),
PlayerAction.action => (ButtonPinMapping.action1, ButtonPinMapping.action2),
PlayerAction.extra => (ButtonPinMapping.extra, ButtonPinMapping.extra), // одинаковая кнопка
_ => throw new ArgumentException($"Unknown player action: {playerAction}")
};
// Обновляет состояния инпута когда приходит новый ввод с платы. Дергается событием.
private void UpdateInputState(string input)
{
// Будем принимать в нативном формате by Pasha. 0 - нажато 1 - не нажато (чтобы преобразовывать циклом)
// В параметрприходит строка вида "1111011111011111"
for (int i = 0; i < input.Length; i++)
{
// Получаем новое состояние пина
bool newState = input[i] == POSITIVE;
// Состояние пина изменилось => пишем в буферы
if (newState != buttonState[i])
try
{
if(newState) // Пин был зажат
{
buttonDown[i] = true;
buttonUp[i] = false;
}
else // Пин был отжат
{
buttonDown[i] = false;
buttonUp[i] = true;
}
gm = GameManager.Instance;
//ОБЯЗАТЕЛЬНЫЙ БЛОК
ComWrapper.OnInputReceived.AddListener(UpdateInputState);
}
buttonState[i] = newState;
catch (Exception e)
{
GameManager.Logger.Log("COMInput", $"Что то в COMInput {e.ToString()}");
return false;
}
return true;
}
if (input[^1] == POSITIVE) PlayerPrefs.SetInt("PrizeGivenBuffer", 1); //пишем в буфер чтобы между кадрами и порядком вызова не проебать
// DebugWindow.Message("Инпуты: " + string.Join("", buttonState));//можно вырезать
}
// Потребление "Нажатия"/"Отжатия". Возвращает что там было и чистит флаг из буфера
private bool ConsumeButtonEvent(bool[] buffer, ButtonPinMapping button)
{
bool result = buffer[(int)button];
buffer[(int)button] = false;
return result;
}
#region PUBLIC_API
/// <summary>
/// Вспомогательный метод для обработки одноразовых событий кнопок по числовому индексу.
/// Возвращает текущее значение флага и сбрасывает его для предотвращения повторной обработки.
/// </summary>
/// <param name="buffer">Буфер событий (buttonDown или buttonUp)</param>
/// <param name="pinIndex">Числовой индекс пина</param>
/// <returns>Состояние флага события до сброса</returns>
private bool ConsumeButtonEvent(bool[] buffer, int pinIndex)
{
bool result = buffer[pinIndex];
buffer[pinIndex] = false;
return result;
//ЭТА ГРУППА МЕТОДОВ ДЛЯ ПОЛУЧЕНИЯ СОСТОЯНИЯ ЛЮБОЙ КНОПКИ ПО ПИНУ (enum ButtonPinMapping)
/// <summary>
/// Получает состояние конкретной кнопки по её физическому пину.
/// Возвращает true если кнопка в данный момент нажата.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка нажата в текущем кадре</returns>
public override bool GetButton(ButtonPinMapping button) => buttonState[(int)button];
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра.
/// Удерживает true до тех пор, пока кто то не проверит, затем сбрасывает.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка была нажата в этом кадре</returns>
public override bool GetButtonDown(ButtonPinMapping button) => ConsumeButtonEvent(buttonDown, button);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра.
/// Удерживает true до тех пор, пока кто то не проверит, затем сбрасывает.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>True если кнопка была отпущена в этом кадре</returns>
public override bool GetButtonUp(ButtonPinMapping button) => ConsumeButtonEvent(buttonUp, button);
/// <summary>
/// Получает состояние кнопки по числовому индексу пина напрямую из буфера состояний.
/// Обходит перечисление ButtonPinMapping для прямого доступа к пинам по индексу.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
/// <remarks>
/// Используется когда необходим прямой доступ к пинам без приведения к перечислению.
/// Подходит для сценариев где количество пинов может превышать размер перечисления.
/// </remarks>
public override bool GetButton(int pinIndex) => buttonState[pinIndex];
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра по числовому индексу пина.
/// Возвращает true только в первом кадре после нажатия и сбрасывает флаг события.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка была нажата в этом кадре</returns>
/// <remarks>
/// Метод "потребляет" событие - после чтения флаг сбрасывается.
/// Использует общий механизм ConsumeButtonEvent для обработки одноразовых событий.
/// </remarks>
public override bool GetButtonDown(int pinIndex) => ConsumeButtonEvent(buttonDown, pinIndex);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра по числовому индексу пина.
/// Возвращает true только в первом кадре после отпускания и сбрасывает флаг события.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-15)</param>
/// <returns>true если кнопка была отпущена в этом кадре</returns>
/// <remarks>
/// Метод "потребляет" событие - после чтения флаг сбрасывается.
/// Использует общий механизм ConsumeButtonEvent для обработки одноразовых событий.
/// </remarks>
public override bool GetButtonUp(int pinIndex) => ConsumeButtonEvent(buttonUp, pinIndex);
// ------------------------------------------------------------------------------------
//ЭТА ГРУППА МЕТОДОВ ПОЗВОЛЯЕТ УЗНАТЬ СОСТОЯНИЯ КНОПОК АКТИВНОГО ИГРОКА (проверка по enum PlayerAction)
//Служебные кнопки типа startOnePlayer, startTwoPlayers, secret, extra, hasPrize, prizeGiven здесь не проверяются
/// <summary>
/// Получает состояние действия для текущего игрока.
/// Возвращает true если действие активно (кнопка нажата) у текущего игрока.
/// </summary>
/// <param name="button">Действие игрока (up, down, left, right, action, extra)</param>
/// <returns>True если действие активно у текущего игрока</returns>
public override bool GetButtonCurrentPlayer(PlayerAction button)
=> GetButton(GetButtonForCurrentPlayer(button));
/// <summary>
/// Проверяет, было ли действие начато текущим игроком в этом кадре.
/// Возвращает true только в первом кадре после начала действия.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было начато в этом кадре</returns>
public override bool GetButtonDownCurrentPlayer(PlayerAction button)
=> GetButtonDown(GetButtonForCurrentPlayer(button));
/// <summary>
/// Проверяет, было ли действие завершено текущим игроком в этом кадре.
/// Возвращает true только в первом кадре после завершения действия.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было завершено в этом кадре</returns>
public override bool GetButtonUpCurrentPlayer(PlayerAction button)
=> GetButtonUp(GetButtonForCurrentPlayer(button));
// ------------------------------------------------------------------------------------
//ЭТА ГРУППА МЕТОДОВ ПОЗВОЛЯЕТ УЗНАТЬ СОСТОЯНИЯ КНОПОК ЛЮБОГО ИГРОКА (проверка по enum PlayerAction)
//Служебные кнопки типа startOnePlayer, startTwoPlayers, secret, extra, hasPrize, prizeGiven здесь не проверяются
/// <summary>
/// Получает состояние действия для любого из игроков.
/// Возвращает true если действие активно у первого ИЛИ второго игрока.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие активно у любого игрока</returns>
public override bool GetButtonAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButton);
/// <summary>
/// Проверяет, было ли действие начато любым игроком в этом кадре.
/// Возвращает true если первый ИЛИ второй игрок начали действие в этом кадре.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было начато любым игроком в этом кадре</returns>
public override bool GetButtonDownAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButtonDown);
/// <summary>
/// Проверяет, было ли действие завершено любым игроком в этом кадре.
/// Возвращает true если первый ИЛИ второй игрок завершили действие в этом кадре.
/// </summary>
/// <param name="button">Действие игрока</param>
/// <returns>True если действие было завершено любым игроком в этом кадре</returns>
public override bool GetButtonUpAnyPlayer(PlayerAction button)
=> CheckBothPlayers(button, GetButtonUp);
#endregion
//==========ХЕЛПЕРЫ==========
// Проверка нажатия с обоих контроллеров игроков
private bool CheckBothPlayers(PlayerAction action, Func<ButtonPinMapping, bool> buttonFunc)
{
var buttons = _buttonByAction(action);
return buttonFunc(buttons.player1button) || buttonFunc(buttons.player2button);
}
// Проверка нажатия с контроллера текущего игрока
private ButtonPinMapping GetButtonForCurrentPlayer(PlayerAction button)
{
var buttons = _buttonByAction(button);
return currentPlayer == 1 ? buttons.player1button : buttons.player2button;
}
// Маппер из действий игрока => пины платы
private (ButtonPinMapping player1button, ButtonPinMapping player2button) _buttonByAction(PlayerAction playerAction) =>
playerAction switch
{
PlayerAction.up => (ButtonPinMapping.up1, ButtonPinMapping.up2),
PlayerAction.down => (ButtonPinMapping.down1, ButtonPinMapping.down2),
PlayerAction.left => (ButtonPinMapping.left1, ButtonPinMapping.left2),
PlayerAction.right => (ButtonPinMapping.right1, ButtonPinMapping.right2),
PlayerAction.action => (ButtonPinMapping.action1, ButtonPinMapping.action2),
PlayerAction.extra => (ButtonPinMapping.extra, ButtonPinMapping.extra), // одинаковая кнопка
_ => throw new ArgumentException($"Unknown player action: {playerAction}")
};
// Обновляет состояния инпута когда приходит новый ввод с платы. Дергается событием.
private void UpdateInputState(string input)
{
// Будем принимать в нативном формате by Pasha. 0 - нажато 1 - не нажато (чтобы преобразовывать циклом)
// В параметрприходит строка вида "1111011111011111"
for (int i = 0; i < input.Length; i++)
{
// Получаем новое состояние пина
bool newState = input[i] == POSITIVE;
// Состояние пина изменилось => пишем в буферы
if (newState != buttonState[i])
{
if (newState) // Пин был зажат
{
buttonDown[i] = true;
buttonUp[i] = false;
}
else // Пин был отжат
{
buttonDown[i] = false;
buttonUp[i] = true;
}
}
buttonState[i] = newState;
}
if (input[^1] == POSITIVE) PlayerPrefs.SetInt("PrizeGivenBuffer", 1); //пишем в буфер чтобы между кадрами и порядком вызова не проебать
// DebugWindow.Message("Инпуты: " + string.Join("", buttonState));//можно вырезать
}
// Потребление "Нажатия"/"Отжатия". Возвращает что там было и чистит флаг из буфера
private bool ConsumeButtonEvent(bool[] buffer, ButtonPinMapping button)
{
bool result = buffer[(int)button];
buffer[(int)button] = false;
return result;
}
/// <summary>
/// Вспомогательный метод для обработки одноразовых событий кнопок по числовому индексу.
/// Возвращает текущее значение флага и сбрасывает его для предотвращения повторной обработки.
/// </summary>
/// <param name="buffer">Буфер событий (buttonDown или buttonUp)</param>
/// <param name="pinIndex">Числовой индекс пина</param>
/// <returns>Состояние флага события до сброса</returns>
private bool ConsumeButtonEvent(bool[] buffer, int pinIndex)
{
bool result = buffer[pinIndex];
buffer[pinIndex] = false;
return result;
}
}
}
}

View File

@ -2,56 +2,59 @@
using System.Runtime.InteropServices;
using System.Text;
public static class ComNativeWrapper
namespace RR.Common.Controls
{
private const string Dll = "ComNative"; // имя файла в Plugins/x86_64 без .dll
// --- Порты (Unicode) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_ListPortsLength();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_ListPorts(StringBuilder buffer, int bufferChars);
// --- Открытие/закрытие ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_Open(string portName, int baud, int dataBits, int parity, int stopBits,
int readTimeoutMs, int writeTimeoutMs);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int Serial_Close(int handleId);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int Serial_CloseAll(); // NEW
// --- Запись/чтение (строкой) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern int Serial_WriteStringUTF8(int handleId, string utf8, int appendNewline);
// byte[]-вариант к той же native-функции (EntryPoint), чтобы избежать маршаллинга StringBuilder
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "Serial_ReadLineUTF8")]
public static extern int Serial_ReadLineUTF8_bytes(int handleId, byte[] buffer, int bufferSize);
// --- Ошибка (Unicode) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_GetLastErrorW(StringBuilder buffer, int bufferChars);
public static string GetLastError()
public static class ComNativeWrapper
{
int need = Serial_GetLastErrorW(null, 0);
var sb = new StringBuilder(Math.Max(need, 1));
int r = Serial_GetLastErrorW(sb, sb.Capacity);
return (r < 0) ? $"err({r})" : sb.ToString();
}
private const string Dll = "ComNative"; // имя файла в Plugins/x86_64 без .dll
public static string[] ListPorts()
{
int need = Serial_ListPortsLength();
if (need <= 1) return Array.Empty<string>();
var sb = new StringBuilder(need);
int r = Serial_ListPorts(sb, sb.Capacity);
if (r < 0) return Array.Empty<string>();
string joined = sb.ToString();
return string.IsNullOrWhiteSpace(joined) ? Array.Empty<string>() : joined.Split(';');
// --- Порты (Unicode) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_ListPortsLength();
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_ListPorts(StringBuilder buffer, int bufferChars);
// --- Открытие/закрытие ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_Open(string portName, int baud, int dataBits, int parity, int stopBits,
int readTimeoutMs, int writeTimeoutMs);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int Serial_Close(int handleId);
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl)]
public static extern int Serial_CloseAll(); // NEW
// --- Запись/чтение (строкой) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern int Serial_WriteStringUTF8(int handleId, string utf8, int appendNewline);
// byte[]-вариант к той же native-функции (EntryPoint), чтобы избежать маршаллинга StringBuilder
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, EntryPoint = "Serial_ReadLineUTF8")]
public static extern int Serial_ReadLineUTF8_bytes(int handleId, byte[] buffer, int bufferSize);
// --- Ошибка (Unicode) ---
[DllImport(Dll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
public static extern int Serial_GetLastErrorW(StringBuilder buffer, int bufferChars);
public static string GetLastError()
{
int need = Serial_GetLastErrorW(null, 0);
var sb = new StringBuilder(Math.Max(need, 1));
int r = Serial_GetLastErrorW(sb, sb.Capacity);
return (r < 0) ? $"err({r})" : sb.ToString();
}
public static string[] ListPorts()
{
int need = Serial_ListPortsLength();
if (need <= 1) return Array.Empty<string>();
var sb = new StringBuilder(need);
int r = Serial_ListPorts(sb, sb.Capacity);
if (r < 0) return Array.Empty<string>();
string joined = sb.ToString();
return string.IsNullOrWhiteSpace(joined) ? Array.Empty<string>() : joined.Split(';');
}
}
}
}

View File

@ -4,340 +4,343 @@ using RR.Game2D.Core.Managers;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
/// </summary>
public class ComWrapper : MonoBehaviour
namespace RR.Common.Controls
{
//Настраиваем из инспектора
[SerializeField] private bool saveMoneyBetweenSessions;
[SerializeField] private bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
[SerializeField] private bool keepAlive = true;
public static UnityEvent<int> OnCreditAdded;
public static UnityEvent<int> OnCreditRemoved;
public static UnityEvent<string> OnInputReceived;
#region Приватные поля
private const string REQUEST_INPUT = "91";
private const string DEFAULT_INPUT = "INPUT1111111111111111";
private const string LIGHTSIGNAL_PREFIX = "80";
private static int h; // идентификатор хэндла
private static byte[] buf = new byte[1024]; // Байт-буфер куда будут приходить данные с порта
private static int CreditsCount;
private bool boardEncrypted = false;
// private string inputString = DEFAULT_INPUT;
#endregion
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
/// </summary>
/// <remarks>
/// Обязателен к вызову при старте приложения. Выполняет:
/// - Создание UnityEvent'ов для уведомлений
/// - Загрузку количества кредитов из PlayerPrefs
/// - Открытие COM-порта
/// - Запуск периодического опроса пинов (если включена проверка призов)
/// </remarks>
public void Initialize()
public class ComWrapper : MonoBehaviour
{
//Создаем ивенты
OnCreditAdded = new UnityEvent<int>();
OnCreditRemoved = new UnityEvent<int>();
OnInputReceived = new UnityEvent<string>();
//Настраиваем из инспектора
[SerializeField] private bool saveMoneyBetweenSessions;
[SerializeField] private bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
[SerializeField] private bool keepAlive = true;
//Начальная настройка
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
StartCoroutine(SetupBoard());
}
public static UnityEvent<int> OnCreditAdded;
public static UnityEvent<int> OnCreditRemoved;
public static UnityEvent<string> OnInputReceived;
/// <summary>
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
/// </summary>
/// <remarks>
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
/// Автоматически сохраняет состояние в PlayerPrefs.
/// </remarks>
public static void AddCredit()
{
CreditsCount++;
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
OnCreditAdded?.Invoke(CreditsCount);
}
#region Приватные поля
private const string REQUEST_INPUT = "91";
private const string DEFAULT_INPUT = "INPUT1111111111111111";
private const string LIGHTSIGNAL_PREFIX = "80";
/// <summary>
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
/// </summary>
/// <param name="message">Команда для отправки</param>
/// <remarks>
/// Используется для отправки пользовательских команд на плату.
/// Команда должна быть в формате, ожидаемом прошивкой платы.
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
/// </remarks>
public static void Send(string message)
{
try
private static int h; // идентификатор хэндла
private static byte[] buf = new byte[1024]; // Байт-буфер куда будут приходить данные с порта
private static int CreditsCount;
private bool boardEncrypted = false;
// private string inputString = DEFAULT_INPUT;
#endregion
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
/// </summary>
/// <remarks>
/// Обязателен к вызову при старте приложения. Выполняет:
/// - Создание UnityEvent'ов для уведомлений
/// - Загрузку количества кредитов из PlayerPrefs
/// - Открытие COM-порта
/// - Запуск периодического опроса пинов (если включена проверка призов)
/// </remarks>
public void Initialize()
{
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать
//GameManager.Logger.Log("Wrote: " + w);
//Создаем ивенты
OnCreditAdded = new UnityEvent<int>();
OnCreditRemoved = new UnityEvent<int>();
OnInputReceived = new UnityEvent<string>();
//Начальная настройка
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
StartCoroutine(SetupBoard());
}
catch
/// <summary>
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
/// </summary>
/// <remarks>
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
/// Автоматически сохраняет состояние в PlayerPrefs.
/// </remarks>
public static void AddCredit()
{
// DebugWindow.Message("Ошибка отправки строкового сигнала.");
}
}
/// <summary>
/// Отправляет команду управления подсветкой на периферийную плату.
/// </summary>
/// <param name="lightMode">Режим подсветки из перечисления LightMode</param>
/// <remarks>
/// Формирует команду вида "80XXX" где XXX - код режима подсветки.
/// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление.
/// </remarks>
public static void Send(LightMode lightMode)
{
try
{
string signal = LIGHTSIGNAL_PREFIX + getLightString(lightMode);
Send(signal);
//ВОТЭТОТ МЕТОД ПОД СВОИ ПОДСВЕТКИ НАДО ПОДГОНЯТЬ
string getLightString(LightMode mode) =>
mode switch
{
LightMode.Idle1 => "500",
LightMode.Idle2 => "507",
LightMode.Orange => "501",
LightMode.Blue => "502",
LightMode.Win => "503",
LightMode.Lose => "504",
LightMode.Prize => "505",
LightMode.Tama => "513",
LightMode.Dog => "514",
LightMode.Sam => "515",
LightMode.Mistress => "516",
_ => "500"
};
}
catch
{
// DebugWindow.Message("Ошибка отправки светового сигнала.");
}
}
/// <summary>
/// Выполняет списание платы за запуск игры.
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
/// При успешном списании увеличивает счетчики запусков.
/// </summary>
/// <returns>
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
/// false - если кредитов недостаточно и платный режим
/// </returns>
/// <remarks>
/// В случае успеха:
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
/// - При платном режиме уменьшает количество кредитов на 1
/// - Вызывает событие onCoinRemoved при списании кредита
/// </remarks>
public static bool Pay(int amount = 1)
{
// блок проверки оплаты
// TODO: Сделать свою систему для проверки оплаты и бесплатной игры
// if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл")
// {
// //Счетчики пробега
// PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
// PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
//
// return true;
// }
if (CreditsCount >= amount)
{
CreditsCount -= amount;
CreditsCount++;
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
//счетчики пробега
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
OnCreditRemoved?.Invoke(CreditsCount);
return true;
OnCreditAdded?.Invoke(CreditsCount);
}
return false;
}
//===========================end: PUBLIC API===========================//
private void Update() => pollPort();
private void OnDisable() => ClosePort();
private void OnApplicationQuit() => ClosePort();
private IEnumerator KeepPortAliveProcess()
{
OnInputReceived?.AddListener(InputReceivedCather);
GameManager.Logger.Log("Подключаю проверку порта.");
while (true)
/// <summary>
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
/// </summary>
/// <param name="message">Команда для отправки</param>
/// <remarks>
/// Используется для отправки пользовательских команд на плату.
/// Команда должна быть в формате, ожидаемом прошивкой платы.
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
/// </remarks>
public static void Send(string message)
{
requestInputs();
yield return new WaitForSeconds(5f);
//нет инпута
if (GetInputToggle() == 0)
try
{
GameManager.Logger.Log("Плата отвалилась. Запускаю переподключение.");
initPort();
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать
//GameManager.Logger.Log("Wrote: " + w);
}
//есть инпут
catch
{
// DebugWindow.Message("Ошибка отправки строкового сигнала.");
}
}
/// <summary>
/// Отправляет команду управления подсветкой на периферийную плату.
/// </summary>
/// <param name="lightMode">Режим подсветки из перечисления LightMode</param>
/// <remarks>
/// Формирует команду вида "80XXX" где XXX - код режима подсветки.
/// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление.
/// </remarks>
public static void Send(LightMode lightMode)
{
try
{
string signal = LIGHTSIGNAL_PREFIX + getLightString(lightMode);
Send(signal);
//ВОТЭТОТ МЕТОД ПОД СВОИ ПОДСВЕТКИ НАДО ПОДГОНЯТЬ
string getLightString(LightMode mode) =>
mode switch
{
LightMode.Idle1 => "500",
LightMode.Idle2 => "507",
LightMode.Orange => "501",
LightMode.Blue => "502",
LightMode.Win => "503",
LightMode.Lose => "504",
LightMode.Prize => "505",
LightMode.Tama => "513",
LightMode.Dog => "514",
LightMode.Sam => "515",
LightMode.Mistress => "516",
_ => "500"
};
}
catch
{
// DebugWindow.Message("Ошибка отправки светового сигнала.");
}
}
/// <summary>
/// Выполняет списание платы за запуск игры.
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
/// При успешном списании увеличивает счетчики запусков.
/// </summary>
/// <returns>
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
/// false - если кредитов недостаточно и платный режим
/// </returns>
/// <remarks>
/// В случае успеха:
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
/// - При платном режиме уменьшает количество кредитов на 1
/// - Вызывает событие onCoinRemoved при списании кредита
/// </remarks>
public static bool Pay(int amount = 1)
{
// блок проверки оплаты
// TODO: Сделать свою систему для проверки оплаты и бесплатной игры
// if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл")
// {
// //Счетчики пробега
// PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
// PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
//
// return true;
// }
if (CreditsCount >= amount)
{
CreditsCount -= amount;
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
//счетчики пробега
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
OnCreditRemoved?.Invoke(CreditsCount);
return true;
}
return false;
}
//===========================end: PUBLIC API===========================//
private void Update() => pollPort();
private void OnDisable() => ClosePort();
private void OnApplicationQuit() => ClosePort();
private IEnumerator KeepPortAliveProcess()
{
OnInputReceived?.AddListener(InputReceivedCather);
GameManager.Logger.Log("Подключаю проверку порта.");
while (true)
{
requestInputs();
yield return new WaitForSeconds(5f);
//нет инпута
if (GetInputToggle() == 0)
{
GameManager.Logger.Log("Плата отвалилась. Запускаю переподключение.");
initPort();
}
//есть инпут
else
{
ToggleInputReceived(0);
}
yield return new WaitForSeconds(5);
}
}
private static void ToggleInputReceived(int value) => PlayerPrefs.SetInt("inputToggle", value);
int GetInputToggle() => PlayerPrefs.GetInt("inputToggle", 0);
private static void InputReceivedCather(string input)
{
if (!string.IsNullOrEmpty(input))
{
//пришел инпут
ToggleInputReceived(1);
}
}
#region Приватные методы
//Ну тут по дебаг логам все должно быть понятно
//Если интересно, то h - это хэндл. Что то на плюсовом, типа ID соединения или чтото. Если 0 то нету соед. если меньше то ошибка, больше 0 - норм.
private void initPort()
{
GameManager.Logger.Log("ComWrapper", "Старт программы");
ComNativeWrapper.Serial_CloseAll();
var ports = ComNativeWrapper.ListPorts();
GameManager.Logger.Log("ComWrapper", "Ports: " + string.Join(", ", ports));
if (ports.Length == 0) { GameManager.Logger.Log("Нет портов, завершаю"); return; }
string port = ports[^1];
GameManager.Logger.Log("ComWrapper", "Открываю порт: " + port);
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 5, 10);
GameManager.Logger.Log("ComWrapper", $"handle: {h}");
if (h <= 0)
{
GameManager.Logger.Log("ComWrapper", "Open failed: " + ComNativeWrapper.GetLastError());
return;
}
else
{
ToggleInputReceived(0);
GameManager.Logger.Log("ComWrapper", $"Открыл порт: {port}");
}
yield return new WaitForSeconds(5);
}
private static void ClosePort()
{
Debug.Log("Закрываю все порты");
if (h > 0)
{
ComNativeWrapper.Serial_Close(h);
h = 0;
}
}
//Тут чтение, не трогаем(опционально)
private static string ReadBufferLine()
{
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
if (read > 0)
{
string s = Encoding.UTF8.GetString(buf, 0, read);
//GameManager.Logger.Log($"ReadLine len={read} data=[{s}]");
return s;
}
else if (read < 0)
{
GameManager.Logger.Log("ComWrapper", $"ReadLine err={read} :: {ComNativeWrapper.GetLastError()}");
}
return "";
}
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
private void requestInputs() => Send(REQUEST_INPUT);
private void pollPort()
{
//Тут все просто, читаем что нам приходит с порта и сравниваем.
string comInput = ReadBufferLine();
if (comInput != "")
{
GameManager.Logger.Log("ComWrapper", "COM: " + comInput);
if (comInput == "COIN") AddCredit();
if (comInput.StartsWith("INPUT")) OnInputReceived?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT"
}
}
private IEnumerator SetupBoard()
{
initPort();
requestInputs();
//проверяем вдруг не зашифрованная версия
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
if (keepAlive) StartCoroutine(KeepPortAliveProcess());
yield return null;
}
#endregion
}
private static void ToggleInputReceived(int value) => PlayerPrefs.SetInt("inputToggle", value);
int GetInputToggle() => PlayerPrefs.GetInt("inputToggle", 0);
private static void InputReceivedCather(string input)
public enum LightMode
{
if (!string.IsNullOrEmpty(input))
{
//пришел инпут
ToggleInputReceived(1);
}
Idle1,
Idle2,
Orange,
Blue,
Win,
Lose,
Prize,
Tama,
Dog,
Sam,
Mistress
}
#region Приватные методы
//Ну тут по дебаг логам все должно быть понятно
//Если интересно, то h - это хэндл. Что то на плюсовом, типа ID соединения или чтото. Если 0 то нету соед. если меньше то ошибка, больше 0 - норм.
private void initPort()
{
GameManager.Logger.Log("ComWrapper", "Старт программы");
ComNativeWrapper.Serial_CloseAll();
var ports = ComNativeWrapper.ListPorts();
GameManager.Logger.Log("ComWrapper","Ports: " + string.Join(", ", ports));
if (ports.Length == 0) { GameManager.Logger.Log("Нет портов, завершаю"); return; }
string port = ports[^1];
GameManager.Logger.Log("ComWrapper","Открываю порт: " + port);
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 5, 10);
GameManager.Logger.Log("ComWrapper",$"handle: {h}");
if (h <= 0)
{
GameManager.Logger.Log("ComWrapper","Open failed: " + ComNativeWrapper.GetLastError());
return;
}
else
{
GameManager.Logger.Log("ComWrapper",$"Открыл порт: {port}");
}
}
private static void ClosePort()
{
Debug.Log("Закрываю все порты");
if (h > 0)
{
ComNativeWrapper.Serial_Close(h);
h = 0;
}
}
//Тут чтение, не трогаем(опционально)
private static string ReadBufferLine()
{
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
if (read > 0)
{
string s = Encoding.UTF8.GetString(buf, 0, read);
//GameManager.Logger.Log($"ReadLine len={read} data=[{s}]");
return s;
}
else if (read < 0)
{
GameManager.Logger.Log("ComWrapper",$"ReadLine err={read} :: {ComNativeWrapper.GetLastError()}");
}
return "";
}
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
private void requestInputs() => Send(REQUEST_INPUT);
private void pollPort()
{
//Тут все просто, читаем что нам приходит с порта и сравниваем.
string comInput = ReadBufferLine();
if (comInput != "")
{
GameManager.Logger.Log("ComWrapper", "COM: " + comInput);
if (comInput == "COIN") AddCredit();
if (comInput.StartsWith("INPUT")) OnInputReceived?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT"
}
}
private IEnumerator SetupBoard()
{
initPort();
requestInputs();
//проверяем вдруг не зашифрованная версия
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
if (keepAlive) StartCoroutine(KeepPortAliveProcess());
yield return null;
}
#endregion
}
public enum LightMode
{
Idle1,
Idle2,
Orange,
Blue,
Win,
Lose,
Prize,
Tama,
Dog,
Sam,
Mistress
}

View File

@ -3,127 +3,130 @@ using RR.Common.Managers;
using RR.Game2D.Core.Managers;
using UnityEngine;
/// <summary>
/// Система управления выдачей призов и ведения статистики игры.
/// Отвечает за процесс выдачи призов, подсчет запусков и наград.
/// </summary>
public class PrizeSystem : MonoBehaviour
namespace RR.Common.Controls
{
private static PrizeSystem Instance { get; set; }
private static Coroutine prizeCoroutine;
private static bool _prizing = false;
public static readonly string key_starts = "STARTS";
public static readonly string key_startsNul = "STARTSNUL";
public static readonly string key_prizesgiven = "PRIZESGIVEN";
public static readonly string key_prizesgivenNul = "PRIZESGIVENNUL";
[SerializeField] AudioClip prizeErrorSound;
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему призов и счетчики статистики.
/// Должен быть вызван один раз при запуске приложения.
/// Система управления выдачей призов и ведения статистики игры.
/// Отвечает за процесс выдачи призов, подсчет запусков и наград.
/// </summary>
/// <remarks>
/// Выполняет:
/// - Установку статического экземпляра (singleton)
/// - Инициализацию счетчиков в PlayerPrefs если они отсутствуют
/// - Подготовку системы к работе
/// </remarks>
public void Initialize()
public class PrizeSystem : MonoBehaviour
{
if (Instance != null && Instance != this)
private static PrizeSystem Instance { get; set; }
private static Coroutine prizeCoroutine;
private static bool _prizing = false;
public static readonly string key_starts = "STARTS";
public static readonly string key_startsNul = "STARTSNUL";
public static readonly string key_prizesgiven = "PRIZESGIVEN";
public static readonly string key_prizesgivenNul = "PRIZESGIVENNUL";
[SerializeField] AudioClip prizeErrorSound;
//===========================start: PUBLIC API===========================//
/// <summary>
/// Инициализирует систему призов и счетчики статистики.
/// Должен быть вызван один раз при запуске приложения.
/// </summary>
/// <remarks>
/// Выполняет:
/// - Установку статического экземпляра (singleton)
/// - Инициализацию счетчиков в PlayerPrefs если они отсутствуют
/// - Подготовку системы к работе
/// </remarks>
public void Initialize()
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
InitCounters();
}
/// <summary>
/// Запускает процесс выдачи физического приза игроку.
/// Проверяет настройки и текущее состояние перед началом выдачи.
/// </summary>
/// <remarks>
/// Условия для выдачи приза:
/// - В настройках включена выдача призов при игре с ботом
/// - В данный момент не выполняется другая выдача приза
///
/// Процесс выдачи:
/// - Включает мотор хопера
/// - Ожидает подтверждения выдачи от датчиков (10 сек таймаут)
/// - Выключает мотор по завершению
/// - Обновляет счетчики выданных призов
/// - Воспроизводит звук ошибки при неудаче
/// </remarks>
public static void GivePrize()
{
// bool settingEnabled = SettingsManager.instance.GetSettingValue("Выдача призов при игре с ботом") == "Выдавать";
//
// if (!settingEnabled) { DebugWindow.Message("Выдача приза отключена в настройках"); return; }
if (_prizing)
{
GameManager.Logger.Log("PrizeSystem", "Уже идет выдача приза. Не спамь кнопку.");
return;
}
//
// DebugWindow.Message("Выдаю приз");
if (prizeCoroutine != null) Instance.StopCoroutine(prizeCoroutine);
prizeCoroutine = Instance.StartCoroutine(Instance.Prize());
}
//===========================end: PUBLIC API===========================//
#region Приватные методы
private IEnumerator Prize()
{
_prizing = true;
// DebugWindow.Message("Начинаю выдачу");
const float timeout = 10; //сек
ComWrapper.Send("301");//включаем мотор
float startTime = Time.time;
bool success = false;
PlayerPrefs.SetInt("PrizeGivenBuffer", 0);//На всякий случай подготовка буфера
//проверить где он пишется
while (Time.time - startTime < timeout)
{
if (PlayerPrefs.GetInt("PrizeGivenBuffer", 0) == 1)
if (Instance != null && Instance != this)
{
success = true;
IncreasePrizeCounters();
break;
Destroy(gameObject);
return;
}
yield return null;
Instance = this;
DontDestroyOnLoad(gameObject);
InitCounters();
}
//Выключаем мотор
ComWrapper.Send("300");
// DebugWindow.Message("Закончил. Успех: " + success);
_prizing = false;
/// <summary>
/// Запускает процесс выдачи физического приза игроку.
/// Проверяет настройки и текущее состояние перед началом выдачи.
/// </summary>
/// <remarks>
/// Условия для выдачи приза:
/// - В настройках включена выдача призов при игре с ботом
/// - В данный момент не выполняется другая выдача приза
///
/// Процесс выдачи:
/// - Включает мотор хопера
/// - Ожидает подтверждения выдачи от датчиков (10 сек таймаут)
/// - Выключает мотор по завершению
/// - Обновляет счетчики выданных призов
/// - Воспроизводит звук ошибки при неудаче
/// </remarks>
public static void GivePrize()
{
// bool settingEnabled = SettingsManager.instance.GetSettingValue("Выдача призов при игре с ботом") == "Выдавать";
//
// if (!settingEnabled) { DebugWindow.Message("Выдача приза отключена в настройках"); return; }
if (_prizing)
{
GameManager.Logger.Log("PrizeSystem", "Уже идет выдача приза. Не спамь кнопку.");
return;
}
//
// DebugWindow.Message("Выдаю приз");
if (prizeCoroutine != null) Instance.StopCoroutine(prizeCoroutine);
prizeCoroutine = Instance.StartCoroutine(Instance.Prize());
}
//===========================end: PUBLIC API===========================//
if (!success) AudioManager.Instance.PlaySfx(prizeErrorSound);
}
#region Приватные методы
private IEnumerator Prize()
{
_prizing = true;
// DebugWindow.Message("Начинаю выдачу");
private static void IncreasePrizeCounters()
{
PlayerPrefs.SetInt(key_prizesgiven, PlayerPrefs.GetInt(key_prizesgiven) + 1);
PlayerPrefs.SetInt(key_prizesgivenNul, PlayerPrefs.GetInt(key_prizesgivenNul) + 1);
}
const float timeout = 10; //сек
ComWrapper.Send("301");//включаем мотор
float startTime = Time.time;
bool success = false;
private static void InitCounters()
{
if (!PlayerPrefs.HasKey(key_starts)) PlayerPrefs.SetInt(key_starts, 0);
if (!PlayerPrefs.HasKey(key_startsNul)) PlayerPrefs.SetInt(key_startsNul, 0);
if (!PlayerPrefs.HasKey(key_prizesgiven)) PlayerPrefs.SetInt(key_prizesgiven, 0);
if (!PlayerPrefs.HasKey(key_prizesgivenNul)) PlayerPrefs.SetInt(key_prizesgivenNul, 0);
PlayerPrefs.SetInt("PrizeGivenBuffer", 0);//На всякий случай подготовка буфера
//проверить где он пишется
while (Time.time - startTime < timeout)
{
if (PlayerPrefs.GetInt("PrizeGivenBuffer", 0) == 1)
{
success = true;
IncreasePrizeCounters();
break;
}
yield return null;
}
//Выключаем мотор
ComWrapper.Send("300");
// DebugWindow.Message("Закончил. Успех: " + success);
_prizing = false;
if (!success) AudioManager.Instance.PlaySfx(prizeErrorSound);
}
private static void IncreasePrizeCounters()
{
PlayerPrefs.SetInt(key_prizesgiven, PlayerPrefs.GetInt(key_prizesgiven) + 1);
PlayerPrefs.SetInt(key_prizesgivenNul, PlayerPrefs.GetInt(key_prizesgivenNul) + 1);
}
private static void InitCounters()
{
if (!PlayerPrefs.HasKey(key_starts)) PlayerPrefs.SetInt(key_starts, 0);
if (!PlayerPrefs.HasKey(key_startsNul)) PlayerPrefs.SetInt(key_startsNul, 0);
if (!PlayerPrefs.HasKey(key_prizesgiven)) PlayerPrefs.SetInt(key_prizesgiven, 0);
if (!PlayerPrefs.HasKey(key_prizesgivenNul)) PlayerPrefs.SetInt(key_prizesgivenNul, 0);
}
#endregion
}
#endregion
}
}

View File

@ -1,92 +1,95 @@
using RR.Game2D.Core.Managers;
using UnityEngine;
/// <summary>
/// Статический фасад для унифицированного доступа к системе ввода.
/// Предоставляет глобальный интерфейс для работы с управлением независимо от выбранного источника.
/// </summary>
/// <remarks>
/// Этот класс является точкой входа для всей игровой логики, работающей с вводом.
/// Автоматически инициализирует выбранный источник управления (клавиатура или COM-порт)
/// и предоставляет статические методы для удобного доступа из любого места кода.
///
/// Использование:
/// 1. Настройте тип ввода в инспекторе
/// 2. Вызовите Initialize() при старте игры
/// 3. Используйте статические методы для проверки ввода
/// </remarks>
public class In : MonoBehaviour
namespace RR.Common.Controls
{
[Header("Выбор источника управления")]
[SerializeField] private InputSourceType inputType;
private static InputSource source;
/// <summary>
/// Инициализирует систему ввода на основе выбранного типа.
/// Должен быть вызван один раз при запуске приложения.
/// Статический фасад для унифицированного доступа к системе ввода.
/// Предоставляет глобальный интерфейс для работы с управлением независимо от выбранного источника.
/// </summary>
/// <remarks>
/// Создает соответствующий экземпляр InputSource:
/// - KeyboardInput для тестирования на клавиатуре
/// - COMinput для работы с периферийной платой
/// Этот класс является точкой входа для всей игровой логики, работающей с вводом.
/// Автоматически инициализирует выбранный источник управления (клавиатура или COM-порт)
/// и предоставляет статические методы для удобного доступа из любого места кода.
///
/// Использование:
/// 1. Настройте тип ввода в инспекторе
/// 2. Вызовите Initialize() при старте игры
/// 3. Используйте статические методы для проверки ввода
/// </remarks>
public void Initialize()
public class In : MonoBehaviour
{
source = inputType switch
[Header("Выбор источника управления")]
[SerializeField] private InputSourceType inputType;
private static InputSource source;
/// <summary>
/// Инициализирует систему ввода на основе выбранного типа.
/// Должен быть вызван один раз при запуске приложения.
/// </summary>
/// <remarks>
/// Создает соответствующий экземпляр InputSource:
/// - KeyboardInput для тестирования на клавиатуре
/// - COMinput для работы с периферийной платой
/// </remarks>
public void Initialize()
{
InputSourceType.Keyboard => new KeyboardInput(),
InputSourceType.COM => new COMInput(),
// _ => throw new System.NotImplementedException()
_ => null
};
source = inputType switch
{
InputSourceType.Keyboard => new KeyboardInput(),
InputSourceType.COM => new COMInput(),
// _ => throw new System.NotImplementedException()
_ => null
};
var success = source?.Initialize() ?? false;
if (!success) GameManager.Logger.LogWarning("In.cs","Не удалось проинициалзировать источник управления.");
var success = source?.Initialize() ?? false;
if (!success) GameManager.Logger.LogWarning("In.cs", "Не удалось проинициалзировать источник управления.");
}
// ================================================================================
// СТАТИЧЕСКИЕ МЕТОДЫ ДЛЯ РАБОТЫ С ФИЗИЧЕСКИМИ ПИНАМИ
// ================================================================================
/// <summary>Проверяет состояние конкретной кнопки по её физическому пину</summary>
public static bool GetButton(ButtonPinMapping button) => source.GetButton(button);
/// <summary>Проверяет нажатие кнопки по пину в текущем кадре</summary>
public static bool GetButtonDown(ButtonPinMapping button) => source.GetButtonDown(button);
/// <summary>Проверяет отпускание кнопки по пину в текущем кадре</summary>
public static bool GetButtonUp(ButtonPinMapping button) => source.GetButtonUp(button);
/// <summary>Проверяет состояние кнопки по числовому индексу пина</summary>
public static bool GetButton(int pinIndex) => source.GetButton(pinIndex);
/// <summary>Проверяет нажатие кнопки по индексу пина в текущем кадре</summary>
public static bool GetButtonDown(int pinIndex) => source.GetButtonDown(pinIndex);
/// <summary>Проверяет отпускание кнопки по индексу пина в текущем кадре</summary>
public static bool GetButtonUp(int pinIndex) => source.GetButtonUp(pinIndex);
// ================================================================================
// СТАТИЧЕСКИЕ МЕТОДЫ ДЛЯ РАБОТЫ С АБСТРАКТНЫМИ ДЕЙСТВИЯМИ ИГРОКОВ
// ================================================================================
/// <summary>Проверяет состояние действия для любого из игроков</summary>
public static bool GetButtonAnyPlayer(PlayerAction button) => source.GetButtonAnyPlayer(button);
/// <summary>Проверяет начало действия любым игроком в текущем кадре</summary>
public static bool GetButtonDownAnyPlayer(PlayerAction button) => source.GetButtonDownAnyPlayer(button);
/// <summary>Проверяет завершение действия любым игроком в текущем кадре</summary>
public static bool GetButtonUpAnyPlayer(PlayerAction button) => source.GetButtonUpAnyPlayer(button);
/// <summary>Проверяет состояние действия для текущего активного игрока</summary>
public static bool GetButtonCurrentPlayer(PlayerAction button) => source.GetButtonCurrentPlayer(button);
/// <summary>Проверяет начало действия текущим игроком в текущем кадре</summary>
public static bool GetButtonDownCurrentPlayer(PlayerAction button) => source.GetButtonDownCurrentPlayer(button);
/// <summary>Проверяет завершение действия текущим игроком в текущем кадре</summary>
public static bool GetButtonUpCurrentPlayer(PlayerAction button) => source.GetButtonUpCurrentPlayer(button);
}
// ================================================================================
// СТАТИЧЕСКИЕ МЕТОДЫ ДЛЯ РАБОТЫ С ФИЗИЧЕСКИМИ ПИНАМИ
// ================================================================================
/// <summary>Проверяет состояние конкретной кнопки по её физическому пину</summary>
public static bool GetButton(ButtonPinMapping button) => source.GetButton(button);
/// <summary>Проверяет нажатие кнопки по пину в текущем кадре</summary>
public static bool GetButtonDown(ButtonPinMapping button) => source.GetButtonDown(button);
/// <summary>Проверяет отпускание кнопки по пину в текущем кадре</summary>
public static bool GetButtonUp(ButtonPinMapping button) => source.GetButtonUp(button);
/// <summary>Проверяет состояние кнопки по числовому индексу пина</summary>
public static bool GetButton(int pinIndex) => source.GetButton(pinIndex);
/// <summary>Проверяет нажатие кнопки по индексу пина в текущем кадре</summary>
public static bool GetButtonDown(int pinIndex) => source.GetButtonDown(pinIndex);
/// <summary>Проверяет отпускание кнопки по индексу пина в текущем кадре</summary>
public static bool GetButtonUp(int pinIndex) => source.GetButtonUp(pinIndex);
// ================================================================================
// СТАТИЧЕСКИЕ МЕТОДЫ ДЛЯ РАБОТЫ С АБСТРАКТНЫМИ ДЕЙСТВИЯМИ ИГРОКОВ
// ================================================================================
/// <summary>Проверяет состояние действия для любого из игроков</summary>
public static bool GetButtonAnyPlayer(PlayerAction button) => source.GetButtonAnyPlayer(button);
/// <summary>Проверяет начало действия любым игроком в текущем кадре</summary>
public static bool GetButtonDownAnyPlayer(PlayerAction button) => source.GetButtonDownAnyPlayer(button);
/// <summary>Проверяет завершение действия любым игроком в текущем кадре</summary>
public static bool GetButtonUpAnyPlayer(PlayerAction button) => source.GetButtonUpAnyPlayer(button);
/// <summary>Проверяет состояние действия для текущего активного игрока</summary>
public static bool GetButtonCurrentPlayer(PlayerAction button) => source.GetButtonCurrentPlayer(button);
/// <summary>Проверяет начало действия текущим игроком в текущем кадре</summary>
public static bool GetButtonDownCurrentPlayer(PlayerAction button) => source.GetButtonDownCurrentPlayer(button);
/// <summary>Проверяет завершение действия текущим игроком в текущем кадре</summary>
public static bool GetButtonUpCurrentPlayer(PlayerAction button) => source.GetButtonUpCurrentPlayer(button);
}

View File

@ -1,117 +1,120 @@
/// <summary>
/// Абстрактный базовый класс для всех источников ввода в системе.
/// Определяет унифицированный интерфейс для работы с аппаратными кнопками и виртуальными действиями игроков.
/// </summary>
/// <remarks>
/// Наследники этого класса реализуют конкретные способы ввода:
/// - COMinput: работа с периферийной платой через COM-порт
/// - KeyboardInput: эмуляция ввода с клавиатуры для тестирования
///
/// Класс предоставляет три уровня абстракции:
/// 1. Низкоуровневый доступ к физическим пинам
/// 2. Доступ к абстрактным игровым действиям
/// 3. Мультиплеерные методы для работы с несколькими игроками
/// </remarks>
public abstract class InputSource
namespace RR.Common.Controls
{
/// <summary>
/// Инициализирует источник ввода. Должен быть вызван перед использованием других методов.
/// Абстрактный базовый класс для всех источников ввода в системе.
/// Определяет унифицированный интерфейс для работы с аппаратными кнопками и виртуальными действиями игроков.
/// </summary>
/// <returns>true если инициализация прошла успешно, false в случае ошибки</returns>
public abstract bool Initialize();
/// <remarks>
/// Наследники этого класса реализуют конкретные способы ввода:
/// - COMinput: работа с периферийной платой через COM-порт
/// - KeyboardInput: эмуляция ввода с клавиатуры для тестирования
///
/// Класс предоставляет три уровня абстракции:
/// 1. Низкоуровневый доступ к физическим пинам
/// 2. Доступ к абстрактным игровым действиям
/// 3. Мультиплеерные методы для работы с несколькими игроками
/// </remarks>
public abstract class InputSource
{
/// <summary>
/// Инициализирует источник ввода. Должен быть вызван перед использованием других методов.
/// </summary>
/// <returns>true если инициализация прошла успешно, false в случае ошибки</returns>
public abstract bool Initialize();
//----------------------------------------------------------------------------------
//----------------------------------------------------------------------------------
/// <summary>
/// Получает текущее состояние конкретной кнопки по её физическому пину.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
public abstract bool GetButton(ButtonPinMapping button);
/// <summary>
/// Получает текущее состояние конкретной кнопки по её физическому пину.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
public abstract bool GetButton(ButtonPinMapping button);
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true только в первом кадре после нажатия кнопки</returns>
public abstract bool GetButtonDown(ButtonPinMapping button);
/// <summary>
/// Проверяет, была ли кнопка нажата в начале текущего кадра.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true только в первом кадре после нажатия кнопки</returns>
public abstract bool GetButtonDown(ButtonPinMapping button);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true только в первом кадре после отпускания кнопки</returns>
public abstract bool GetButtonUp(ButtonPinMapping button);
/// <summary>
/// Проверяет, была ли кнопка отпущена в начале текущего кадра.
/// </summary>
/// <param name="button">Пин кнопки на периферийной плате</param>
/// <returns>true только в первом кадре после отпускания кнопки</returns>
public abstract bool GetButtonUp(ButtonPinMapping button);
//----------------------------------------------------------------------------------
//----------------------------------------------------------------------------------
/// <summary>
/// Получает состояние кнопки по числовому индексу пина.
/// Перегрузка для работы с системами, где количество пинов превышает размер перечисления.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
public abstract bool GetButton(int pinIndex);
/// <summary>
/// Получает состояние кнопки по числовому индексу пина.
/// Перегрузка для работы с системами, где количество пинов превышает размер перечисления.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true если кнопка нажата в текущий момент</returns>
public abstract bool GetButton(int pinIndex);
/// <summary>
/// Проверяет нажатие кнопки по числовому индексу пина в текущем кадре.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true только в первом кадре после нажатия</returns>
public abstract bool GetButtonDown(int pinIndex);
/// <summary>
/// Проверяет нажатие кнопки по числовому индексу пина в текущем кадре.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true только в первом кадре после нажатия</returns>
public abstract bool GetButtonDown(int pinIndex);
/// <summary>
/// Проверяет отпускание кнопки по числовому индексу пина в текущем кадре.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true только в первом кадре после отпускания</returns>
public abstract bool GetButtonUp(int pinIndex);
/// <summary>
/// Проверяет отпускание кнопки по числовому индексу пина в текущем кадре.
/// </summary>
/// <param name="pinIndex">Числовой индекс пина (0-based)</param>
/// <returns>true только в первом кадре после отпускания</returns>
public abstract bool GetButtonUp(int pinIndex);
//----------------------------------------------------------------------------------
//----------------------------------------------------------------------------------
/// <summary>
/// Получает состояние игрового действия для любого из игроков.
/// Используется для мультиплеерных сценариев где важно действие любого игрока.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие активно у первого ИЛИ второго игрока</returns>
public abstract bool GetButtonAnyPlayer(PlayerAction button);
/// <summary>
/// Получает состояние игрового действия для любого из игроков.
/// Используется для мультиплеерных сценариев где важно действие любого игрока.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие активно у первого ИЛИ второго игрока</returns>
public abstract bool GetButtonAnyPlayer(PlayerAction button);
/// <summary>
/// Проверяет начало игрового действия любым игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие начато любым игроком в этом кадре</returns>
public abstract bool GetButtonDownAnyPlayer(PlayerAction button);
/// <summary>
/// Проверяет начало игрового действия любым игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие начато любым игроком в этом кадре</returns>
public abstract bool GetButtonDownAnyPlayer(PlayerAction button);
/// <summary>
/// Проверяет завершение игрового действия любым игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие завершено любым игроком в этом кадре</returns>
public abstract bool GetButtonUpAnyPlayer(PlayerAction button);
/// <summary>
/// Проверяет завершение игрового действия любым игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие завершено любым игроком в этом кадре</returns>
public abstract bool GetButtonUpAnyPlayer(PlayerAction button);
//----------------------------------------------------------------------------------
//----------------------------------------------------------------------------------
/// <summary>
/// Получает состояние игрового действия для текущего активного игрока.
/// Используется для одиночной игры или поочередного управления.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие активно у текущего игрока</returns>
public abstract bool GetButtonCurrentPlayer(PlayerAction button);
/// <summary>
/// Получает состояние игрового действия для текущего активного игрока.
/// Используется для одиночной игры или поочередного управления.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие активно у текущего игрока</returns>
public abstract bool GetButtonCurrentPlayer(PlayerAction button);
/// <summary>
/// Проверяет начало игрового действия текущим игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие начато текущим игроком в этом кадре</returns>
public abstract bool GetButtonDownCurrentPlayer(PlayerAction button);
/// <summary>
/// Проверяет начало игрового действия текущим игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие начато текущим игроком в этом кадре</returns>
public abstract bool GetButtonDownCurrentPlayer(PlayerAction button);
/// <summary>
/// Проверяет завершение игрового действия текущим игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие завершено текущим игроком в этом кадре</returns>
public abstract bool GetButtonUpCurrentPlayer(PlayerAction button);
/// <summary>
/// Проверяет завершение игрового действия текущим игроком в текущем кадре.
/// </summary>
/// <param name="button">Абстрактное игровое действие</param>
/// <returns>true если действие завершено текущим игроком в этом кадре</returns>
public abstract bool GetButtonUpCurrentPlayer(PlayerAction button);
}
}

View File

@ -1,5 +1,11 @@
public enum InputSourceType
namespace RR.Common.Controls
{
COM,
Keyboard
}
/// <summary>
/// Типы источников ввода для управления игрой.
/// </summary>
public enum InputSourceType
{
COM,
Keyboard
}
}

View File

@ -9,262 +9,264 @@ using RR.Game2D.Core.Managers;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
/// <summary>
/// Реализация системы ввода с клавиатуры для тестирования и разработки.
/// Эмулирует работу периферийной платы через стандартные клавиши клавиатуры.
/// </summary>
/// <remarks>
/// Соответствие клавиш и пинов:
/// - Игрок 1: WASD + Space
/// - Игрок 2: Стрелки + RightControl
/// - Служебные: 1, 2, 3, F5
/// - Датчики хопера: O, P
/// </remarks>
public class KeyboardInput : InputSource
namespace RR.Common.Controls
{
private int currentPlayer = 1;//ПЕРЕОПРЕДЕЛИТЕ ОПРЕДЕЛЕНИЕ ТЕКУЩЕГО ИГРОКА ПОД СВОЮ СИСТЕМУ.
private GameManager gm;//я получаю кто сейчас ходит через этого менеджера. Если у вас другое, допишите.
// Кэш Key -> KeyControl + ссылка на то, для какой клавиатуры он построен
private static readonly Dictionary<Key, KeyControl> _keyCache = new();
private static Keyboard _cachedKeyboard;
private static void EnsureKeyboardCache()
/// <summary>
/// Реализация системы ввода с клавиатуры для тестирования и разработки.
/// Эмулирует работу периферийной платы через стандартные клавиши клавиатуры.
/// </summary>
/// <remarks>
/// Соответствие клавиш и пинов:
/// - Игрок 1: WASD + Space
/// - Игрок 2: Стрелки + RightControl
/// - Служебные: 1, 2, 3, F5
/// - Датчики хопера: O, P
/// </remarks>
public class KeyboardInput : InputSource
{
var kb = Keyboard.current;
if (kb == null)
private int currentPlayer = 1;//ПЕРЕОПРЕДЕЛИТЕ ОПРЕДЕЛЕНИЕ ТЕКУЩЕГО ИГРОКА ПОД СВОЮ СИСТЕМУ.
private GameManager gm;//я получаю кто сейчас ходит через этого менеджера. Если у вас другое, допишите.
// Кэш Key -> KeyControl + ссылка на то, для какой клавиатуры он построен
private static readonly Dictionary<Key, KeyControl> _keyCache = new();
private static Keyboard _cachedKeyboard;
private static void EnsureKeyboardCache()
{
var kb = Keyboard.current;
if (kb == null)
{
_keyCache.Clear();
_cachedKeyboard = null;
return;
}
// Если клавиатура сменилась (или кэш пуст) — перестроить
if (_cachedKeyboard == kb && _keyCache.Count != 0) return;
_cachedKeyboard = kb;
_keyCache.Clear();
_cachedKeyboard = null;
return;
var keys = kb.allKeys;
if (keys.Count <= 0) return;
foreach (var kc in keys.Where(kc => kc != null))
{
_keyCache[kc.keyCode] = kc;
}
}
// Если клавиатура сменилась (или кэш пуст) — перестроить
if (_cachedKeyboard == kb && _keyCache.Count != 0) return;
_cachedKeyboard = kb;
_keyCache.Clear();
var keys = kb.allKeys;
if (keys.Count <= 0) return;
foreach (var kc in keys.Where(kc => kc != null))
private static KeyControl GetKeyControl(Key key)
{
_keyCache[kc.keyCode] = kc;
if (key == Key.None) return null;
// Можем спокойно вызываться даже в самом раннем кадре
EnsureKeyboardCache();
return _cachedKeyboard == null ? null : _keyCache.GetValueOrDefault(key);
}
// --- адаптер: из твоего старого KeyCode -> в новый Key ---
private static Key ToNewKey(UnityEngine.KeyCode old)
{
// Пытаемся по имени (у многих совпадают)
if (Enum.TryParse<Key>(old.ToString(), true, out var k)) return k;
// Несовпадающие названия — руками:
return old switch
{
KeyCode.Return => Key.Enter,
KeyCode.KeypadEnter => Key.NumpadEnter,
KeyCode.LeftControl => Key.LeftCtrl,
KeyCode.RightControl => Key.RightCtrl,
KeyCode.LeftShift => Key.LeftShift,
KeyCode.RightShift => Key.RightShift,
KeyCode.LeftAlt => Key.LeftAlt,
KeyCode.RightAlt => Key.RightAlt,
KeyCode.LeftCommand => Key.LeftMeta,
KeyCode.RightCommand => Key.RightMeta,
KeyCode.None => Key.None,
_ => Key.None // если нужен редкий кей — добавь сюда
};
}
// Удобные обёртки
private static bool IsPressed(Key k)
{
if (k == Key.None) return false;
var c = GetKeyControl(k);
return c is { isPressed: true };
}
private static bool WasPressedThisFrame(Key k)
{
if (k == Key.None) return false;
var c = GetKeyControl(k);
return c is { wasPressedThisFrame: true };
}
private static bool WasReleasedThisFrame(Key k)
{
if (k == Key.None) return false;
var c = GetKeyControl(k);
return c is { wasReleasedThisFrame: true };
}
/// Проверяет состояние кнопки по её физическому пину через клавиатуру (Input System)
public override bool GetButton(ButtonPinMapping button) => IsPressed(ToNewKey(getKeyCode(button)));
/// Проверяет нажатие кнопки по пину в текущем кадре (Input System)
public override bool GetButtonDown(ButtonPinMapping button) => WasPressedThisFrame(ToNewKey(getKeyCode(button)));
/// Проверяет отпускание кнопки по пину в текущем кадре (Input System)
public override bool GetButtonUp(ButtonPinMapping button) => WasReleasedThisFrame(ToNewKey(getKeyCode(button)));
//----------------------------------------------------------------------------------
/// По числовому индексу пина
public override bool GetButton(int pinIndex) => IsPressed(ToNewKey(getKeyCode(pinIndex)));
public override bool GetButtonDown(int pinIndex) => WasPressedThisFrame(ToNewKey(getKeyCode(pinIndex)));
public override bool GetButtonUp(int pinIndex) => WasReleasedThisFrame(ToNewKey(getKeyCode(pinIndex)));
//----------------------------------------------------------------------------------
/// Действия текущего игрока (для клавиатуры — это по сути игрок 1)
public override bool GetButtonCurrentPlayer(PlayerAction button) => GetButton(GetButtonForCurrentPlayer(button));
public override bool GetButtonDownCurrentPlayer(PlayerAction button) => GetButtonDown(GetButtonForCurrentPlayer(button));
public override bool GetButtonUpCurrentPlayer(PlayerAction button) => GetButtonUp(GetButtonForCurrentPlayer(button));
//----------------------------------------------------------------------------------
/// Любой игрок (для клавиатуры — эквивалент текущего)
public override bool GetButtonAnyPlayer(PlayerAction button) => GetButtonCurrentPlayer(button);
public override bool GetButtonDownAnyPlayer(PlayerAction button) => GetButtonDownCurrentPlayer(button);
public override bool GetButtonUpAnyPlayer(PlayerAction button) => GetButtonUpCurrentPlayer(button);
//----------------------------------------------------------------------------------
/// <summary>
/// Инициализирует клавиатурный ввод. Всегда возвращает true так как клавиатура всегда доступна.
/// </summary>
/// <returns>Всегда true</returns>
public override bool Initialize()
{
gm = GameManager.Instance;
return true;
}
// ================================================================================
// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
// ================================================================================
/// <summary>
/// Определяет кнопку текущего игрока для указанного действия.
/// Для клавиатуры всегда возвращает кнопку первого игрока.
/// </summary>
private ButtonPinMapping GetButtonForCurrentPlayer(PlayerAction button)
{
var buttons = _buttonByAction(button);
return currentPlayer == 1 ? buttons.player1button : buttons.player2button;
}
/// <summary>
/// Проверяет действие для обоих игроков используя указанную функцию проверки.
/// </summary>
private bool CheckBothPlayers(PlayerAction action, System.Func<ButtonPinMapping, bool> buttonFunc)
{
var buttons = _buttonByAction(action);
return buttonFunc(buttons.player1button) || buttonFunc(buttons.player2button);
}
/// <summary>
/// Возвращает соответствие между игровыми действиями и физическими кнопками игроков.
/// </summary>
private (ButtonPinMapping player1button, ButtonPinMapping player2button) _buttonByAction(PlayerAction playerAction) =>
playerAction switch
{
PlayerAction.up => (ButtonPinMapping.up1, ButtonPinMapping.up2),
PlayerAction.down => (ButtonPinMapping.down1, ButtonPinMapping.down2),
PlayerAction.left => (ButtonPinMapping.left1, ButtonPinMapping.left2),
PlayerAction.right => (ButtonPinMapping.right1, ButtonPinMapping.right2),
PlayerAction.action => (ButtonPinMapping.action1, ButtonPinMapping.action2),
PlayerAction.extra => (ButtonPinMapping.extra, ButtonPinMapping.extra),
_ => throw new System.ArgumentException($"Unknown player action: {playerAction}")
};
/// <summary>
/// Преобразует физический пин в соответствующую клавишу клавиатуры.
/// </summary>
private KeyCode getKeyCode(ButtonPinMapping button)
{
return button switch
{
ButtonPinMapping.up1 => KeyCode.W,
ButtonPinMapping.right1 => KeyCode.D,
ButtonPinMapping.down1 => KeyCode.S,
ButtonPinMapping.left1 => KeyCode.A,
ButtonPinMapping.action1 => KeyCode.Space,
ButtonPinMapping.up2 => KeyCode.UpArrow,
ButtonPinMapping.right2 => KeyCode.RightArrow,
ButtonPinMapping.down2 => KeyCode.DownArrow,
ButtonPinMapping.left2 => KeyCode.LeftArrow,
ButtonPinMapping.action2 => KeyCode.RightControl,
ButtonPinMapping.startOnePlayer => KeyCode.Alpha1,
ButtonPinMapping.startTwoPlayers => KeyCode.Alpha2,
ButtonPinMapping.secret => KeyCode.F5,
ButtonPinMapping.extra => KeyCode.Alpha3,
ButtonPinMapping.hasPrize => KeyCode.O,
ButtonPinMapping.prizeGiven => KeyCode.P,
_ => KeyCode.None,
};
}
/// <summary>
/// Преобразует числовой индекс пина в соответствующую клавишу клавиатуры.
/// Используется для методов с прямым доступом по индексу.
/// </summary>
private KeyCode getKeyCode(int pinIndex)
{
return pinIndex switch
{
0 => KeyCode.W, // up1
1 => KeyCode.D, // right1
2 => KeyCode.S, // down1
3 => KeyCode.A, // left1
4 => KeyCode.Space, // action1
5 => KeyCode.UpArrow, // up2
6 => KeyCode.RightArrow, // right2
7 => KeyCode.DownArrow, // down2
8 => KeyCode.LeftArrow, // left2
9 => KeyCode.RightControl, // action2
10 => KeyCode.Alpha1, // startOnePlayer
11 => KeyCode.Alpha2, // startTwoPlayers
12 => KeyCode.F5, // secret
13 => KeyCode.Alpha3, // extra
14 => KeyCode.O, // hasPrize
15 => KeyCode.P, // prizeGiven
_ => KeyCode.None, // невалидный индекс
};
}
}
private static KeyControl GetKeyControl(Key key)
{
if (key == Key.None) return null;
// Можем спокойно вызываться даже в самом раннем кадре
EnsureKeyboardCache();
return _cachedKeyboard == null ? null : _keyCache.GetValueOrDefault(key);
}
// --- адаптер: из твоего старого KeyCode -> в новый Key ---
private static Key ToNewKey(UnityEngine.KeyCode old)
{
// Пытаемся по имени (у многих совпадают)
if (Enum.TryParse<Key>(old.ToString(), true, out var k)) return k;
// Несовпадающие названия — руками:
return old switch
{
KeyCode.Return => Key.Enter,
KeyCode.KeypadEnter => Key.NumpadEnter,
KeyCode.LeftControl => Key.LeftCtrl,
KeyCode.RightControl => Key.RightCtrl,
KeyCode.LeftShift => Key.LeftShift,
KeyCode.RightShift => Key.RightShift,
KeyCode.LeftAlt => Key.LeftAlt,
KeyCode.RightAlt => Key.RightAlt,
KeyCode.LeftCommand => Key.LeftMeta,
KeyCode.RightCommand => Key.RightMeta,
KeyCode.None => Key.None,
_ => Key.None // если нужен редкий кей — добавь сюда
};
}
// Удобные обёртки
private static bool IsPressed(Key k)
{
if (k == Key.None) return false;
var c = GetKeyControl(k);
return c is { isPressed: true };
}
private static bool WasPressedThisFrame(Key k)
{
if (k == Key.None) return false;
var c = GetKeyControl(k);
return c is { wasPressedThisFrame: true };
}
private static bool WasReleasedThisFrame(Key k)
{
if (k == Key.None) return false;
var c = GetKeyControl(k);
return c is { wasReleasedThisFrame: true };
}
/// Проверяет состояние кнопки по её физическому пину через клавиатуру (Input System)
public override bool GetButton(ButtonPinMapping button) => IsPressed(ToNewKey(getKeyCode(button)));
/// Проверяет нажатие кнопки по пину в текущем кадре (Input System)
public override bool GetButtonDown(ButtonPinMapping button) => WasPressedThisFrame(ToNewKey(getKeyCode(button)));
/// Проверяет отпускание кнопки по пину в текущем кадре (Input System)
public override bool GetButtonUp(ButtonPinMapping button) => WasReleasedThisFrame(ToNewKey(getKeyCode(button)));
//----------------------------------------------------------------------------------
/// По числовому индексу пина
public override bool GetButton(int pinIndex) => IsPressed(ToNewKey(getKeyCode(pinIndex)));
public override bool GetButtonDown(int pinIndex) => WasPressedThisFrame(ToNewKey(getKeyCode(pinIndex)));
public override bool GetButtonUp(int pinIndex) => WasReleasedThisFrame(ToNewKey(getKeyCode(pinIndex)));
//----------------------------------------------------------------------------------
/// Действия текущего игрока (для клавиатуры — это по сути игрок 1)
public override bool GetButtonCurrentPlayer(PlayerAction button) => GetButton(GetButtonForCurrentPlayer(button));
public override bool GetButtonDownCurrentPlayer(PlayerAction button) => GetButtonDown(GetButtonForCurrentPlayer(button));
public override bool GetButtonUpCurrentPlayer(PlayerAction button) => GetButtonUp(GetButtonForCurrentPlayer(button));
//----------------------------------------------------------------------------------
/// Любой игрок (для клавиатуры — эквивалент текущего)
public override bool GetButtonAnyPlayer(PlayerAction button) => GetButtonCurrentPlayer(button);
public override bool GetButtonDownAnyPlayer(PlayerAction button) => GetButtonDownCurrentPlayer(button);
public override bool GetButtonUpAnyPlayer(PlayerAction button) => GetButtonUpCurrentPlayer(button);
//----------------------------------------------------------------------------------
/// <summary>
/// Инициализирует клавиатурный ввод. Всегда возвращает true так как клавиатура всегда доступна.
/// </summary>
/// <returns>Всегда true</returns>
public override bool Initialize()
{
gm = GameManager.Instance;
return true;
}
// ================================================================================
// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ
// ================================================================================
/// <summary>
/// Определяет кнопку текущего игрока для указанного действия.
/// Для клавиатуры всегда возвращает кнопку первого игрока.
/// </summary>
private ButtonPinMapping GetButtonForCurrentPlayer(PlayerAction button)
{
var buttons = _buttonByAction(button);
return currentPlayer == 1 ? buttons.player1button : buttons.player2button;
}
/// <summary>
/// Проверяет действие для обоих игроков используя указанную функцию проверки.
/// </summary>
private bool CheckBothPlayers(PlayerAction action, System.Func<ButtonPinMapping, bool> buttonFunc)
{
var buttons = _buttonByAction(action);
return buttonFunc(buttons.player1button) || buttonFunc(buttons.player2button);
}
/// <summary>
/// Возвращает соответствие между игровыми действиями и физическими кнопками игроков.
/// </summary>
private (ButtonPinMapping player1button, ButtonPinMapping player2button) _buttonByAction(PlayerAction playerAction) =>
playerAction switch
{
PlayerAction.up => (ButtonPinMapping.up1, ButtonPinMapping.up2),
PlayerAction.down => (ButtonPinMapping.down1, ButtonPinMapping.down2),
PlayerAction.left => (ButtonPinMapping.left1, ButtonPinMapping.left2),
PlayerAction.right => (ButtonPinMapping.right1, ButtonPinMapping.right2),
PlayerAction.action => (ButtonPinMapping.action1, ButtonPinMapping.action2),
PlayerAction.extra => (ButtonPinMapping.extra, ButtonPinMapping.extra),
_ => throw new System.ArgumentException($"Unknown player action: {playerAction}")
};
/// <summary>
/// Преобразует физический пин в соответствующую клавишу клавиатуры.
/// </summary>
private KeyCode getKeyCode(ButtonPinMapping button)
{
return button switch
{
ButtonPinMapping.up1 => KeyCode.W,
ButtonPinMapping.right1 => KeyCode.D,
ButtonPinMapping.down1 => KeyCode.S,
ButtonPinMapping.left1 => KeyCode.A,
ButtonPinMapping.action1 => KeyCode.Space,
ButtonPinMapping.up2 => KeyCode.UpArrow,
ButtonPinMapping.right2 => KeyCode.RightArrow,
ButtonPinMapping.down2 => KeyCode.DownArrow,
ButtonPinMapping.left2 => KeyCode.LeftArrow,
ButtonPinMapping.action2 => KeyCode.RightControl,
ButtonPinMapping.startOnePlayer => KeyCode.Alpha1,
ButtonPinMapping.startTwoPlayers => KeyCode.Alpha2,
ButtonPinMapping.secret => KeyCode.F5,
ButtonPinMapping.extra => KeyCode.Alpha3,
ButtonPinMapping.hasPrize => KeyCode.O,
ButtonPinMapping.prizeGiven => KeyCode.P,
_ => KeyCode.None,
};
}
/// <summary>
/// Преобразует числовой индекс пина в соответствующую клавишу клавиатуры.
/// Используется для методов с прямым доступом по индексу.
/// </summary>
private KeyCode getKeyCode(int pinIndex)
{
return pinIndex switch
{
0 => KeyCode.W, // up1
1 => KeyCode.D, // right1
2 => KeyCode.S, // down1
3 => KeyCode.A, // left1
4 => KeyCode.Space, // action1
5 => KeyCode.UpArrow, // up2
6 => KeyCode.RightArrow, // right2
7 => KeyCode.DownArrow, // down2
8 => KeyCode.LeftArrow, // left2
9 => KeyCode.RightControl, // action2
10 => KeyCode.Alpha1, // startOnePlayer
11 => KeyCode.Alpha2, // startTwoPlayers
12 => KeyCode.F5, // secret
13 => KeyCode.Alpha3, // extra
14 => KeyCode.O, // hasPrize
15 => KeyCode.P, // prizeGiven
_ => KeyCode.None, // невалидный индекс
};
}
}
#else
/// <summary>
@ -441,3 +443,4 @@ public class KeyboardInput : InputSource
#endif
}

View File

@ -1,19 +1,22 @@
/// <summary>
/// Абстрактные игровые действия для управления игроком.
/// Обеспечивает независимость от физического расположения кнопок.
/// </summary>
public enum PlayerAction
namespace RR.Common.Controls
{
/// <summary>Вверх</summary>
up,
/// <summary>Вниз</summary>
down,
/// <summary>Влево</summary>
left,
/// <summary>Вправо</summary>
right,
/// <summary>Основное действие</summary>
action,
/// <summary>Дополнительное действие(ВНИМАНИЕ РАБОТАЕТ У ОБОИХ ИГРОКОВ ОДНОВРЕМЕННО)</summary>
extra
}
/// <summary>
/// Абстрактные игровые действия для управления игроком.
/// Обеспечивает независимость от физического расположения кнопок.
/// </summary>
public enum PlayerAction
{
/// <summary>Вверх</summary>
up,
/// <summary>Вниз</summary>
down,
/// <summary>Влево</summary>
left,
/// <summary>Вправо</summary>
right,
/// <summary>Основное действие</summary>
action,
/// <summary>Дополнительное действие(ВНИМАНИЕ РАБОТАЕТ У ОБОИХ ИГРОКОВ ОДНОВРЕМЕННО)</summary>
extra
}
}

View File

@ -0,0 +1,40 @@
using System;
using UnityEngine;
using RR.Game2D.Behaviour;
namespace RR.Game2D.Entities
{
[RequireComponent(typeof(BoxCollider2D))]
public class JumpPad2D : MonoBehaviour
{
[SerializeField] private float verticalBand = 0.75f;
private BoxCollider2D _col;
public Action<GameObject> PlayerJumped;
private void Awake()
{
_col = GetComponent<BoxCollider2D>();
if (!_col.isTrigger)
_col.isTrigger = true;
}
private void OnTriggerEnter2D(Collider2D other)
{
if (!other.CompareTag("Player")) return;
PlayerController playerController = other.GetComponent<PlayerController>();
Rigidbody2D playerRB = other.GetComponent<Rigidbody2D>();
if (!(Mathf.Abs(other.transform.position.y - _col.bounds.max.y) <= verticalBand)) return;
Vector2 newPlayerVelocity = playerRB.linearVelocity;
newPlayerVelocity.y = playerController.jumpVelocity;
playerRB.linearVelocity = newPlayerVelocity;
PlayerJumped?.Invoke(other.gameObject);
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using RR.Game2D.Core;
using UnityEngine;
namespace RR.Game2D.Entities
{
public class MovingPlatform : MonoBehaviour
{
[Header("Настройки движения платформы")]
[SerializeField] private float moveSpeed = 1f;
[SerializeField] private float pathLength = 5f;
[SerializeField] private Direction2D moveDirection = Direction2D.Right;
[Header("Прикрепление игрока")]
[SerializeField] private bool attachPlayer = true;
private Vector3 _startPosition;
private Vector3 _endPosition;
private void Awake()
{
_startPosition = transform.position;
_endPosition = moveDirection switch
{
Direction2D.Down => _startPosition + Vector3.down * pathLength,
Direction2D.Left => _startPosition + Vector3.left * pathLength,
Direction2D.Right => _startPosition + Vector3.right * pathLength,
Direction2D.Up => _startPosition + Vector3.up * pathLength,
_ => _startPosition
};
}
private void FixedUpdate()
{
float pingPong = Mathf.PingPong(Time.time * moveSpeed, 1f);
transform.position = Vector3.Lerp(_startPosition, _endPosition, pingPong);
}
private void OnCollisionEnter2D(Collision2D other)
{
if (!attachPlayer) return;
if (!other.gameObject.CompareTag("Player")) return;
other.transform.SetParent(transform);
}
private void OnCollisionExit2D(Collision2D other)
{
if (!attachPlayer) return;
if (!other.gameObject.CompareTag("Player")) return;
other.transform.SetParent(null);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 76ff605ebb30e1043bf07de270a4cd73

View File

@ -1,16 +1,19 @@
using UnityEngine;
using UnityEngine;
using RR.Game2D.Core.Managers;
public class CoinCollectible : Collectible
namespace RR.Common.Entities.Props
{
public override bool TryPickup(GameObject newOwner, Transform optionalAttachPoint = null)
public class CoinCollectible : Collectible
{
base.TryPickup(newOwner, optionalAttachPoint);
GameManager.Instance.AddCoin();
Destroy(gameObject);
return true;
public override bool TryPickup(GameObject newOwner, Transform optionalAttachPoint = null)
{
base.TryPickup(newOwner, optionalAttachPoint);
GameManager.Instance.AddCoin();
Destroy(gameObject);
return true;
}
}
}
}

View File

@ -1,34 +1,37 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.VFX;
using RR.Common.Interfaces;
using RR.Common.Audio;
using RR.Game2D.Core.Managers;
[RequireComponent(typeof(Collider2D))]
public class Collectible : MonoBehaviour, IPickable
namespace RR.Common.Entities.Props
{
[SerializeField] private SoundEffect pickupSFX;
[SerializeField] private VisualEffect pickupVFX;
private void OnTriggerEnter2D(Collider2D other)
[RequireComponent(typeof(Collider2D))]
public class Collectible : MonoBehaviour, IPickable
{
if (other.CompareTag("Player")) TryPickup(other.gameObject);
}
[SerializeField] private SoundEffect pickupSFX;
[SerializeField] private VisualEffect pickupVFX;
public virtual bool TryPickup(GameObject newOwner, Transform optionalAttachPoint = null)
{
if (pickupSFX.sound != null)
GameManager.Instance.Audio.PlaySfxAt(pickupSFX.sound, transform.position,
pickupSFX.volume, pickupSFX.pitch);
if (pickupVFX != null)
private void OnTriggerEnter2D(Collider2D other)
{
pickupVFX.transform.position = transform.position;
pickupVFX.Play();
pickupVFX.Stop();
if (other.CompareTag("Player")) TryPickup(other.gameObject);
}
public virtual bool TryPickup(GameObject newOwner, Transform optionalAttachPoint = null)
{
if (pickupSFX.sound != null)
GameManager.Instance.Audio.PlaySfxAt(pickupSFX.sound, transform.position,
pickupSFX.volume, pickupSFX.pitch);
if (pickupVFX != null)
{
pickupVFX.transform.position = transform.position;
pickupVFX.Play();
pickupVFX.Stop();
}
return true;
}
return true;
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections;
using RR.Game2D.Core;
using UnityEngine;
public class ShiftingPlatform : MonoBehaviour
{
[Header("Настройки смещения платформы")]
[SerializeField] private Vector3 shiftAmount = new Vector3(0f, 2f, 0f);
[SerializeField] private float shiftDuration = 1f;
[SerializeField] private Direction2D shiftDirection = Direction2D.Down;
[Header("Прикрепление игрока")]
[SerializeField] private bool attachPlayer = true;
private Vector3 _startPosition;
private void Awake()
{
_startPosition = transform.position;
}
private void OnCollisionEnter2D(Collision2D other)
{
if (!other.gameObject.CompareTag("Player")) return;
if (attachPlayer) other.transform.SetParent(transform);
Vector3 shiftVector = shiftDirection switch
{
Direction2D.Down => Vector3.down * shiftAmount.y,
Direction2D.Left => Vector3.left * shiftAmount.x,
Direction2D.Right => Vector3.right * shiftAmount.x,
Direction2D.Up => Vector3.up * shiftAmount.y,
_ => Vector3.zero
};
StopAllCoroutines();
StartCoroutine(ShiftPlatform(shiftVector));
}
private void OnCollisionExit2D(Collision2D other)
{
if (!other.gameObject.CompareTag("Player")) return;
if (attachPlayer) other.transform.SetParent(null);
Vector3 shiftVector = shiftDirection switch
{
Direction2D.Down => Vector3.up * shiftAmount.y,
Direction2D.Left => Vector3.right * shiftAmount.x,
Direction2D.Right => Vector3.left * shiftAmount.x,
Direction2D.Up => Vector3.down * shiftAmount.y,
_ => Vector3.zero
};
StopAllCoroutines();
StartCoroutine(ShiftPlatform(shiftVector));
}
private IEnumerator ShiftPlatform(Vector3 shiftVector)
{
Vector3 initialPosition = transform.position;
Vector3 finalPosition = initialPosition + shiftVector;
float elapsedTime = 0f;
while (elapsedTime < shiftDuration)
{
transform.position = Vector3.Lerp(initialPosition, finalPosition, (elapsedTime / shiftDuration));
elapsedTime += Time.deltaTime;
yield return null;
}
transform.position = finalPosition;
}
}

View File

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

View File

@ -0,0 +1,202 @@
using System.Collections;
using UnityEngine;
public class VanishingPlatform : MonoBehaviour
{
[Header("Настройки исчезновения платформы")]
[SerializeField] private float vanishDuration = 0.5f;
[SerializeField] private float reappearDelay = 2f;
[SerializeField] private float reappearDuration = 0.5f;
private CompositeCollider2D _compositeCollider;
private Rigidbody2D _rigidbody;
private SpriteRenderer[] _spriteRenderers;
private Coroutine _vanishCoroutine;
private Coroutine _reappearCoroutine;
private Color[] _initialColors;
private void Awake()
{
_compositeCollider = GetComponent<CompositeCollider2D>();
_rigidbody = GetComponent<Rigidbody2D>();
_spriteRenderers = GetComponentsInChildren<SpriteRenderer>();
if (_compositeCollider == null)
Debug.LogWarning($"[{nameof(VanishingPlatform)}] CompositeCollider2D не найден на {gameObject.name}.");
if (_rigidbody == null)
Debug.LogWarning($"[{nameof(VanishingPlatform)}] Rigidbody2D не найден на {gameObject.name}. CompositeCollider2D требует Rigidbody2D.");
if (_spriteRenderers == null || _spriteRenderers.Length == 0)
Debug.LogWarning($"[{nameof(VanishingPlatform)}] SpriteRenderer'ы не найдены на {gameObject.name} и его дочерних объектах.");
}
private void OnCollisionEnter2D(Collision2D other)
{
if (!other.gameObject.CompareTag("Player")) return;
// Если в процессе была корутина Reappear — её отменяем
if (_reappearCoroutine != null)
{
StopCoroutine(_reappearCoroutine);
_reappearCoroutine = null;
}
if (_vanishCoroutine != null)
StopCoroutine(_vanishCoroutine);
_vanishCoroutine = StartCoroutine(Vanish());
}
private void OnCollisionExit2D(Collision2D other)
{
if (!other.gameObject.CompareTag("Player")) return;
// Если в процессе была корутина Vanish — её отменяем
if (_vanishCoroutine != null)
{
StopCoroutine(_vanishCoroutine);
_vanishCoroutine = null;
}
if (_reappearCoroutine != null)
StopCoroutine(_reappearCoroutine);
_reappearCoroutine = StartCoroutine(Reappear());
}
private IEnumerator Vanish()
{
if (_spriteRenderers == null || _spriteRenderers.Length == 0)
yield break; // нечего анимировать
// Сохраняем начальные цвета для всех рендереров (если ещё не сохранены)
if (_initialColors == null || _initialColors.Length != _spriteRenderers.Length)
{
_initialColors = new Color[_spriteRenderers.Length];
for (int i = 0; i < _spriteRenderers.Length; i++)
_initialColors[i] = _spriteRenderers[i].color;
}
// Запоминаем текущую альфу для каждого рендерера
float[] startAlphas = new float[_spriteRenderers.Length];
for (int i = 0; i < _spriteRenderers.Length; i++)
startAlphas[i] = _spriteRenderers[i].color.a;
float elapsed = 0f;
while (elapsed < vanishDuration)
{
elapsed += Time.deltaTime;
float t = vanishDuration > 0f ? Mathf.Clamp01(elapsed / vanishDuration) : 1f;
for (int i = 0; i < _spriteRenderers.Length; i++)
{
Color c = _initialColors[i];
float alpha = Mathf.Lerp(startAlphas[i], 0f, t);
_spriteRenderers[i].color = new Color(c.r, c.g, c.b, alpha);
}
yield return null;
}
// Принудительно установить 0 альфу
for (int i = 0; i < _spriteRenderers.Length; i++)
{
Color c = _initialColors[i];
_spriteRenderers[i].color = new Color(c.r, c.g, c.b, 0f);
}
// Отключаем физику через Rigidbody2D.simulated, что отключает и CompositeCollider2D
if (_rigidbody)
_rigidbody.simulated = false;
_vanishCoroutine = null;
}
private IEnumerator Reappear()
{
if (_spriteRenderers == null || _spriteRenderers.Length == 0)
yield break; // нечего анимировать
// Убедимся, что у нас есть базовые цвета — если нет, прочитаем текущие
if (_initialColors == null || _initialColors.Length != _spriteRenderers.Length)
{
_initialColors = new Color[_spriteRenderers.Length];
for (int i = 0; i < _spriteRenderers.Length; i++)
_initialColors[i] = _spriteRenderers[i].color;
}
// Задержка перед появлением (как в предыдущей логике)
yield return new WaitForSeconds(reappearDelay);
// Запоминаем текущую альфу для каждого рендерера
float[] startAlphas = new float[_spriteRenderers.Length];
for (int i = 0; i < _spriteRenderers.Length; i++)
startAlphas[i] = _spriteRenderers[i].color.a;
float elapsed = 0f;
while (elapsed < reappearDuration)
{
elapsed += Time.deltaTime;
float t = reappearDuration > 0f ? Mathf.Clamp01(elapsed / reappearDuration) : 1f;
for (int i = 0; i < _spriteRenderers.Length; i++)
{
Color c = _initialColors[i];
float alpha = Mathf.Lerp(startAlphas[i], 1f, t);
_spriteRenderers[i].color = new Color(c.r, c.g, c.b, alpha);
}
yield return null;
}
// Принудительно установить полную непрозрачность
for (int i = 0; i < _spriteRenderers.Length; i++)
{
Color c = _initialColors[i];
_spriteRenderers[i].color = new Color(c.r, c.g, c.b, 1f);
}
// Включаем физику обратно
if (_rigidbody)
_rigidbody.simulated = true;
_reappearCoroutine = null;
}
/// <summary>
/// Внешний метод для немедленного запуска анимации исчезновения (например, вызов из других скриптов).
/// </summary>
public void TriggerVanish()
{
if (_reappearCoroutine != null)
{
StopCoroutine(_reappearCoroutine);
_reappearCoroutine = null;
}
if (_vanishCoroutine != null)
StopCoroutine(_vanishCoroutine);
_vanishCoroutine = StartCoroutine(Vanish());
}
/// <summary>
/// Внешний метод для немедленного запуска анимации появления (например, вызов из других скриптов).
/// </summary>
public void TriggerReappear()
{
if (_vanishCoroutine != null)
{
StopCoroutine(_vanishCoroutine);
_vanishCoroutine = null;
}
if (_reappearCoroutine != null)
StopCoroutine(_reappearCoroutine);
_reappearCoroutine = StartCoroutine(Reappear());
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4edf3d1d9d2a8584ba8c45271c11148d

View File

@ -1,4 +1,5 @@
using System;
using RR.Common.Controls;
using RR.Game2D.Core.Managers;
using UnityEngine;
using UnityEngine.InputSystem;

View File

@ -1,4 +1,5 @@
using RR.Common.Behaviour.ScriptableObjects;
using RR.Common.Controls;
using RR.Game2D.Core.Managers;
using UnityEngine;
using UnityEngine.Events;

View File

@ -1,89 +1,92 @@
using System;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using RR.Common.Controls;
using RR.Common.Managers;
using UnityEngine;
using RR.Common.Utility;
using RR.Game2D.Core.Managers;
public class PrizeSystemManager : MonoBehaviour
namespace RR.Common.Managers
{
public static PrizeSystemManager Instance { get; private set; }
[Header("Настройки выдачи призов")]
[SerializeField] private bool givePrizes = true;
[SerializeField] private bool enableHopper = false;
[Header("Настройки генерации QR-кодов")]
[SerializeField] private List<QRCodeInfo> qrCodes = new();
private readonly List<Texture2D> generatedQRCodes = new();
[SerializeField] private string _phoneNumberMask = "+7XXXXXXXXXX";
// [SerializeField] private string telegramLinkPrefix = "https://t.me/";
// [SerializeField] private string whatsappLinkPrefix = "https://wa.me/";
private string _phoneNumber = string.Empty;
public void Initialize()
public class PrizeSystemManager : MonoBehaviour
{
if (Instance != null && Instance != this)
public static PrizeSystemManager Instance { get; private set; }
[Header("Настройки выдачи призов")]
[SerializeField] private bool givePrizes = true;
[SerializeField] private bool enableHopper = false;
[Header("Настройки генерации QR-кодов")]
[SerializeField] private List<QRCodeInfo> qrCodes = new();
private readonly List<Texture2D> generatedQRCodes = new();
[SerializeField] private string _phoneNumberMask = "+7XXXXXXXXXX";
// [SerializeField] private string telegramLinkPrefix = "https://t.me/";
// [SerializeField] private string whatsappLinkPrefix = "https://wa.me/";
private string _phoneNumber = string.Empty;
public void Initialize()
{
Destroy(gameObject);
return;
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
Debug.Log(givePrizes
? "Prize System Initialized: Prizes will be given."
: "Prize System Initialized: Prizes will NOT be given.");
Debug.Log(enableHopper ? "Hopper is enabled for prize dispensing." : "Hopper is disabled.");
_phoneNumber = SettingsManager.Instance.GetCurrentGameSettings().phoneNumber;
_phoneNumber = PhoneNumberUtils.ValidatePhoneNumber(_phoneNumber);
_phoneNumber = PhoneNumberUtils.ApplyPhoneMask(_phoneNumber, _phoneNumberMask);
GenerateQRCodes();
}
Instance = this;
DontDestroyOnLoad(gameObject);
Debug.Log(givePrizes
? "Prize System Initialized: Prizes will be given."
: "Prize System Initialized: Prizes will NOT be given.");
Debug.Log(enableHopper ? "Hopper is enabled for prize dispensing." : "Hopper is disabled.");
_phoneNumber = SettingsManager.Instance.GetCurrentGameSettings().phoneNumber;
_phoneNumber = PhoneNumberUtils.ValidatePhoneNumber(_phoneNumber);
_phoneNumber = PhoneNumberUtils.ApplyPhoneMask(_phoneNumber, _phoneNumberMask);
// Если не инициализируется в GameManager, инициализировать тут
// private void Awake()
// {
// Initialize();
// }
GenerateQRCodes();
}
// Если не инициализируется в GameManager, инициализировать тут
// private void Awake()
// {
// Initialize();
// }
public void GivePrize()
{
if (enableHopper)
PrizeSystem.GivePrize();
else
Debug.Log("Hopper is disabled. Prize dispensing skipped.");
}
public Texture2D[] GetQRCodes()
{
return generatedQRCodes.ToArray();
}
private void GenerateQRCodes()
{
// Очищаем предыдущие QR-коды
if (generatedQRCodes.Count > 0) generatedQRCodes.Clear();
foreach (QRCodeInfo data in qrCodes)
public void GivePrize()
{
if (!data.generateQRCode) continue;
string fullLink = data.GetFullLink(_phoneNumber);
Texture2D qrTexture = QRMaker.MakeTexture(fullLink,
8,
true,
QRCodeInfo.GetColor(data.darkColor),
QRCodeInfo.GetColor(data.lightColor));
generatedQRCodes.Add(qrTexture);
Debug.Log($"Generated QR Code for link: {fullLink}");
if (enableHopper)
PrizeSystem.GivePrize();
else
Debug.Log("Hopper is disabled. Prize dispensing skipped.");
}
public Texture2D[] GetQRCodes()
{
return generatedQRCodes.ToArray();
}
private void GenerateQRCodes()
{
// Очищаем предыдущие QR-коды
if (generatedQRCodes.Count > 0) generatedQRCodes.Clear();
foreach (QRCodeInfo data in qrCodes)
{
if (!data.generateQRCode) continue;
string fullLink = data.GetFullLink(_phoneNumber);
Texture2D qrTexture = QRMaker.MakeTexture(fullLink,
8,
true,
QRCodeInfo.GetColor(data.darkColor),
QRCodeInfo.GetColor(data.lightColor));
generatedQRCodes.Add(qrTexture);
Debug.Log($"Generated QR Code for link: {fullLink}");
}
}
}
}
}

View File

@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using RR.Common.Controls;
using RR.Game2D.Core.Managers;
using TMPro;
using UnityEngine;
@ -23,7 +24,7 @@ public class SettingsCombination : MonoBehaviour
public Image startsTimerImage, prizeTimerImage, numberImage, telegramImage;
void OnEnable()
private void OnEnable()
{
gm = GameManager.Instance;
RefreshUI();

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using RR.Common.Managers;
using UnityEngine;
@ -9,115 +9,117 @@ using RR.Common.UI;
using RR.Game2D.Core.Managers;
using UnityEngine.EventSystems;
public class LoseBannerController : MonoBehaviour
namespace RR.Common.UI
{
[SerializeField] private GameEndUIController bannersController;
[Header("Кнопки")]
[SerializeField] private TextMeshProUGUI loseText;
[SerializeField] private TextMeshProUGUI creditText;
[SerializeField] private Button continueButton;
[SerializeField] private TextMeshProUGUI continueButtonText;
[SerializeField] private float continueButtonCountdown = 30f;
[Header("Тексты")]
[SerializeField] private string creditTextFormat = "У вас осталось: {0}";
private PaymentManager _paymentManager;
private Coroutine _continueCountdownCoroutine;
public event Action ContinueClicked;
private void OnEnable()
public class LoseBannerController : MonoBehaviour
{
bannersController.LoseBannerShown += OnLoseBannerShown;
_paymentManager = GameManager.Instance.Payment;
if (_paymentManager != null)
[SerializeField] private GameEndUIController bannersController;
[Header("Кнопки")]
[SerializeField] private TextMeshProUGUI loseText;
[SerializeField] private TextMeshProUGUI creditText;
[SerializeField] private Button continueButton;
[SerializeField] private TextMeshProUGUI continueButtonText;
[SerializeField] private float continueButtonCountdown = 30f;
[Header("Тексты")]
[SerializeField] private string creditTextFormat = "У вас осталось: {0}";
private PaymentManager _paymentManager;
private Coroutine _continueCountdownCoroutine;
public event Action ContinueClicked;
private void OnEnable()
{
int credits = _paymentManager.CreditsCount;
creditText.text = string.Format(creditTextFormat, credits);
_paymentManager.OnCreditAdded.AddListener(UnlockContinueButton);
}
}
bannersController.LoseBannerShown += OnLoseBannerShown;
private void OnDisable()
{
bannersController.LoseBannerShown -= OnLoseBannerShown;
// Остановим корутину, если объект деактивируется
if (_continueCountdownCoroutine != null)
{
StopCoroutine(_continueCountdownCoroutine);
_continueCountdownCoroutine = null;
}
if (_paymentManager != null)
{
_paymentManager.OnCreditAdded.RemoveListener(UnlockContinueButton);
}
}
_paymentManager = GameManager.Instance.Payment;
private void Awake()
{
if (continueButton) continueButton.onClick.AddListener(OnContinueClicked);
// Инициализируем видимость/интерактивность кнопок в безопасное состояние
if (continueButton) continueButton.gameObject.SetActive(false);
if (continueButton && continueButtonText) continueButtonText.text = string.Empty;
}
private void OnLoseBannerShown(float gameTime)
{
// EventSystem.current.firstSelectedGameObject = continueButton.gameObject;
EventSystem.current.SetSelectedGameObject(continueButton.gameObject);
loseText.text = $"Время игры: {GameEndUIController.FormatTime(gameTime)}";
creditText.text = string.Format(creditTextFormat, _paymentManager.CreditsCount);
if (continueButtonCountdown <= 0f)
{
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
if (_paymentManager != null)
{
int credits = _paymentManager.CreditsCount;
creditText.text = string.Format(creditTextFormat, credits);
_paymentManager.OnCreditAdded.AddListener(UnlockContinueButton);
}
}
if (continueButton)
private void OnDisable()
{
continueButton.gameObject.SetActive(true);
continueButton.interactable = _paymentManager.CreditsCount > 0;
bannersController.LoseBannerShown -= OnLoseBannerShown;
// Остановим корутину, если объект деактивируется
if (_continueCountdownCoroutine != null)
{
StopCoroutine(_continueCountdownCoroutine);
_continueCountdownCoroutine = null;
}
if (_paymentManager != null)
{
_paymentManager.OnCreditAdded.RemoveListener(UnlockContinueButton);
}
}
// Остановим предыдущую корутину, если есть
if (_continueCountdownCoroutine != null)
private void Awake()
{
StopCoroutine(_continueCountdownCoroutine);
_continueCountdownCoroutine = null;
if (continueButton) continueButton.onClick.AddListener(OnContinueClicked);
// Инициализируем видимость/интерактивность кнопок в безопасное состояние
if (continueButton) continueButton.gameObject.SetActive(false);
if (continueButton && continueButtonText) continueButtonText.text = string.Empty;
}
_continueCountdownCoroutine = StartCoroutine(ContinueCountdownCoroutine(continueButtonCountdown));
}
private void OnLoseBannerShown(float gameTime)
{
// EventSystem.current.firstSelectedGameObject = continueButton.gameObject;
EventSystem.current.SetSelectedGameObject(continueButton.gameObject);
private IEnumerator ContinueCountdownCoroutine(float duration)
{
float elapsed = 0f;
// Обновляем сразу первоначальный текст
int initialSeconds = Mathf.RoundToInt(duration);
UpdateContinueText(initialSeconds);
loseText.text = $"Время игры: {GameEndUIController.FormatTime(gameTime)}";
creditText.text = string.Format(creditTextFormat, _paymentManager.CreditsCount);
if (continueButtonCountdown <= 0f)
{
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
}
if (continueButton)
{
continueButton.gameObject.SetActive(true);
continueButton.interactable = _paymentManager.CreditsCount > 0;
}
// Остановим предыдущую корутину, если есть
if (_continueCountdownCoroutine != null)
{
StopCoroutine(_continueCountdownCoroutine);
_continueCountdownCoroutine = null;
}
_continueCountdownCoroutine = StartCoroutine(ContinueCountdownCoroutine(continueButtonCountdown));
}
private IEnumerator ContinueCountdownCoroutine(float duration)
{
float elapsed = 0f;
// Обновляем сразу первоначальный текст
int initialSeconds = Mathf.RoundToInt(duration);
UpdateContinueText(initialSeconds);
while (elapsed < duration)
{
// Using yield return null and manual timing with unscaled time for more reliable updates
float startTime = Time.unscaledTime;
yield return null;
// Wait for approximately 1 second using unscaled time
while (Time.unscaledTime - startTime < 1.0f)
{
yield return null;
}
elapsed += 1f;
int remaining = Mathf.Max(0, Mathf.RoundToInt(duration - elapsed));
UpdateContinueText(remaining);
@ -130,43 +132,44 @@ public class LoseBannerController : MonoBehaviour
GameManager.Instance.LoadScene("MainMenu", false);
}
_continueCountdownCoroutine = null;
}
private void UpdateContinueText(int secondsRemaining)
{
if (continueButtonText)
continueButtonText.text = $"Продолжить? ({secondsRemaining})";
}
private void UnlockContinueButton(int newCredits)
{
if (_paymentManager.IsFreePlayEnabled() || SettingsManager.Instance.GetCurrentGameSettings().isFreeGame)
{
if (continueButton)
{
continueButton.interactable = true;
}
return;
}
if (continueButton)
{
continueButton.interactable = _paymentManager.CreditsCount > 0;
}
}
private void OnContinueClicked()
{
// При клике на "Продолжить" — остановим таймер и продолжим игру
if (_continueCountdownCoroutine != null)
{
StopCoroutine(_continueCountdownCoroutine);
_continueCountdownCoroutine = null;
}
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.ContinueGame();
private void UpdateContinueText(int secondsRemaining)
{
if (continueButtonText)
continueButtonText.text = $"Продолжить? ({secondsRemaining})";
}
private void UnlockContinueButton(int newCredits)
{
if (_paymentManager.IsFreePlayEnabled() || SettingsManager.Instance.GetCurrentGameSettings().isFreeGame)
{
if (continueButton)
{
continueButton.interactable = true;
}
return;
}
if (continueButton)
{
continueButton.interactable = _paymentManager.CreditsCount > 0;
}
}
private void OnContinueClicked()
{
// При клике на "Продолжить" — остановим таймер и продолжим игру
if (_continueCountdownCoroutine != null)
{
StopCoroutine(_continueCountdownCoroutine);
_continueCountdownCoroutine = null;
}
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.ContinueGame();
}
}
}
}

View File

@ -1,56 +1,60 @@
using RR.Common.Managers;
using RR.Game2D.Core.Managers;
using UnityEngine;
using UnityEngine.UI;
public class QRCodeUIController : MonoBehaviour
namespace RR.Common.UI
{
[SerializeField] private Transform content;
private PrizeSystemManager _prizeSystemManager;
private GameObject[] _qrCodes;
private void Start()
public class QRCodeUIController : MonoBehaviour
{
_prizeSystemManager = PrizeSystemManager.Instance;
[SerializeField] private Transform content;
GameManager.Instance.GameWonEvent += AddQRCodes;
}
private PrizeSystemManager _prizeSystemManager;
private GameObject[] _qrCodes;
private void OnDestroy()
{
GameManager.Instance.GameWonEvent -= AddQRCodes;
}
private void AddQRCodes(float _)
{
if (_qrCodes != null) ClearQRCodes();
Texture2D[] qrCodesImages = _prizeSystemManager.GetQRCodes();
_qrCodes = new GameObject[qrCodesImages.Length];
for (int i = 0; i < qrCodesImages.Length; i++)
private void Start()
{
_qrCodes[i] = new GameObject("QRCode_" + i, typeof(RectTransform), typeof(Image));
_qrCodes[i].transform.SetParent(content, false);
Sprite sprite = Sprite.Create(qrCodesImages[i],
new Rect(0, 0, qrCodesImages[i].width,
qrCodesImages[i].height),
new Vector2(0.5f, 0.5f));
_qrCodes[i].GetComponent<Image>().sprite = sprite;
_prizeSystemManager = PrizeSystemManager.Instance;
GameManager.Instance.GameWonEvent += AddQRCodes;
}
private void OnDestroy()
{
GameManager.Instance.GameWonEvent -= AddQRCodes;
}
private void AddQRCodes(float _)
{
if (_qrCodes != null) ClearQRCodes();
Texture2D[] qrCodesImages = _prizeSystemManager.GetQRCodes();
_qrCodes = new GameObject[qrCodesImages.Length];
for (int i = 0; i < qrCodesImages.Length; i++)
{
_qrCodes[i] = new GameObject("QRCode_" + i, typeof(RectTransform), typeof(Image));
_qrCodes[i].transform.SetParent(content, false);
Sprite sprite = Sprite.Create(qrCodesImages[i],
new Rect(0, 0, qrCodesImages[i].width,
qrCodesImages[i].height),
new Vector2(0.5f, 0.5f));
_qrCodes[i].GetComponent<Image>().sprite = sprite;
}
}
private void ClearQRCodes()
{
if (_qrCodes == null) return;
foreach (GameObject qrCode in _qrCodes)
{
Destroy(qrCode);
}
_qrCodes = null;
}
}
private void ClearQRCodes()
{
if (_qrCodes == null) return;
foreach (GameObject qrCode in _qrCodes)
{
Destroy(qrCode);
}
_qrCodes = null;
}
}
}

View File

@ -1,78 +1,81 @@
using System.Linq;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
public class AutoCursorHider : MonoBehaviour
namespace RR.Common.Utility
{
[Header("Поведение")]
[Tooltip("Сколько секунд после движения мыши держать курсор видимым")]
public float mouseVisibleHold = 1.0f;
[Tooltip("Блокировать ли курсор, когда он скрыт (Locked)") ]
public bool lockWhenHidden = false;
[Tooltip("Начинать со скрытого курсора")]
public bool startHidden = true;
private float _lastMouseMoveTime = -999f;
private bool _visible;
private void Awake()
public class AutoCursorHider : MonoBehaviour
{
SetCursorVisible(!startHidden);
[Header("Поведение")]
[Tooltip("Сколько секунд после движения мыши держать курсор видимым")]
public float mouseVisibleHold = 1.0f;
[Tooltip("Блокировать ли курсор, когда он скрыт (Locked)")]
public bool lockWhenHidden = false;
[Tooltip("Начинать со скрытого курсора")]
public bool startHidden = true;
private float _lastMouseMoveTime = -999f;
private bool _visible;
private void Awake()
{
SetCursorVisible(!startHidden);
}
private void Update()
{
bool mouseMoved = WasMouseMovedOrClickedThisFrame();
if (mouseMoved)
_lastMouseMoveTime = Time.unscaledTime;
bool nonPointerInput = WasKeyboardOrGamepadThisFrame();
// Показать курсор сразу при движении мыши
if (mouseMoved && !_visible)
SetCursorVisible(true);
// Скрыть курсор при управлении клавишами/геймпадом
if (nonPointerInput && _visible)
SetCursorVisible(false);
// Если мышь не трогали какое-то время — можно снова спрятать (опционально)
if (_visible && !mouseMoved && (Time.unscaledTime - _lastMouseMoveTime) > mouseVisibleHold && nonPointerInput)
SetCursorVisible(false);
}
private void SetCursorVisible(bool show)
{
if (_visible == show) return;
_visible = show;
Cursor.visible = show;
Cursor.lockState = show ? CursorLockMode.None
: (lockWhenHidden ? CursorLockMode.Locked : CursorLockMode.None);
// Совет: для UI-меню лучше Confined вместо Locked, если хочется скрыть не блокируя:
// Cursor.lockState = show ? CursorLockMode.None : CursorLockMode.Confined;
}
private static bool WasMouseMovedOrClickedThisFrame()
{
if (Mouse.current == null) return false;
if (Mouse.current.delta.ReadValue().sqrMagnitude > 0) return true;
if (Mouse.current.scroll.ReadValue().y != 0) return true;
return Mouse.current.leftButton.wasPressedThisFrame ||
Mouse.current.rightButton.wasPressedThisFrame ||
Mouse.current.middleButton.wasPressedThisFrame;
}
private static bool WasKeyboardOrGamepadThisFrame()
{
bool kb = Keyboard.current != null && Keyboard.current.anyKey.isPressed;
bool gp = Gamepad.current != null && Gamepad.current.allControls.Any(c => c is ButtonControl { wasPressedThisFrame: true });
return kb || gp;
}
// Публичные методы, если нужно управлять вручную
public void ForceShowCursor() => SetCursorVisible(true);
public void ForceHideCursor() => SetCursorVisible(false);
}
private void Update()
{
bool mouseMoved = WasMouseMovedOrClickedThisFrame();
if (mouseMoved)
_lastMouseMoveTime = Time.unscaledTime;
bool nonPointerInput = WasKeyboardOrGamepadThisFrame();
// Показать курсор сразу при движении мыши
if (mouseMoved && !_visible)
SetCursorVisible(true);
// Скрыть курсор при управлении клавишами/геймпадом
if (nonPointerInput && _visible)
SetCursorVisible(false);
// Если мышь не трогали какое-то время — можно снова спрятать (опционально)
if (_visible && !mouseMoved && (Time.unscaledTime - _lastMouseMoveTime) > mouseVisibleHold && nonPointerInput)
SetCursorVisible(false);
}
private void SetCursorVisible(bool show)
{
if (_visible == show) return;
_visible = show;
Cursor.visible = show;
Cursor.lockState = show ? CursorLockMode.None
: (lockWhenHidden ? CursorLockMode.Locked : CursorLockMode.None);
// Совет: для UI-меню лучше Confined вместо Locked, если хочется скрыть не блокируя:
// Cursor.lockState = show ? CursorLockMode.None : CursorLockMode.Confined;
}
private static bool WasMouseMovedOrClickedThisFrame()
{
if (Mouse.current == null) return false;
if (Mouse.current.delta.ReadValue().sqrMagnitude > 0) return true;
if (Mouse.current.scroll.ReadValue().y != 0) return true;
return Mouse.current.leftButton.wasPressedThisFrame ||
Mouse.current.rightButton.wasPressedThisFrame ||
Mouse.current.middleButton.wasPressedThisFrame;
}
private static bool WasKeyboardOrGamepadThisFrame()
{
bool kb = Keyboard.current != null && Keyboard.current.anyKey.isPressed;
bool gp = Gamepad.current != null && Gamepad.current.allControls.Any(c => c is ButtonControl { wasPressedThisFrame: true });
return kb || gp;
}
// Публичные методы, если нужно управлять вручную
public void ForceShowCursor() => SetCursorVisible(true);
public void ForceHideCursor() => SetCursorVisible(false);
}
}

View File

@ -1,354 +1,357 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
public class CompositeLogger : ILogger, IDisposable
namespace RR.Common.Utility
{
public ILogHandler logHandler { get; set; }
public bool logEnabled { get; set; }
public LogType filterLogType { get; set; }
private ILogger _oldLogger;
private readonly List<ILogger> _additiveLoggers = new();
public class CompositeLogger : ILogger, IDisposable
{
public ILogHandler logHandler { get; set; }
public bool logEnabled { get; set; }
public LogType filterLogType { get; set; }
public void Initialize(ILogger additiveLogger = null)
{
_oldLogger = Debug.unityLogger;
logHandler = _oldLogger.logHandler;
logEnabled = true;
filterLogType = _oldLogger.filterLogType;
if (additiveLogger != null) _additiveLoggers.Add(additiveLogger);
}
public void AddLogger(ILogger additiveLogger)
{
if (additiveLogger == null)
throw new ArgumentNullException(nameof(additiveLogger));
_additiveLoggers.Add(additiveLogger);
}
public void AddLoggers(params ILogger[] additiveLoggers)
{
if (additiveLoggers == null)
throw new ArgumentNullException(nameof(additiveLoggers));
foreach (var logger in additiveLoggers)
private ILogger _oldLogger;
private readonly List<ILogger> _additiveLoggers = new();
public void Initialize(ILogger additiveLogger = null)
{
if (logger != null)
_additiveLoggers.Add(logger);
_oldLogger = Debug.unityLogger;
logHandler = _oldLogger.logHandler;
logEnabled = true;
filterLogType = _oldLogger.filterLogType;
if (additiveLogger != null) _additiveLoggers.Add(additiveLogger);
}
}
public void RemoveLogger(ILogger additiveLogger)
{
_additiveLoggers.Remove(additiveLogger);
}
public void RemoveLoggers(params ILogger[] additiveLoggers)
{
_additiveLoggers.RemoveAll(additiveLoggers.Contains);
}
public bool IsLogTypeAllowed(LogType logType)
{
return _oldLogger.IsLogTypeAllowed(logType) || _additiveLoggers.Any(logger => logger.IsLogTypeAllowed(logType));
}
#region Ilogger Implementation
public void LogFormat(LogType logType, string format, params object[] args)
{
_oldLogger.LogFormat(logType, format, args);
foreach (ILogger additiveLogger in _additiveLoggers)
public void AddLogger(ILogger additiveLogger)
{
try
if (additiveLogger == null)
throw new ArgumentNullException(nameof(additiveLogger));
_additiveLoggers.Add(additiveLogger);
}
public void AddLoggers(params ILogger[] additiveLoggers)
{
if (additiveLoggers == null)
throw new ArgumentNullException(nameof(additiveLoggers));
foreach (var logger in additiveLoggers)
{
additiveLogger.LogFormat(logType, format, args);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
if (logger != null)
_additiveLoggers.Add(logger);
}
}
}
public void LogFormat(LogType logType, Object context, string format, params object[] args)
{
_oldLogger.LogFormat(logType, context, format, args);
foreach (ILogger additiveLogger in _additiveLoggers)
public void RemoveLogger(ILogger additiveLogger)
{
try
{
additiveLogger.LogFormat(logType, context, format, args);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
_additiveLoggers.Remove(additiveLogger);
}
}
public void LogException(Exception exception)
{
_oldLogger.LogException(exception);
foreach (ILogger additiveLogger in _additiveLoggers)
public void RemoveLoggers(params ILogger[] additiveLoggers)
{
try
{
additiveLogger.LogException(exception);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
_additiveLoggers.RemoveAll(additiveLoggers.Contains);
}
}
public void LogException(Exception exception, Object context)
{
_oldLogger.LogException(exception, context);
foreach (ILogger additiveLogger in _additiveLoggers)
public bool IsLogTypeAllowed(LogType logType)
{
try
{
additiveLogger.LogException(exception, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
return _oldLogger.IsLogTypeAllowed(logType) || _additiveLoggers.Any(logger => logger.IsLogTypeAllowed(logType));
}
}
public void Log(object message)
{
_oldLogger.Log(message);
foreach (ILogger additiveLogger in _additiveLoggers)
#region Ilogger Implementation
public void LogFormat(LogType logType, string format, params object[] args)
{
try
{
additiveLogger.Log(message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, object message)
{
_oldLogger.Log(logType, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(logType, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, object message, GameObject gameObject)
{
_oldLogger.Log(logType, message, gameObject);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(logType, message, gameObject);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, object message, Object context)
{
_oldLogger.Log(logType, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(logType, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, string tag, object message, Object context)
{
_oldLogger.Log(logType, tag, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(logType, tag, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, string tag, object message)
{
_oldLogger.Log(tag, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(tag, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(string tag, object message)
{
_oldLogger.Log(tag, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(tag, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(string tag, object message, Object context)
{
_oldLogger.Log(tag, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(tag, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogWarning(string tag, object message)
{
_oldLogger.LogWarning(tag, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogWarning(tag, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogWarning(string tag, object message, Object context)
{
_oldLogger.LogWarning(tag, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogWarning(tag, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogError(string tag, object message)
{
_oldLogger.LogError(tag, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogError(tag, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogError(string tag, object message, Object context)
{
_oldLogger.LogError(tag, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogError(tag, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
#endregion
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposing) return;
lock (_additiveLoggers)
{
foreach (ILogger additiveLogger in _additiveLoggers.ToArray())
_oldLogger.LogFormat(logType, format, args);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
if (additiveLogger is IDisposable disposableLogger)
disposableLogger.Dispose();
additiveLogger.LogFormat(logType, format, args);
}
catch (Exception ex)
{
try { _oldLogger?.LogException(ex); } catch { /* ignore */ }
_oldLogger.LogException(ex);
}
}
_additiveLoggers.Clear();
}
public void LogFormat(LogType logType, Object context, string format, params object[] args)
{
_oldLogger.LogFormat(logType, context, format, args);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogFormat(logType, context, format, args);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogException(Exception exception)
{
_oldLogger.LogException(exception);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogException(exception);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogException(Exception exception, Object context)
{
_oldLogger.LogException(exception, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogException(exception, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(object message)
{
_oldLogger.Log(message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, object message)
{
_oldLogger.Log(logType, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(logType, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, object message, GameObject gameObject)
{
_oldLogger.Log(logType, message, gameObject);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(logType, message, gameObject);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, object message, Object context)
{
_oldLogger.Log(logType, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(logType, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, string tag, object message, Object context)
{
_oldLogger.Log(logType, tag, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(logType, tag, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(LogType logType, string tag, object message)
{
_oldLogger.Log(tag, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(tag, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(string tag, object message)
{
_oldLogger.Log(tag, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(tag, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void Log(string tag, object message, Object context)
{
_oldLogger.Log(tag, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.Log(tag, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogWarning(string tag, object message)
{
_oldLogger.LogWarning(tag, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogWarning(tag, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogWarning(string tag, object message, Object context)
{
_oldLogger.LogWarning(tag, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogWarning(tag, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogError(string tag, object message)
{
_oldLogger.LogError(tag, message);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogError(tag, message);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
public void LogError(string tag, object message, Object context)
{
_oldLogger.LogError(tag, message, context);
foreach (ILogger additiveLogger in _additiveLoggers)
{
try
{
additiveLogger.LogError(tag, message, context);
}
catch (Exception ex)
{
_oldLogger.LogException(ex);
}
}
}
#endregion
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposing) return;
lock (_additiveLoggers)
{
foreach (ILogger additiveLogger in _additiveLoggers.ToArray())
{
try
{
if (additiveLogger is IDisposable disposableLogger)
disposableLogger.Dispose();
}
catch (Exception ex)
{
try { _oldLogger?.LogException(ex); } catch { /* ignore */ }
}
}
_additiveLoggers.Clear();
}
}
~CompositeLogger()
{
Dispose();
}
}
~CompositeLogger()
{
Dispose();
}
}
}

View File

@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
#if UNITY_EDITOR
@ -6,19 +6,22 @@ using UnityEditor;
#endif
public class ReadOnlyInInspectorAttribute : PropertyAttribute {}
namespace RR.Common.Utilities
{
public class ReadOnlyInInspectorAttribute : PropertyAttribute { }
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ReadOnlyInInspectorAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> EditorGUI.GetPropertyHeight(property, label, true);
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
[CustomPropertyDrawer(typeof(ReadOnlyInInspectorAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
GUI.enabled = false; EditorGUI.PropertyField(position, property, label, true); GUI.enabled = true;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
=> EditorGUI.GetPropertyHeight(property, label, true);
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
GUI.enabled = false; EditorGUI.PropertyField(position, property, label, true); GUI.enabled = true;
}
}
}
#endif
#endif
}

View File

@ -697,7 +697,7 @@ PlayerSettings:
framebufferDepthMemorylessMode: 0
qualitySettingsNames: []
projectName: Mario-like platformer
organizationId:
organizationId: dis_bro
cloudEnabled: 0
legacyClampBlendShapeWeights: 0
hmiLoadingImage: {fileID: 0}