查看: 301|回复: 0

[教程] 【功能组件】安卓水平仪

[复制链接]
发表于 2026-8-2 21:34:22 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本帖最后由 Maz马 于 2026-8-29 20:55 编辑

在早期时写了一个视差组件,最近在更新优化
好友@blurred提供了一个应用在视差组件上的,安卓手机传感器草案(就是安卓没有鼠标,用陀螺仪来检测)
当时问题蛮多的,但是因为并不是主要功能就没有彻底完善,把一些问题当成了设备个例的问题

然后现在完善了一下,并记录一下思路的过程

1.模块化调用,解耦,内部直接角度转弧度输出X,Y,Z三个数据

2.当时以为是设备个例问题的偏移抖动
   但是因为最近打王者,猴子的皮肤“无相”就是一个非常流畅的视差图,unity可往,我renpy亦可往
   所以怎么想都和设备个例无关,在重复尝试后,发现实际是不应该调用地磁,地磁极度的不稳定和滞后,
   还有一方面的因数是参数映射错了,把偏航角当成X的映射,实际应该是Z。
   在抖动稳定之后,怎么解决设备差呢?
   答案是归一化,不管设备基准如何,我们只使用数学比例来转实际值

3.分开灵敏度,套用到组件之后,偏移值的手感十分奇怪,出于用户体验设计,发现三轴应该有自己的度量衡,
   给三轴分开量程之后,诡异的偏移就消失了(

4.稳定状态对齐,因为是一个3D空间,应该实时滑窗去追踪稳定的角度,而不是定死原点

5.自管理生命周期,应该只有被调用时才去申请陀螺仪数据,自己管理注销,而不是持续工作浪费性能和设备功耗

测试后,基本能稳定使用了,renpy圈迎来噩耗
可以摇一摇打开拼多多了!


原帖已更新【变换组件】视差图片
https://www.renpy.cn/forum.php?mod=viewthread&tid=1745

[RenPy] 纯文本查看 复制代码
# ---------- 安卓水平仪 ----------
init python:
    """
    Android Sensor (Ver 1.0 Ren'py 8.3.6)
    Author: Maz
    """
    import math
    import time
    class AndroidSensor:
        def __init__(self,max_roll=15.0,max_pitch=30.0,max_yaw=30.0,deadzone=0.05):
            # ---------- 调用系统 ----------
            self.sensor_get = False            # 传感器系统是否可用
            self.sensor_listener = None        # Android传感器监听
            self.sensor_manager = None         # Android传感器终端
            self.sensor_timeout = None         # 传感器数据回调间隔 在init用默认的GAME (20ms)
            self.sensor_rotate = [0,0,0,0]     # 传感器旋转矢量数据(四元)

            # 超时自动注销
            self.sensor_init = False           # 是否已收到首帧传感器数据(收到时校准基准)
            self.sensor_update = False         # 活跃锁
            self.sensor_update_time = 0.0      # 活跃时间戳
            self.sensor_update_timeout = 5.0   # 活跃间隔

            # ---------- 感应角度 ----------
            # 映射三轴量程,对应半屏,越小越灵敏
            self.max_roll = max_roll           # 横滚角 左右倾斜 人类习惯短边倾角变化不大
            self.max_pitch = max_pitch         # 俯仰角 前后翻动 长边变化大,放宽防饱和
            self.max_yaw = max_yaw             # 偏航角 左右旋转 旋转幅度大,约等同于长边
            
            self.deadzone = deadzone           # 死区 三轴量程不同,归一化比例保证手感一致
            
            # ---------- 即时角度 ----------
            # 映射三轴距离
            self.roll = 0.0                    # x
            self.pitch = 0.0                   # y
            self.yaw = 0.0                     # z

            # ---------- 基准角度 ----------
            # 映射三轴原点
            self.base_roll = 0
            self.base_pitch = 0
            self.base_yaw = 0
            
            # ---------- 稳定角度 ----------
            # 动态基准更新
            self.stable_roll = 0.0
            self.stable_pitch = 0.0
            self.stable_yaw = 0.0
            
            # 缓动对齐
            self.stable_update = False         # 更新锁
            self.stable_update_timeout = 1.0   # 更新间隔
            
            # 稳定检测
            self.stable_timer = 0.0            # 确认计时(秒)
            self.stable_timeout = 5.0          # 确认间隔(秒)角度增量持续在阈值内这么久才锁定基准角
            self.stable_threshold = 5.0        # 判定阈值(度)相邻采样角度变化总量小于阈值才视为稳定
            self.stable_sample = (0.0,0.0,0.0) # 上次采样角度
            self.stable_target = (0.0,0.0,0.0) # 锁定目标角度

            # 采样节流
            self.stable_step_time = 0.0        # 节流时间戳
            self.stable_step_timeout = 1.0     # 节流间隔

            self.init_sensors()
        # 准备传感器
        def init_sensors(self):
            try:
                # Android相关
                import jnius
                PythonActivity = jnius.autoclass('org.renpy.android.PythonSDLActivity')
                Context = jnius.autoclass('android.content.Context')
                Sensor = jnius.autoclass('android.hardware.Sensor')
                SensorManager = jnius.autoclass('android.hardware.SensorManager')
                activity = PythonActivity.mActivity
                self.sensor_manager = activity.getSystemService(Context.SENSOR_SERVICE)
                self.sensor_timeout = SensorManager.SENSOR_DELAY_GAME
                # 旋转矢量传感器(系统级融合,含陀螺仪补偿,响应快、跟手)
                self.rotation_vector = self.sensor_manager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR)
                if self.rotation_vector is None:
                    self.rotation_vector = self.sensor_manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
                if self.rotation_vector is None:
                    print("设备缺少旋转矢量传感器")
                    return
                # Android持续回调
                class SensorListener(jnius.PythonJavaClass):
                    __javainterfaces__ = ['android/hardware.SensorEventListener']
                    def __init__(self,manager,**kwargs):
                        super(SensorListener,self).__init__(**kwargs)
                        self.manager = manager
                    @jnius.java_method('(Landroid/hardware/SensorEvent;)V')
                    def onSensorChanged(self,event):
                        # 长时间没有组件调用 update(活跃时间戳未刷新):自行注销监听释放资源
                        if time.time() - self.manager.sensor_update_time > self.manager.sensor_update_timeout:
                            self.manager._unregister()
                            return
                        # 保存旋转矢量(四元数 x,y,z,w)
                        v = event.values
                        self.manager.sensor_rotate = [v[i] for i in range(min(4,len(v)))]
                        # 计算倾角
                        self.manager._check_direction()
                        # 首次收到数据:以当前真实姿态校准基准角与稳定角(原点起步)
                        if not self.manager.sensor_init:
                            self.manager.sensor_init = True
                            self.manager.base_roll = self.manager.roll
                            self.manager.base_pitch = self.manager.pitch
                            self.manager.base_yaw = self.manager.yaw
                            self.manager.stable_roll = self.manager.roll
                            self.manager.stable_pitch = self.manager.pitch
                            self.manager.stable_yaw = self.manager.yaw
                        # 更新稳定角检测
                        self.manager._check_stable()
                    @jnius.java_method('(Landroid/hardware/Sensor;I)V')
                    def onAccuracyChanged(self,sensor,accuracy):
                        pass
                self.sensor_listener = SensorListener(self)
                self.sensor_get = True
                print("倾角传感器对象准备完成(按需注册监听)")
                 
            except Exception as e:
                print(f"无法初始化倾角传感器: {e}")
                self.sensor_get = False
        # 注销传感器
        def stop_sensors(self):
            self._unregister()
        # 申请传感器监听
        def _register(self):
            if self.sensor_update or not self.sensor_get:
                return
            try:
                self.sensor_manager.registerListener(
                    self.sensor_listener,
                    self.rotation_vector,
                    self.sensor_timeout
                )
                self.sensor_update = True
                # 注册是异步的,回调首帧才拿到真实姿态:先清校准标志,收到首帧数据时再同步基准角与稳定角(原点起步)
                self.sensor_init = False
            except Exception as e:
                print(f"注册传感器监听失败: {e}")
        # 注销传感器监听
        def _unregister(self):
            if not self.sensor_update:
                return
            try:
                self.sensor_manager.unregisterListener(self.sensor_listener)
            except:
                pass
            self.sensor_update = False
        # 根据旋转矢量计算设备方向
        def _check_direction(self):
            try:
                import jnius
                SensorManager = jnius.autoclass('android.hardware.SensorManager')
                # 旋转矢量 → 旋转矩阵 → 欧拉角
                R = [0] * 9
                I = [0] * 9
                SensorManager.getRotationMatrixFromVector(R,self.sensor_rotate)
                orientation = [0] * 3
                SensorManager.getOrientation(R,orientation)
                # 弧度转角度 映射到 x/y/z
                self.yaw = math.degrees(orientation[0])  # 偏航角(左右旋转)
                self.pitch = math.degrees(orientation[1]) # 俯仰角(前后翻动)
                self.roll = math.degrees(orientation[2]) # 横滚角(左右倾斜)                
            except Exception as e:
                print(f"计算方向时出错: {e}")
        # 死区过滤:角度差小于该轴死区时不响应
        def _check_deadzone(self,angle_diff,zone):
            if abs(angle_diff) < zone:
                return 0
            return angle_diff
        # 稳定追踪:稳定确认后锁定目标角度,再缓动对齐
        def _check_stable(self):
            now = time.time()
            # 更新节流,避免高频回调拖累性能
            if now - self.stable_step_time < self.stable_step_timeout:
                return
            self.stable_step_time = now
            
            # 相邻采样三轴角度变化总量(度)
            cur = (self.roll,self.pitch,self.yaw)
            delta = abs(cur[0] - self.stable_sample[0]) + abs(cur[1] - self.stable_sample[1]) + abs(cur[2] - self.stable_sample[2])
            self.stable_sample = cur
            if self.stable_update:
                # 对齐锁定,稳定角度用缓动间隔向目标角度逼近
                k = 1.0 - math.exp(-self.stable_step_timeout / self.stable_update_timeout)
                # 三轴分别平滑对齐(带 ±180 环绕处理)
                diff = self.stable_target[0] - self.stable_roll
                if diff > 180:
                    diff -= 360
                elif diff < -180:
                    diff += 360
                self.stable_roll += diff * k
                diff = self.stable_target[1] - self.stable_pitch
                if diff > 180:
                    diff -= 360
                elif diff < -180:
                    diff += 360
                self.stable_pitch += diff * k
                diff = self.stable_target[2] - self.stable_yaw
                if diff > 180:
                    diff -= 360
                elif diff < -180:
                    diff += 360
                self.stable_yaw += diff * k
                # 收敛到目标角度附近结束锁定
                if (abs(self.stable_roll - self.stable_target[0]) < self.stable_threshold and
                        abs(self.stable_pitch - self.stable_target[1]) < self.stable_threshold and
                        abs(self.stable_yaw - self.stable_target[2]) < self.stable_threshold):
                    self.stable_update = False
            else:
                # 角度波动低于阈值 设备趋于稳定累加稳定确认计时
                if delta < self.stable_threshold:
                    self.stable_timer += self.stable_step_timeout
                    # 大于稳定确认定时 锁定当前角为目标基准角,开始缓动对齐基准
                    if self.stable_timer >= self.stable_timeout:
                        self.stable_target = cur
                        self.stable_timer = 0.0
                        self.stable_update = True
                else:
                    # 仍在晃动 清零计时
                    self.stable_timer = 0.0
        # 返回偏移量:归一化后按屏幕尺寸还原
        def update(self):
            # 记录使用时间戳(本帧有组件调用);若监听已注销则按需重新申请
            self.sensor_update_time = time.time()
            if not self.sensor_update:
                self._register()
            if not self.sensor_get:
                return 0.0,0.0,0.0
            # 尚未收到首帧传感器数据:直接返回零偏移,避免用 0 基准计算造成初始跳变
            if not self.sensor_init:
                return 0.0,0.0,0.0
            # 基准长时间跟随稳定角更新(有新的稳定角就同步基准);新组件首帧也会触发 update_base
            self.update_base()
            # 相对基准的角度差(三轴:roll→x,pitch→y,yaw→z)
            roll_diff = self.roll - self.base_roll
            pitch_diff = self.pitch - self.base_pitch
            yaw_diff = self.yaw - self.base_yaw
            # 处理 roll/yaw 的360度跳变问题(pitch 范围 ±90 无需处理)
            if roll_diff > 180:
                roll_diff -= 360
            elif roll_diff < -180:
                roll_diff += 360
            if yaw_diff > 180:
                yaw_diff -= 360
            elif yaw_diff < -180:
                yaw_diff += 360
            # 死区/阈值过滤:各轴死区 = 量程 × 比例(按轴归一化)
            roll_diff = self._check_deadzone(roll_diff, self.max_roll * self.deadzone)
            pitch_diff = self._check_deadzone(pitch_diff, self.max_pitch * self.deadzone)
            yaw_diff = self._check_deadzone(yaw_diff, self.max_yaw * self.deadzone)
            # 归一化:各轴独立量程(度),量程越小该轴越灵敏;偏差达量程即满屏半偏移
            norm_roll = max(-1.0,min(1.0,roll_diff / self.max_roll))
            norm_pitch = max(-1.0,min(1.0,pitch_diff / self.max_pitch))
            norm_yaw = max(-1.0,min(1.0,yaw_diff / self.max_yaw))
            # 通过屏幕尺寸还原成像素偏移量:以 roll→X 为基准(用宽),pitch→Y 与旋转 yaw→Z 用高同比
            # 满量程(max_roll/max_pitch/max_yaw 度)即对应半屏位移
            tilt_x = norm_roll * config.screen_width * 0.5
            tilt_y = norm_pitch * config.screen_height * 0.5
            tilt_z = norm_yaw * config.screen_height * 0.5
            return tilt_x,tilt_y,tilt_z
        # 更新基准角:持续跟随稳定角
        def update_base(self):
            if not self.sensor_get:
                return
            if self.stable_update:
                self.base_roll = self.stable_roll
                self.base_pitch = self.stable_pitch
                self.base_yaw = self.stable_yaw
     
    # 全局单例,同时作为是否能够调用的判断
    ANDROID_SENSOR = None
    if renpy.variant("android"):
        # jnius 仅 Android 可用,iOS 无传感器支持则保持 None
        ANDROID_SENSOR = AndroidSensor()
        # 传感器可用才注册退出注销回调;初始化失败保持 None(从未注册,无需注销)
        if ANDROID_SENSOR.sensor_get:
            config.quit_callbacks.append(ANDROID_SENSOR.stop_sensors)
        else:
            ANDROID_SENSOR = None

# 使用方法:
#
# 调用 ANDROID_SENSOR.update() 会返回水平仪映射在XYZ三轴上的三个量


#点击头像 查看我写的更多屎
粉身碎骨浑不怕,要留答辩在人间


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

相关侵权、举报、投诉及建议等,欢迎加入QQ交流群:1028986456

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.|苏ICP备17067825号

在本版发帖返回顶部