From c2b26c0491886b99f422e830cd9ec1637a4ddc2e Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Sun, 1 Mar 2026 09:36:29 +0800 Subject: first --- Assets/Scripts/Camera.meta | 8 ++ Assets/Scripts/Camera/BindCameraHeightToShader.cs | 53 +++++++++ .../Camera/BindCameraHeightToShader.cs.meta | 2 + Assets/Scripts/Camera/CameraFollow.cs | 121 +++++++++++++++++++++ Assets/Scripts/Camera/CameraFollow.cs.meta | 2 + Assets/Scripts/Camera/WCGame.Camera.asmdef | 3 + Assets/Scripts/Camera/WCGame.Camera.asmdef.meta | 7 ++ Assets/Scripts/Core.meta | 8 ++ Assets/Scripts/Core/LoadGroup.cs | 12 ++ Assets/Scripts/Core/LoadGroup.cs.meta | 2 + Assets/Scripts/Core/Loader.cs | 73 +++++++++++++ Assets/Scripts/Core/Loader.cs.meta | 2 + Assets/Scripts/Core/WCGame.Core.asmdef | 14 +++ Assets/Scripts/Core/WCGame.Core.asmdef.meta | 7 ++ Assets/Scripts/GamePlay.meta | 8 ++ Assets/Scripts/GamePlay/Player.meta | 8 ++ Assets/Scripts/GamePlay/Player/ChairMovement.cs | 102 +++++++++++++++++ .../Scripts/GamePlay/Player/ChairMovement.cs.meta | 2 + .../Scripts/GamePlay/Player/WheelMeshPosition.cs | 17 +++ .../GamePlay/Player/WheelMeshPosition.cs.meta | 2 + Assets/Scripts/GamePlay/Player/WheelTransforms.cs | 48 ++++++++ .../GamePlay/Player/WheelTransforms.cs.meta | 2 + Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef | 16 +++ .../Scripts/GamePlay/WCGame.GamePlay.asmdef.meta | 7 ++ 24 files changed, 526 insertions(+) create mode 100644 Assets/Scripts/Camera.meta create mode 100644 Assets/Scripts/Camera/BindCameraHeightToShader.cs create mode 100644 Assets/Scripts/Camera/BindCameraHeightToShader.cs.meta create mode 100644 Assets/Scripts/Camera/CameraFollow.cs create mode 100644 Assets/Scripts/Camera/CameraFollow.cs.meta create mode 100644 Assets/Scripts/Camera/WCGame.Camera.asmdef create mode 100644 Assets/Scripts/Camera/WCGame.Camera.asmdef.meta create mode 100644 Assets/Scripts/Core.meta create mode 100644 Assets/Scripts/Core/LoadGroup.cs create mode 100644 Assets/Scripts/Core/LoadGroup.cs.meta create mode 100644 Assets/Scripts/Core/Loader.cs create mode 100644 Assets/Scripts/Core/Loader.cs.meta create mode 100644 Assets/Scripts/Core/WCGame.Core.asmdef create mode 100644 Assets/Scripts/Core/WCGame.Core.asmdef.meta create mode 100644 Assets/Scripts/GamePlay.meta create mode 100644 Assets/Scripts/GamePlay/Player.meta create mode 100644 Assets/Scripts/GamePlay/Player/ChairMovement.cs create mode 100644 Assets/Scripts/GamePlay/Player/ChairMovement.cs.meta create mode 100644 Assets/Scripts/GamePlay/Player/WheelMeshPosition.cs create mode 100644 Assets/Scripts/GamePlay/Player/WheelMeshPosition.cs.meta create mode 100644 Assets/Scripts/GamePlay/Player/WheelTransforms.cs create mode 100644 Assets/Scripts/GamePlay/Player/WheelTransforms.cs.meta create mode 100644 Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef create mode 100644 Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef.meta (limited to 'Assets/Scripts') diff --git a/Assets/Scripts/Camera.meta b/Assets/Scripts/Camera.meta new file mode 100644 index 0000000..a55d430 --- /dev/null +++ b/Assets/Scripts/Camera.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7c21c0fcc6d0f0b449732e0f5acf4d2c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Camera/BindCameraHeightToShader.cs b/Assets/Scripts/Camera/BindCameraHeightToShader.cs new file mode 100644 index 0000000..4381e2c --- /dev/null +++ b/Assets/Scripts/Camera/BindCameraHeightToShader.cs @@ -0,0 +1,53 @@ +using UnityEngine; + +namespace Camera +{ + public class BindCameraHeightToShader : MonoBehaviour + { + public float layerHeight = 5; + private static readonly int PlayerHeight = Shader.PropertyToID("_PlayerHeight"); + private static readonly int LayerHeight = Shader.PropertyToID("_LayerHeight"); + + private float scaleAverage + { + get + { + var t = transform; + var scale = t.localScale; + var average = (scale.x + scale.y + scale.z) / 3; + return average; + } + } + + private void FixedUpdate() + { + Shader.SetGlobalFloat(PlayerHeight, transform.position.y); + Shader.SetGlobalFloat(LayerHeight, layerHeight); + } + + private void OnDrawGizmos() + { + var t = transform; + var average = this.scaleAverage; + + Gizmos.color = Color.yellow; + Gizmos.DrawWireCube( + t.position, + new Vector3(2f, 0.2f, 2f) * average + ); + } + + private void OnDrawGizmosSelected() + { + var t = transform; + var average = this.scaleAverage; + + Gizmos.color = Color.green; + Gizmos.DrawWireCube( + t.position + Vector3.up * (layerHeight / 2), + (new Vector3(2.2f, 0f, 2.2f) * average) + + Vector3.up * layerHeight + ); + } + } +} diff --git a/Assets/Scripts/Camera/BindCameraHeightToShader.cs.meta b/Assets/Scripts/Camera/BindCameraHeightToShader.cs.meta new file mode 100644 index 0000000..f9942ed --- /dev/null +++ b/Assets/Scripts/Camera/BindCameraHeightToShader.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5aa8cf760c4b2a54a994e58169a6ad80 \ No newline at end of file diff --git a/Assets/Scripts/Camera/CameraFollow.cs b/Assets/Scripts/Camera/CameraFollow.cs new file mode 100644 index 0000000..47b96f4 --- /dev/null +++ b/Assets/Scripts/Camera/CameraFollow.cs @@ -0,0 +1,121 @@ +using System; +using JetBrains.Annotations; +using UnityEngine; + +namespace Camera +{ + public class CameraFollow : MonoBehaviour + { + [CanBeNull] public static CameraFollow Current; + private Transform _followTarget; + + private float _defaultYaw; + private bool _enableMove; + private float _height; + + [CanBeNull] + public Transform followTarget + { + get => _followTarget; + set { + if (_followTarget == value) return; + + _followTarget = value; + + if (_followTarget != null) + OnFollowTargetChanged(value); + else + OnFollowTargetLost(); + } + } + + private void Awake() + { + _defaultYaw = rotationYaw.eulerAngles.y; + + if (Current != null) + Debug.LogWarning("Duplicate CameraFollow Found!"); + Current = this; + followTarget = defaultFollowTarget; + } + + [Header("Parameters")] + [SerializeField] private bool enableYaw; + [SerializeField] private Transform defaultFollowTarget; + [SerializeField] [Range(0.015f, 1f)] private float yawRotationLerp; + [SerializeField] [Range(0.015f, 1f)] private float positionLerp; + [SerializeField] [Range(0.015f, 1f)] private float heightLerp; + [SerializeField] private LayerMask heightCheckLayerMask; + + [Header("Bindings")] + [SerializeField] private Transform rotationYaw; + [SerializeField] private Transform rotationPitch; + [SerializeField] private Transform positionCamera; + [SerializeField] private Transform positionHeight; + + private void OnFollowTargetChanged([CanBeNull] Transform target) + { + Debug.Log($"Follow target: {target?.name}"); + _enableMove = target != null; + } + + private void OnFollowTargetLost() + { + Debug.Log("Follow target lost"); + _enableMove = false; + } + + private void FixedUpdate() + { + FixedUpdateHeight(); + + if (_enableMove) + { + try + { + FixedUpdateMoveCamera(); + } + catch (NullReferenceException) + { + OnFollowTargetLost(); + } + } + } + + private void FixedUpdateMoveCamera() + { + var transformYaw = _followTarget.rotation.eulerAngles.y; + var transformPosition = _followTarget.position; + + var targetYaw = transformYaw; + var targetYawRotation = Quaternion.Euler(0, enableYaw ? targetYaw : _defaultYaw, 0); + var targetPosition = new Vector3(transformPosition.x, 0, transformPosition.z); + + transform.position = Vector3.Lerp(transform.position, targetPosition, positionLerp); + rotationYaw.rotation = Quaternion.Lerp(rotationYaw.rotation, targetYawRotation, yawRotationLerp); + positionHeight.position = new Vector3( + transformPosition.x, + Mathf.Lerp(positionHeight.position.y, _height, heightLerp), + transformPosition.z + ); + } + + private void FixedUpdateHeight() + { + var transformPosition = transform.position; + var start = transformPosition + Vector3.up * 100; + var end = transformPosition + Vector3.down * 100; + Physics.Linecast(start, end, out RaycastHit hit, heightCheckLayerMask); + if (hit.collider == null) + _height = transformPosition.y; + else + _height = hit.point.y; + } + + private void OnDestroy() + { + if (Current == this) + Current = null; + } + } +} diff --git a/Assets/Scripts/Camera/CameraFollow.cs.meta b/Assets/Scripts/Camera/CameraFollow.cs.meta new file mode 100644 index 0000000..35f7072 --- /dev/null +++ b/Assets/Scripts/Camera/CameraFollow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 967f7d10ac322f048890a51be69e4081 \ No newline at end of file diff --git a/Assets/Scripts/Camera/WCGame.Camera.asmdef b/Assets/Scripts/Camera/WCGame.Camera.asmdef new file mode 100644 index 0000000..a760cc0 --- /dev/null +++ b/Assets/Scripts/Camera/WCGame.Camera.asmdef @@ -0,0 +1,3 @@ +{ + "name": "WCGame.Camera" +} diff --git a/Assets/Scripts/Camera/WCGame.Camera.asmdef.meta b/Assets/Scripts/Camera/WCGame.Camera.asmdef.meta new file mode 100644 index 0000000..fd2222d --- /dev/null +++ b/Assets/Scripts/Camera/WCGame.Camera.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b84af68224800a9499b937de477023db +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core.meta b/Assets/Scripts/Core.meta new file mode 100644 index 0000000..456c237 --- /dev/null +++ b/Assets/Scripts/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7e4629895c506b1438e4c029456d1437 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Core/LoadGroup.cs b/Assets/Scripts/Core/LoadGroup.cs new file mode 100644 index 0000000..7381c5d --- /dev/null +++ b/Assets/Scripts/Core/LoadGroup.cs @@ -0,0 +1,12 @@ + +using System.Collections.Generic; +using UnityEngine; + +namespace Core +{ + [CreateAssetMenu(fileName = "LoadGroup", menuName = "WCGame/LoadGroup")] + public class LoadGroup : ScriptableObject + { + public List GameObjects; + } +} diff --git a/Assets/Scripts/Core/LoadGroup.cs.meta b/Assets/Scripts/Core/LoadGroup.cs.meta new file mode 100644 index 0000000..2e81310 --- /dev/null +++ b/Assets/Scripts/Core/LoadGroup.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c6a85e5be7398304e8084f1e1b140abf \ No newline at end of file diff --git a/Assets/Scripts/Core/Loader.cs b/Assets/Scripts/Core/Loader.cs new file mode 100644 index 0000000..23cf216 --- /dev/null +++ b/Assets/Scripts/Core/Loader.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Events; + +namespace Core +{ + public class Loader : MonoBehaviour + { + public static Loader Current; + + public List loadGroups; + public UnityEvent onLoaded; + + private bool _started; + + +#if UNITY_EDITOR + [HideInInspector] public int objectCount; + [HideInInspector] public int loadedObjectCount; + + private void OnGUI() + { + GUILayout.Label($"Loaded: {loadedObjectCount / objectCount * 100}% ({loadedObjectCount}/{objectCount})"); + } +#endif + + private void Update() + { + if (!_started) + _started = true; + } + + private void Awake() + { +#if UNITY_EDITOR + foreach (var loadGroup in loadGroups) + objectCount += loadGroup.GameObjects.Count; +#endif + if (Current == null) + Current = this; + + StartCoroutine(Load()); + } + + private IEnumerator Load() + { + yield return new WaitUntil(() => _started); + foreach (var loadGroup in loadGroups) + { + if (loadGroups == null) + continue; + foreach (var loadGroupGameObject in loadGroup.GameObjects) + { + Instantiate(loadGroupGameObject); +#if UNITY_EDITOR + loadedObjectCount++; +#endif + yield return null; + } + } + onLoaded.Invoke(); + Destroy(gameObject); + } + + private void OnDestroy() + { + if (Current == this) + Current = null; + } + } +} diff --git a/Assets/Scripts/Core/Loader.cs.meta b/Assets/Scripts/Core/Loader.cs.meta new file mode 100644 index 0000000..8cd6a76 --- /dev/null +++ b/Assets/Scripts/Core/Loader.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 57b520943bbc8914d80e8523d908b4c2 \ No newline at end of file diff --git a/Assets/Scripts/Core/WCGame.Core.asmdef b/Assets/Scripts/Core/WCGame.Core.asmdef new file mode 100644 index 0000000..6218367 --- /dev/null +++ b/Assets/Scripts/Core/WCGame.Core.asmdef @@ -0,0 +1,14 @@ +{ + "name": "WCGame.Core", + "rootNamespace": "", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/Core/WCGame.Core.asmdef.meta b/Assets/Scripts/Core/WCGame.Core.asmdef.meta new file mode 100644 index 0000000..234ed6b --- /dev/null +++ b/Assets/Scripts/Core/WCGame.Core.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cdb8b834ac5fd5b4f9c5c43cc62e0cfb +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/GamePlay.meta b/Assets/Scripts/GamePlay.meta new file mode 100644 index 0000000..ed34f09 --- /dev/null +++ b/Assets/Scripts/GamePlay.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 78be0ebfe38357744accc2adcc890eb0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/GamePlay/Player.meta b/Assets/Scripts/GamePlay/Player.meta new file mode 100644 index 0000000..bebca6a --- /dev/null +++ b/Assets/Scripts/GamePlay/Player.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 70966302095eeba4d87abbe830c3f877 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/GamePlay/Player/ChairMovement.cs b/Assets/Scripts/GamePlay/Player/ChairMovement.cs new file mode 100644 index 0000000..f254c97 --- /dev/null +++ b/Assets/Scripts/GamePlay/Player/ChairMovement.cs @@ -0,0 +1,102 @@ +using System; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace GamePlay.Player +{ + [RequireComponent(typeof(PlayerInput))] + [RequireComponent(typeof(Rigidbody))] + public class ChairMovement : MonoBehaviour + { + private PlayerInput _playerInput; + private Rigidbody _rigidbody; + + private float _inputHorizontal; + private float _inputVertical; + + public float maxMotorTorque = 400f; + public float maxSteerAngle = 90f; + public float brakeTorque = 1000f; + + public WheelCollider frontLeftWheel; + public WheelCollider frontRightWheel; + public WheelCollider rearLeftWheel; + public WheelCollider rearRightWheel; + + private void Awake() + { + _playerInput = GetComponent(); + _rigidbody = GetComponent(); + + _playerInput.onActionTriggered += OnActionTriggered; + } + + private void OnActionTriggered(InputAction.CallbackContext ctx) + { + if (ctx.action.name == "Move") + { + var moveInput = ctx.ReadValue(); + _inputHorizontal = moveInput.x; + _inputVertical = moveInput.y; + } + } + + private void FixedUpdate() + { + ApplySteering(); + ApplyMotor(); + ApplyBrakes(); + } + + private void ApplySteering() + { + float steer = maxSteerAngle * _inputHorizontal; + frontLeftWheel.steerAngle = steer; + frontRightWheel.steerAngle = steer; + } + + private void ApplyMotor() + { + if (_inputVertical > 0) + { + float motor = maxMotorTorque * _inputVertical; + OperateWheel(w => w.motorTorque = motor); + + // 释放刹车(因为在前进) + OperateWheel(w => w.brakeTorque = 0); + } + } + + private void ApplyBrakes() + { + if (_inputVertical < 0) + { + if (_rigidbody.linearVelocity.z > 0.1f) + { + float brake = brakeTorque * Mathf.Abs(_inputVertical); + OperateWheel(w => w.brakeTorque = brake); + OperateWheel(w => w.motorTorque = 0); + } + else + { + OperateWheel(w => w.brakeTorque = 0); + OperateWheel(w => w.motorTorque = maxMotorTorque * _inputVertical); + } + } + else if (Mathf.Abs(_inputVertical) < 0.1f) + { + float parkingBrake = 10f; + OperateWheel(w => w.brakeTorque = parkingBrake); + OperateWheel(w => w.motorTorque = 0); + } + } + + private void OperateWheel(Action o) + { + o(frontLeftWheel); + o(frontRightWheel); + o(frontLeftWheel); + o(frontRightWheel); + } + } +} diff --git a/Assets/Scripts/GamePlay/Player/ChairMovement.cs.meta b/Assets/Scripts/GamePlay/Player/ChairMovement.cs.meta new file mode 100644 index 0000000..04bb4df --- /dev/null +++ b/Assets/Scripts/GamePlay/Player/ChairMovement.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eb1f6a494562d1f4c9f1cf36acd136b2 \ No newline at end of file diff --git a/Assets/Scripts/GamePlay/Player/WheelMeshPosition.cs b/Assets/Scripts/GamePlay/Player/WheelMeshPosition.cs new file mode 100644 index 0000000..6f68a4c --- /dev/null +++ b/Assets/Scripts/GamePlay/Player/WheelMeshPosition.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +namespace GamePlay.Player +{ + public class WheelMeshPosition : MonoBehaviour + { + [Range(0, 1)] public float lerpSpeed; + public WheelCollider wheelCollider; + + private void FixedUpdate() + { + wheelCollider.GetWorldPose(out Vector3 pos, out Quaternion rot); + transform.position = Vector3.Lerp(transform.position, pos, lerpSpeed); + transform.rotation = Quaternion.Slerp(transform.rotation, rot, lerpSpeed); + } + } +} diff --git a/Assets/Scripts/GamePlay/Player/WheelMeshPosition.cs.meta b/Assets/Scripts/GamePlay/Player/WheelMeshPosition.cs.meta new file mode 100644 index 0000000..f0cb003 --- /dev/null +++ b/Assets/Scripts/GamePlay/Player/WheelMeshPosition.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c250814400f5b9f4197bd111feae1bdb \ No newline at end of file diff --git a/Assets/Scripts/GamePlay/Player/WheelTransforms.cs b/Assets/Scripts/GamePlay/Player/WheelTransforms.cs new file mode 100644 index 0000000..0834f32 --- /dev/null +++ b/Assets/Scripts/GamePlay/Player/WheelTransforms.cs @@ -0,0 +1,48 @@ +using System; +using UnityEngine; + +namespace GamePlay.Player +{ + [ExecuteAlways] + public class WheelTransforms : MonoBehaviour + { + [Header("Parameters")] + [Range(0f, 4f)] public float length; + [Range(0f, 4f)] public float width; + [Range(-5f, 5f)] public float offsetX; + [Range(-5f, 5f)] public float offsetZ; + [Range(-5f, 5f)] public float offsetY; + + [Header("Bindings")] + public Transform frontLeft; + public Transform frontRight; + public Transform rearLeft; + public Transform rearRight; + +#if UNITY_EDITOR + private void Update() + { + if (Application.isPlaying) return; + UpdateWheels(); + } +#endif + + private void FixedUpdate() + { + UpdateWheels(); + } + + private void UpdateWheels() + { + frontLeft.localPosition = GetPosition(-1, 1); + frontRight.localPosition = GetPosition(1, 1); + rearLeft.localPosition = GetPosition(-1, -1); + rearRight.localPosition = GetPosition(1, -1); + } + + private Vector3 GetPosition(float axisX, float axisZ) + { + return new Vector3(width / 2 * axisX + offsetX, offsetY, length / 2 * axisZ + offsetZ); + } + } +} diff --git a/Assets/Scripts/GamePlay/Player/WheelTransforms.cs.meta b/Assets/Scripts/GamePlay/Player/WheelTransforms.cs.meta new file mode 100644 index 0000000..ad330a6 --- /dev/null +++ b/Assets/Scripts/GamePlay/Player/WheelTransforms.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b17f16ff6b13c774ba87283567ee2169 \ No newline at end of file diff --git a/Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef b/Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef new file mode 100644 index 0000000..852576f --- /dev/null +++ b/Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef @@ -0,0 +1,16 @@ +{ + "name": "WCGame.GamePlay", + "rootNamespace": "", + "references": [ + "GUID:75469ad4d38634e559750d17036d5f7c" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef.meta b/Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef.meta new file mode 100644 index 0000000..084005e --- /dev/null +++ b/Assets/Scripts/GamePlay/WCGame.GamePlay.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0ed168a6ec579ae4c8dc6a86d00f033b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: -- cgit