基于Unity或Python的罗技方向盘控制


基于Unity或Python的罗技方向盘控制

1、罗技方向盘简介

罗技方向盘(Logitech G29)是一款专为游戏设计的方向盘,适用于PC、PS3和PS4平台。它具有真实的方向盘手感、踏板和换挡杆,可以带来更加真实的游戏体验。
可以在淘宝或者京东上购买,价格约为2000元。
本篇文章测试采用的是罗技方向盘G923。

2、基于Unity的罗技方向盘控制

在实现这一功能之前,我们需要在Project Settings / Input Manager中进行按键的配置。

然后,在Car对象上添加CarController脚本,该脚本可以在Standard Assets / Vehicles / Car / Scripts中找到。

CarController脚本中,我们可以找到GetInput()函数,该函数用于获取输入,我们可以在该函数中添加罗技方向盘的输入。

CarController脚本示例代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CarController : MonoBehaviour
{
    private const string HORIZONTAL = "Horizontal";
    private const string VERTICAL = "Vertical";

    private float horizontalInput;
    private float verticalInput;
    private float currentSteerAngle;
    private float currentbreakForce;
    private bool isBreaking;
    public enum ControlType
    {
        Keyboard,
        SteeringWheel
    }


    [SerializeField] private float motorForce;
    [SerializeField] private float breakForce;
    [SerializeField] private float maxSteerAngle;

    [SerializeField] private WheelCollider frontLeftWheelCollider;
    [SerializeField] private WheelCollider frontRightWheelCollider;
    [SerializeField] private WheelCollider rearLeftWheelCollider;
    [SerializeField] private WheelCollider rearRightWheelCollider;

    [SerializeField] private Transform frontLeftWheelTransform;
    [SerializeField] private Transform frontRightWheeTransform;
    [SerializeField] private Transform rearLeftWheelTransform;
    [SerializeField] private Transform rearRightWheelTransform;

    [SerializeField] private AudioSource engineSound;

    [SerializeField] private ControlType controlType;



    private void Start()
    {
        if (engineSound == null)
        {
            engineSound = GetComponent();
        }
    }

    private void FixedUpdate()
    {
        GetInput();
        HandleMotor();
        HandleSteering();
        UpdateWheels();
        UpdateEngineSound();
    }


    private void GetInput()
    {
        if (controlType == ControlType.SteeringWheel)
        {
            horizontalInput = Input.GetAxis("Horizontal");
            verticalInput = Input.GetAxis("Vertical");
            verticalInput = (verticalInput + 1) / 2;

            isBreaking = Input.GetAxis("BrakePedalG923") < 0.1f;
        }
        else
        {
            // ʹ ü       
            horizontalInput = Input.GetAxis(HORIZONTAL);
            verticalInput = Input.GetAxis(VERTICAL);
            isBreaking = Input.GetKey(KeyCode.Space);
        }
    }



    private void HandleMotor()
    {
        frontLeftWheelCollider.motorTorque = verticalInput * motorForce;
        frontRightWheelCollider.motorTorque = verticalInput * motorForce;
        currentbreakForce = isBreaking ? breakForce : 0f;
        ApplyBreaking();
    }

    private void ApplyBreaking()
    {
        frontRightWheelCollider.brakeTorque = currentbreakForce;
        frontLeftWheelCollider.brakeTorque = currentbreakForce;
        rearLeftWheelCollider.brakeTorque = currentbreakForce;
        rearRightWheelCollider.brakeTorque = currentbreakForce;
    }

    private void HandleSteering()
    {
        currentSteerAngle = maxSteerAngle * horizontalInput;
        frontLeftWheelCollider.steerAngle = currentSteerAngle;
        frontRightWheelCollider.steerAngle = currentSteerAngle;
    }

    private void UpdateWheels()
    {
        UpdateSingleWheel(frontLeftWheelCollider, frontLeftWheelTransform);
        UpdateSingleWheel(frontRightWheelCollider, frontRightWheeTransform);
        UpdateSingleWheel(rearRightWheelCollider, rearRightWheelTransform);
        UpdateSingleWheel(rearLeftWheelCollider, rearLeftWheelTransform);
    }

    private void UpdateSingleWheel(WheelCollider wheelCollider, Transform wheelTransform)
    {
        Vector3 pos;
        Quaternion rot
;       wheelCollider.GetWorldPose(out pos, out rot);
        wheelTransform.rotation = rot;
        wheelTransform.position = pos;
    }

    private void UpdateEngineSound()
    {
        if (engineSound != null)
        {
            float inputVal = 0f;
            if (controlType == ControlType.SteeringWheel)
            {
                inputVal = Input.GetAxis("Vertical");
                inputVal = (inputVal + 1) / 2;

            }
            else
            {
                inputVal = Mathf.Abs(verticalInput);
            }

            engineSound.pitch = 1 + inputVal;
            engineSound.volume = Mathf.Clamp(inputVal, 0.0f, 0.4f);

            Debug.Log("Updating Engine Sound: pitch = " + engineSound.pitch + ", volume = " + engineSound.volume + ", Input Value = " + inputVal + ", isBreaking Value = " + isBreaking);
        }
    }
}

