From ce66d4091dd920196f483babc4a137c350e186dd Mon Sep 17 00:00:00 2001 From: PhoenixR Date: Tue, 18 Mar 2025 09:14:41 +0000 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FTG joystick --- .../Joystick/Joystick4FTG.swift | 592 ++++++++++++++++++ 1 file changed, 592 insertions(+) create mode 100644 src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick4FTG.swift diff --git a/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick4FTG.swift b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick4FTG.swift new file mode 100644 index 000000000..451e00f08 --- /dev/null +++ b/src/MeloNX/MeloNX/App/Views/Main/ControllerView/Joystick/Joystick4FTG.swift @@ -0,0 +1,592 @@ +// +// Joystick4FTG.swift +// MeloNX +// +// Created by RZH on 2025/3/7. +// + +import SwiftUI + +public struct Joystick4FTG: View { + //先声明数据 + @State var iscool: Bool? = nil + @State private var joystickPositionText = "" + @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 + var dragDiameter: CGFloat { + var selfs = CGFloat(160) + selfs *= controllerScale + if UIDevice.current.systemName.contains("iPadOS") { + return selfs * 1.2 + } + return selfs + } + public var body: some View { + VStack{ + // 新增坐标显示(在摇杆上方) + //旧的FTGJoystick: + /*FTGJoystickBuilder { relativePosition in + + let scaledX = Double(relativePosition.x ) + let scaledY = Double(relativePosition.y ) + + //print("Joystick Position: (\(scaledX), \(scaledY))") + + */ + //新版八向摇杆: + /* + EnhancedEightWayJoystick{relativePosition in + let scaledX = Double(relativePosition.x * 10) + let scaledY = Double(relativePosition.y * 10) + let formattedText = String(format: "X: %5.2f, Y: %5.2f", scaledX, scaledY) + print("Joystick Position: \(formattedText)") + // 更新显示文本 + //self.joystickPositionText = formattedText + */ + + + + /* + //新版自由摇杆: + FreeRoamJoystick { position in + let scaledX = Double(position.x * 10) + let scaledY = Double(position.y * 10) + //print("Joystick Position: \(formattedText)") + // 更新显示文本 + //self.joystickPositionText = formattedText + + */ + //九宫格摇杆: + /* + NineGridJoystick { position in + let scaledX = Double(position.x * 10) + let scaledY = Double(position.y * 10) + */ + + //新的动态九宫格摇杆 + DynamicNineGridJoystick{ position in + let scaledX = Double(position.x * 10) + let scaledY = Double(position.y * 10) + + if iscool != nil { + Ryujinx.shared.virtualController.thumbstickMoved(.right, x: scaledX, y: scaledY) + } else { + Ryujinx.shared.virtualController.thumbstickMoved(.left, x: scaledX, y: scaledY) + } + } + } + } +} + + +struct EnhancedEightWayJoystick: View { + var onDirectionChanged: ((x: Int, y: Int)) -> Void + + @State private var currentDirection: (x: Int, y: Int) = (0, 0) + private let radius: CGFloat = 50 + private let centerPoint = CGPoint(x: 50, y: 50) + + var body: some View { + GeometryReader { geometry in + ZStack { + // 触摸区域 + Circle() + .fill(Color.gray.opacity(0.2)) + .frame(width: 200, height: 200) + .contentShape(Circle()) + + // 方向指示器 + Circle() + .fill(Color.white.opacity(0.7)) + .frame(width: 75, height: 75) + .offset(x: CGFloat(currentDirection.x), y: CGFloat(currentDirection.y)) + } + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { gesture in + let location = gesture.location + //添加使用Geometry计算的dxdy坐标 + let viewFrame = geometry.frame(in: .local) + let center = CGPoint( + x: viewFrame.midX, + y: viewFrame.midY + ) + + //let dx = location.x - centerPoint.x + //let dy = location.y - centerPoint.y + let dx = location.x - center.x + let dy = location.y - center.y + + // 计算极坐标 + let radians = atan2(dy, dx) + var degrees = radians * 180 / .pi + if degrees < 0 { degrees += 360 } + + // 获取调整后的方向 + let direction = get45DegreeDirection(from: degrees) + + // 仅在方向变化时更新 + if direction.x != currentDirection.x || direction.y != currentDirection.y { + currentDirection = direction + onDirectionChanged(direction) + } + } + .onEnded { _ in + currentDirection = (0, 0) + onDirectionChanged((0, 0)) + } + + ) + } + } + + // 45度均匀分布方向判断 + //调转Y轴 + //调整scale到 168 + private func get45DegreeDirection(from degrees: CGFloat) -> (x: Int, y: Int) { + switch degrees { + // 右 (337.5°-22.5°) + case 337.5...360, 0..<22.5: + return (50, 0) + + // 右上 (22.5°-67.5°) + case 22.5..<67.5: + //return (50, -50) + return (50, 50) + // 上 (67.5°-112.5°) + case 67.5..<112.5: + //return (0, -50) + return (0, 50) + // 左上 (112.5°-157.5°) + case 112.5..<157.5: + //return (-50, -50) + return (-50, 50) + // 左 (157.5°-202.5°) + case 157.5..<202.5: + return (-50, 0) + + // 左下 (202.5°-247.5°) + case 202.5..<247.5: + //return (-50, 50) + return (-50, -50) + // 下 (247.5°-292.5°) + case 247.5..<292.5: + //return (0, 50) + return (0, -50) + // 右下 (292.5°-337.5°) + case 292.5..<337.5: + //return (50, 50) + return (50, -50) + default: + return (0, 0) + } + } + } + + +struct FreeRoamJoystick: View { + var onPositionChanged: (CGPoint) -> Void + + private let trackRadius: CGFloat = 90 + private let thumbRadius: CGFloat = 75 + @State private var isEightWay = 0 // 0: 自由模式,1: 八向模式 + @State private var position = CGPoint.zero + @State private var viewCenter: CGPoint = .zero + + + var body: some View { + GeometryReader { geometry in + ZStack { + Spacer() + // 摇杆背景(点击区域) + Circle() + .stroke(Color.gray.opacity(0.5), lineWidth: 4) + .frame(width: trackRadius*2, height: trackRadius*2) + .position(x: geometry.size.width/2, y: geometry.size.height/2) + .contentShape(Circle()) // 关键修改点1:设置点击区域形状 + // 摇杆头 + let maxMoveRadius = trackRadius - thumbRadius + Circle() + .fill(Color.white.opacity(0.4)) + .frame(width: thumbRadius*2, height: thumbRadius*2) + //.offset(x: position.x, y: position.y) + .offset( + x: min(max(position.x, -maxMoveRadius), maxMoveRadius), + y: min(max(position.y, -maxMoveRadius), maxMoveRadius) + ) + .position(x: geometry.size.width/2, y: geometry.size.height/2) + } + .gesture( + DragGesture(minimumDistance: 0) // 关键修改点2:零距离触发 + .onChanged { gesture in + let viewFrame = geometry.frame(in: .local) + let touchPoint = gesture.location + + // 计算相对中心坐标 + let center = CGPoint( + x: viewFrame.midX, + y: viewFrame.midY + ) + let delta = CGPoint( + x: touchPoint.x - center.x, + y: touchPoint.y - center.y + ) + + // 限制在圆形范围内 + let distance = sqrt(delta.x * delta.x + delta.y * delta.y) + let clampedDelta = distance > trackRadius ? CGPoint( + x: delta.x * trackRadius / distance, + y: delta.y * trackRadius / distance + ) : delta + var test_data = clampedDelta + // 更新状态 + //尝试不同返回值 + if isEightWay == 1 { + test_data = getEightDirection(for: test_data) + } + + //position = clampedDelta + position = test_data + //onPositionChanged(clampedDelta) + onPositionChanged(test_data) + } + .onEnded { _ in + + position = .zero + onPositionChanged(.zero) + + } + ) + .onAppear { + viewCenter = CGPoint( + x: geometry.size.width/2, + y: geometry.size.height/2 + ) + } + } + .frame(width: trackRadius, height: trackRadius) + //.frame(alignment: .bottomLeading) + } + private func getEightDirection(for delta: CGPoint) -> CGPoint { + + + let angle = atan2(-delta.y, delta.x) + let degrees = angle * 180 / .pi + let normalized = degrees < 0 ? degrees + 360 : degrees + + switch normalized { + case 337.5...360, 0..<22.5: // 右 + return CGPoint(x: trackRadius, y: 0) + case 22.5..<67.5: // 右上 + return CGPoint(x: trackRadius, y: -trackRadius) + case 67.5..<112.5: // 上 + return CGPoint(x: 0, y: -trackRadius) + case 112.5..<157.5: // 左上 + return CGPoint(x: -trackRadius, y: -trackRadius) + case 157.5..<202.5: // 左 + return CGPoint(x: -trackRadius, y: 0) + case 202.5..<247.5: // 左下 + return CGPoint(x: -trackRadius, y: trackRadius) + case 247.5..<292.5: // 下 + return CGPoint(x: 0, y: trackRadius) + case 292.5..<337.5: // 右下 + return CGPoint(x: trackRadius, y: trackRadius) + default: + return .zero + } + + + } + } + + + + +struct NineGridJoystick: View { + var onDirectionChanged: (CGPoint) -> Void + + private let trackRadius: CGFloat = 90 + private let thumbRadius: CGFloat = 35 + @State private var position = CGPoint.zero + @State private var activeColor = Color.white.opacity(0.4) + + // 九宫格配置参数 + private var gridThreshold: CGFloat { + trackRadius / 3 // 56 + } + + var body: some View { + GeometryReader { geometry in + ZStack { + // 九宫格背景 + gridBackgroundView() + + // 摇杆头 + Circle() + .fill(activeColor) + .frame(width: thumbRadius*2, height: thumbRadius*2) + .offset(x: position.x, y: position.y) + .position(x: geometry.size.width/2, y: geometry.size.height/2) + //.animation(.spring(), value: position) + } + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { gesture in + let center = CGPoint(x: geometry.size.width/2, y: geometry.size.height/2) + let delta = CGPoint( + x: gesture.location.x - center.x, + y: gesture.location.y - center.y + ) + + let direction = getGridDirection(for: delta) + updatePosition(to: direction) + } + .onEnded { _ in + resetPosition() + } + ) + } + .frame(width: trackRadius*2, height: trackRadius*2) + } + + // MARK: - 九宫格视图 + private func gridBackgroundView() -> some View { + ZStack { + // 背景网格线 + Path { path in + // 垂直中线 + path.move(to: CGPoint(x: trackRadius - gridThreshold, y: 0)) + path.addLine(to: CGPoint(x: trackRadius - gridThreshold, y: trackRadius*2)) + path.move(to: CGPoint(x: trackRadius + gridThreshold, y: 0)) + path.addLine(to: CGPoint(x: trackRadius + gridThreshold, y: trackRadius*2)) + // 水平中线 + path.move(to: CGPoint(x: 0, y: trackRadius - gridThreshold)) + path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius - gridThreshold)) + path.move(to: CGPoint(x: 0, y: trackRadius + gridThreshold)) + path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius + gridThreshold)) + } + .stroke(Color.gray.opacity(0.3), lineWidth: 2) + + // 中心区域高亮 + Rectangle() + .fill(Color.gray.opacity(0.1)) + .frame(width: gridThreshold*2, height: gridThreshold*2) + } + } + + // MARK: - 方向判断逻辑 + private func getGridDirection(for delta: CGPoint) -> CGPoint { + let xVal = delta.x + let yVal = delta.y + + switch (xVal, yVal) { + case (..<(-gridThreshold), ..<(-gridThreshold)): // 左上 + return CGPoint(x: -trackRadius, y: -trackRadius) + case ((-gridThreshold)...gridThreshold, ..<(-gridThreshold)): // 上 + return CGPoint(x: 0, y: -trackRadius) + case (gridThreshold..., ..<(-gridThreshold)): // 右上 + return CGPoint(x: trackRadius, y: -trackRadius) + case (..<(-gridThreshold), (-gridThreshold)...gridThreshold): // 左 + return CGPoint(x: -trackRadius, y: 0) + case ((-gridThreshold)...gridThreshold, (-gridThreshold)...gridThreshold): // 中 + return .zero + case (gridThreshold..., (-gridThreshold)...gridThreshold): // 右 + return CGPoint(x: trackRadius, y: 0) + case (..<(-gridThreshold), gridThreshold...): // 左下 + return CGPoint(x: -trackRadius, y: trackRadius) + case ((-gridThreshold)...gridThreshold, gridThreshold...): // 下 + return CGPoint(x: 0, y: trackRadius) + case (gridThreshold..., gridThreshold...): // 右下 + return CGPoint(x: trackRadius, y: trackRadius) + default: + return .zero + } + } + + // MARK: - 状态更新 + private func updatePosition(to direction: CGPoint) { + //withAnimation(.spring()) { + position = direction + activeColor = direction == .zero ? + Color.white.opacity(0.4) : + Color.blue.opacity(0.6) + onDirectionChanged(direction) + //} + } + + private func resetPosition() { + //withAnimation(.spring()) { + position = .zero + activeColor = Color.white.opacity(0.4) + onDirectionChanged(.zero) + //} + } +} + + + +struct DynamicNineGridJoystick: View { + var onDirectionChanged: (CGPoint) -> Void + + private let trackRadius: CGFloat = 100 + private let thumbRadius: CGFloat = 35 + @State private var position = CGPoint.zero + @State private var activeColor = Color.white.opacity(0.4) + @State private var lastDirection: CGPoint = .zero + + // 九宫格配置 + private var gridThreshold: CGFloat { trackRadius / 3 } + private let hapticGenerator = UIImpactFeedbackGenerator(style: .soft) + + var body: some View { + GeometryReader { geometry in + ZStack { + // 九宫格背景 + gridBackgroundView() + + // 摇杆头 + Circle() + .fill(activeColor) + .frame(width: thumbRadius*2, height: thumbRadius*2) + .offset(x: position.x, y: position.y) + .position(x: geometry.size.width/2, y: geometry.size.height/2) + //.animation(.spring(), value: position) + } + // 关键修改点:设置全区域点击识别 + .contentShape(Rectangle()) // 使用矩形识别区域覆盖整个GeometryReader + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { gesture in + handleGestureChange( + location: gesture.location, + center: CGPoint(x: geometry.size.width/2, + y: geometry.size.height/2) + ) + } + .onEnded { _ in + resetPosition() + } + ) + } + .frame(width: trackRadius*2, height: trackRadius*2) + .onAppear { + hapticGenerator.prepare() + } + } + + // MARK: - 手势处理 + private func handleGestureChange(location: CGPoint, center: CGPoint) { + let delta = CGPoint( + x: location.x - center.x, + y: location.y - center.y + ) + + let currentDirection = getGridDirection(for: delta) + + // 仅在新方向变化时触发 + if currentDirection != lastDirection { + updatePosition(to: currentDirection) + triggerHapticFeedback() + lastDirection = currentDirection + } + } + + // MARK: - 九宫格方向判断(优化版) + private func getGridDirection(for delta: CGPoint) -> CGPoint { + let xVal = delta.x + let yVal = delta.y + + // 定义九宫格边界 + let isLeft = xVal < -gridThreshold + let isRight = xVal > gridThreshold + let isUp = yVal < -gridThreshold + let isDown = yVal > gridThreshold + + switch (isLeft, isRight, isUp, isDown) { + case (true, false, true, false): // 左上 + return CGPoint(x: -trackRadius, y: -trackRadius) + case (false, false, true, false): // 上 + return CGPoint(x: 0, y: -trackRadius) + case (false, true, true, false): // 右上 + return CGPoint(x: trackRadius, y: -trackRadius) + case (true, false, false, false): // 左 + return CGPoint(x: -trackRadius, y: 0) + case (false, false, false, false): // 中 + return .zero + case (false, true, false, false): // 右 + return CGPoint(x: trackRadius, y: 0) + case (true, false, false, true): // 左下 + return CGPoint(x: -trackRadius, y: trackRadius) + case (false, false, false, true): // 下 + return CGPoint(x: 0, y: trackRadius) + case (false, true, false, true): // 右下 + return CGPoint(x: trackRadius, y: trackRadius) + default: // 处理对角线滑动时的中间状态 + return calculateEdgeCase(delta: delta) + } + } + + // 处理边缘滑动状态 + private func calculateEdgeCase(delta: CGPoint) -> CGPoint { + let angle = atan2(delta.y, delta.x) * 180 / .pi + let normalized = angle < 0 ? angle + 360 : angle + + switch normalized { + case 45..<135: // 上象限 + return CGPoint(x: 0, y: -trackRadius) + case 135..<225: // 左象限 + return CGPoint(x: -trackRadius, y: 0) + case 225..<315: // 下象限 + return CGPoint(x: 0, y: trackRadius) + default: // 右象限 + return CGPoint(x: trackRadius, y: 0) + } + } + + // MARK: - 视觉与反馈更新 + private func updatePosition(to direction: CGPoint) { + //withAnimation(.interactiveSpring()) { + + position = direction + activeColor = direction == .zero ? + Color.white.opacity(0.4) : + Color.blue.opacity(0.6) + onDirectionChanged(direction) + + } + + private func resetPosition() { + //withAnimation(.spring()) { + position = .zero + activeColor = Color.white.opacity(0.4) + lastDirection = .zero + onDirectionChanged(.zero) + //} + } + + private func triggerHapticFeedback() { + guard position != .zero else { return } + hapticGenerator.impactOccurred(intensity: 0.7) + } + + // MARK: - 九宫格视图(优化版) + private func gridBackgroundView() -> some View { + ZStack { + // 网格线 + Path { path in + let offsets = [-gridThreshold, 0, gridThreshold] + for offset in offsets { + path.move(to: CGPoint(x: trackRadius + offset, y: 0)) + path.addLine(to: CGPoint(x: trackRadius + offset, y: trackRadius*2)) + path.move(to: CGPoint(x: 0, y: trackRadius + offset)) + path.addLine(to: CGPoint(x: trackRadius*2, y: trackRadius + offset)) + } + } + .stroke(Color.gray.opacity(0.3), lineWidth: 2) + + // 中心区域 + RoundedRectangle(cornerRadius: 12) + .fill(Color.gray.opacity(0.1)) + .frame(width: gridThreshold*2, height: gridThreshold*2) + } + } +}