3、基于Python的罗技方向盘控制

有些情况下,我们需要在Python中控制罗技方向盘,比如使用PythonUnity程序进行通信,或者使用Python与其他软件进行通信。
通过这种方式,可以方便添加多重控制算法,如脑机接口控制、手势识别控制等。

可以通过这种方法来获取:

# encoding:utf-8
# 以下代码用于检测logit方向盘的按键是否被摁下

import pygame

pygame.init()
pygame.joystick.init()

# 假设方向盘是第一个连接的控制器
joystick = pygame.joystick.Joystick(0)
joystick.init()

# 获取按钮数量
button_count = joystick.get_numbuttons()
print(f"Number of buttons: &#123;button_count&#125;")

# 检测按钮状态
while True:
    pygame.event.pump()  # 更新状态
    for i in range(button_count):
        button_state = joystick.get_button(i)
        if button_state:
            print(f"Button &#123;i&#125; pressed")

# 退出前清理
pygame.quit()

通常来说,有两种方法可以实现罗技方向盘控制,分别是通过pygame库和logidrivepy库。

3.1、使用pygame

这种方法较为简单,Pygame库中提供了pygame.joystick类,可以用于读取标准摇杆设备的输入,也包括罗技方向盘。
但是pygame.joystick类只能读取到方向盘的输入,无法控制方向盘的输出以及无法实现力反馈效果。
相关代码如下:

# encoding:utf-8
import numpy as np
from pynput import keyboard
import pygame

class Controller:
    def process_input(self):
        raise NotImplementedError

    def process_others(self, *args, **kwargs):
        pass

    def close(self):
        pass


class KeyboardController(Controller):
    STEERING_INCREMENT = 0.04
    STEERING_DECAY = 0.25

    THROTTLE_INCREMENT = 0.1
    THROTTLE_DECAY = 0.2

    BRAKE_INCREMENT = 0.5
    BRAKE_DECAY = 0.5

    def __init__(self):
        self.running = True  # 设置运行标志

        # 定义按键的当前状态
        self.key_states = &#123;
            'w': False,
            's': False,
            'a': False,
            'd': False
        &#125;
        self.listener = keyboard.Listener(on_press=self.on_press, on_release=self.on_release)
        self.listener.start()

        self.steering = 0.
        self.throttle = 0.
        self.brake = 0.
        self.np_random = np.random.RandomState(None)

    def on_press(self, key):
        try:
            if key.char in self.key_states:
                self.key_states[key.char] = True
        except AttributeError:
            pass

    def on_release(self, key):
        try:
            if key.char in self.key_states:
                self.key_states[key.char] = False
        except AttributeError:
            pass

    def process_input(self):

        steering = 0.
        throttle = 0.
        if self.key_states['a']:
            steering -= 1.0
        if self.key_states['d']:
            steering += 1.0
        if self.key_states['w']:
            throttle += 1.0
            self.brake = 0.0
        if self.key_states['s']:
            throttle -= 1.0
            self.brake = 1.0

        self.further_process(steering, throttle)

        return np.array([self.steering, self.throttle, self.brake], dtype=np.float64)

    def further_process(self, steering, throttle):
        if steering == 0.:
            if self.steering > 0.:
                self.steering -= self.STEERING_DECAY
                self.steering = max(0., self.steering)
            elif self.steering < 0.:
                self.steering += self.STEERING_DECAY
                self.steering = min(0., self.steering)
        if throttle == 0.:
            if self.throttle > 0.:
                self.throttle -= self.THROTTLE_DECAY
                self.throttle = max(self.throttle, 0.)
            elif self.throttle < 0.:
                self.throttle += self.BRAKE_DECAY
                self.throttle = min(0., self.throttle)

        if steering > 0.:
            self.steering += self.STEERING_INCREMENT if self.steering > 0. else self.STEERING_DECAY
        elif steering < 0.:
            self.steering -= self.STEERING_INCREMENT if self.steering < 0. else self.STEERING_DECAY

        if throttle > 0.:
            self.throttle = max(self.throttle, 0.)
            self.throttle += self.THROTTLE_INCREMENT
        elif throttle < 0.:
            self.throttle = min(self.throttle, 0.)
            self.throttle -= self.BRAKE_INCREMENT

        rand = self.np_random.rand() / 10000
        # self.throttle += rand[0]
        self.steering += rand

        self.throttle = min(max(-1., self.throttle), 1.)
        self.steering = min(max(-1., self.steering), 1.)

    # def process_others(self, takeover_callback=None):
    #     """This function allows the outer loop to call callback if some signal is received by the controller."""
    #     # if (takeover_callback is None) or (not self.pygame_control) or (not pygame.get_init()):
    #     #     return
    #     for event in pygame.event.get():
    #         print("event")
    #         if event.type == pygame.KEYDOWN and event.key == pygame.K_t:
    #             # Here we allow user to press T for takeover callback.
    #             # takeover_callback()
    #             print("takeover_callback")
    #             pass

    def close(self):
        self.listener.stop()  # 停止监听


class SteeringWheelController(Controller):
    STEERING_MAKEUP = 1.5

    def __init__(self):
        self.running = True  # 设置运行标志

        pygame.display.init()
        pygame.joystick.init()

        assert pygame.joystick.get_count() > 0, "Please connect Steering Wheel or use keyboard input"
        print("Successfully Connect your Steering Wheel!")

        self.joystick = pygame.joystick.Joystick(0)
        self.joystick.init()

        self.right_shift_paddle = False
        self.left_shift_paddle = False

    def process_input(self):
        pygame.event.pump()
        # 转向映射到轴 0, 油门映射到轴 2, 刹车映射到轴 3
        steering = self.joystick.get_axis(0)
        throttle = -self.joystick.get_axis(2) + self.joystick.get_axis(3)

        # print(self.joystick.get_axis(3))
        if self.joystick.get_axis(3) < 0.8:
            brake = 1.0
        else:
            brake = 0.0

        self.right_shift_paddle = self.joystick.get_button(4)  # 右侧换档拨片,按钮 4
        self.left_shift_paddle = self.joystick.get_button(5)   # 左侧换档拨片,按钮 5
        if self.right_shift_paddle:
            print("检测到右侧换档拨片被摁下!")
        if self.left_shift_paddle:
            print("检测到左侧换档拨片被摁下!")

        return [steering * self.STEERING_MAKEUP, throttle / 2, brake]

    def close(self):
        pygame.quit()

3.2、使用logidrivepy库

pygame中仅提供了标准的摇杆控制接口,不能实现特定的力反馈效果(如弹簧力)。
力反馈功能通常需要特定的硬件支持,并且依赖于特定平台或设备的API。
罗技官方提供了SDK。可以通过调用LogitechSteeringWheelEnginesWrapper.dll来实现。

logidrivepy库可用于连接罗技方向盘的python模块。该模块已使用罗技 G920 Driving Force Racing Wheel 进行了测试,可以实现力反馈效果。
logidrivepy库的GitHub地址为:cengizozel/LogiDrivePy:用于连接罗技方向盘的 Python 模块。 (github.com)

原始功能是使用C#实现的,是Unity游戏引擎的罗技方向盘SDK的一部分。
SDK允许开发人员使用一组预定义的力反馈效果或通过定义特定力创建自定义效果,轻松地将罗技方向盘支持添加到他们的游戏中。

logidrivepy库的作者已经将其封装为Python库,能够通过罗技的 DLL(LogitechSteeringWheelEnginesWrapper.dll)加载罗技方向盘并与之交互。

相关代码如下:

# encoding:utf-8
"""
罗技G923数据读取
各个轴和按钮的序号请根据官方的demo查看或者根据此程序尝试
"""
import time
from logidrivepy import LogitechController


if __name__ == '__main__':
    # 创建 LogitechController 实例
    controller = LogitechController()

    left_paddle_button_number = 5
    right_paddle_button_number = 4

    steering_initialize = controller.steering_initialize()
    logi_update = controller.logi_update()
    is_connected = controller.is_connected(0)

    # 初始化 SDK
    print(f"\n---初始化 Logitech Controller---")
    print(f"steering_initialize: &#123;steering_initialize&#125;")
    print(f"logi_update: &#123;logi_update&#125;")
    print(f"is_connected: &#123;is_connected&#125;")

    if steering_initialize and logi_update and is_connected:
        print(f"All tests passed.\n")
    else:
        print(f"Failed to initialize G923 system.\n")

    # 根据需要添加力反馈效果
    controller.play_spring_force(0, 0, 70, 50)
    # 主循环
    while True:
        # 更新控制器状态
        if controller.logi_update():
            # 获取方向盘状态
            state = controller.get_state_engines(0)
            if state:
                # 这里可以获取方向盘的角度、油门值等
                wheel_position = state.contents.lX  # 方向盘位置
                throttle_value = state.contents.lZ  # 油门值
                brake_value = state.contents.lRz  # 刹车值
                clutch_value = state.contents.lY  # 离合值

                print(f"wheel_position: &#123;wheel_position&#125;, "
                      f"throttle_value: &#123;throttle_value&#125;, "
                      f"brake_value: &#123;brake_value&#125;, "
                      f"clutch_value: &#123;clutch_value&#125;"

                      )

                # 检查左侧换档拨片是否被按下
                left_paddle_pressed = controller.button_is_pressed(0, left_paddle_button_number)
                if left_paddle_pressed:
                    print("左侧换档拨片被按下")

                # 检查右侧换档拨片是否被按下
                right_paddle_pressed = controller.button_is_pressed(0, right_paddle_button_number)
                if right_paddle_pressed:
                    print("右侧换档拨片被按下")

                # ... 其他游戏逻辑 ...
        else:
            print("Failed to update controller.")

        # ... 其他应用逻辑 ...
        time.sleep(0.1)

    # 关闭 SDK
    controller.steering_shutdown()

4、资料下载


文章作者: BITBCI
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 BITBCI !
  目录