找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 435|回复: 4

[教程] 视差组件(用在主界面提升时髦值/安卓可用)

[复制链接]
发表于 2025-9-12 03:30:44 | 显示全部楼层 |阅读模式

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

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

×
本帖最后由 Maz马 于 2025-9-13 13:27 编辑

视差组件是一个非常能提升时髦值的东西
无论是用在主界面或是做一些伪3D场景

安卓方案由blurred设计,在正文下方扩展

嗯...实际上这个东西并不是我写的
没找到在哪摸来的...但总之是开源的,而且我对其进行了一些关键的优化,两者作用下因此没有投转载

另外,来自版主的,关于视差的另一个帖子:

用bar制作简单移动视差效果(renpy进阶学习经验五)
https://www.renpy.cn/forum.php?mod=viewthread&tid=1557
(版主的可以看预览,所以我不贴预览图了)

优化的地方我认为可以水点字数(
1.实际就是一个图片跟随鼠标移动功能,原代码
  为了反向移动居然复制了一份,然后就只改了计算...我直接提成参数了

2.原代码没有注意到CDD的渲染锚点是固定在左上角的(0,0)
  pygame取得鼠标的值的计算也是以(0,0)
  而大多数视差的对齐锚点是需要在屏幕中心的,这会导致视差的效果出现不理想的偏移

  锚点其实有两个,但很容易忽略掉其中一个
  第一个是屏幕锚点,通过鼠标取得偏移值
  第二个是图片锚点,图片移动偏移值(一般也是屏幕中心,但这里我写的是图片中心,因为我的虚拟摇杆是这样写的(bushi)

也许有时间我会看看怎么把锚点提取成参数,方便更多需求吧,但暂时就中心偏移了
[RenPy] 纯文本查看 复制代码
### 跟随鼠标移动的视差 ###################################################################
init python:
    import pygame

    # 获取显示对象的实际尺寸,如果无法获取则返回默认尺寸
    def get_displayable_size(displayable, default_size):
        if displayable is None:
            return default_size
        try:
            render = renpy.render(displayable, 0, 0, 0, 0)
            size = render.get_size()
            return size if size[0] > 0 and size[1] > 0 else default_size
        except:
            return default_size

    class TrackCursor(renpy.Displayable):
        def __init__(self,child,paramod=1.0,**kwargs):
            super(TrackCursor, self).__init__()
            # 图片
            self.child = child
            # 移动参数:正数=同向移动,负数=反向移动,绝对值越大移动幅度越小
            self.paramod = paramod

            # 鼠标坐标
            self.mouse_x = 0
            self.mouse_y = 0
            # 渲染坐标
            self.x = 0
            self.y = 0
            # 时间轴
            self.old_time = 0
            self.delta_time = 0
            # 尺寸
            self.child_width = 0
            self.child_height = 0
            # 自适应渲染检查
            self.size_initialized = False
    
        # 事件方法
        def event(self, ev, x, y, st):
            if ev.type == pygame.MOUSEMOTION:
                # 计算鼠标相对于屏幕中心的偏移量
                center_x = config.screen_width / 2
                center_y = config.screen_height / 2
                offset_x = (x - center_x) / self.paramod
                offset_y = (y - center_y) / self.paramod

                self.mouse_x = offset_x
                self.mouse_y = offset_y
    
        # 渲染显示对象
        def render(self, width, height, st, at):
            # 首次渲染时初始化子对象尺寸
            if not self.size_initialized:
                child_render = renpy.render(self.child, width, height, st, at)
                self.child_width, self.child_height = child_render.get_size()
                self.size_initialized = True
            
            render = renpy.Render(width, height)
            
            # 计算渲染间隔
            if self.old_time == 0:
                self.old_time = st
            self.delta_time = st - self.old_time
            self.old_time = st
            
            # 缓动公式
            self.x += (self.mouse_x - self.x) * 1.5 * self.delta_time * abs(self.paramod)
            self.y += (self.mouse_y - self.y) * 1.5 * self.delta_time * abs(self.paramod)
            
            # 计算最终绘制位置:图片中心 + 偏移量
            draw_x = (width - self.child_width) / 2 + self.x
            draw_y = (height - self.child_height) / 2 + self.y
            
            obj_render = renpy.render(self.child, width, height, st, at)
            render.blit(obj_render, (draw_x, draw_y))
            renpy.redraw(self, 0)
            return render

#使用方法:
#两个入参,(图片和偏移系数)
#图片得传入renpy.displayable("xxx")
#偏移系数不能为0,大致来说越靠近0,偏移程度越大。正值跟随鼠标移动,负值反向移动。20-60是一个比较舒适的范围。
#示例:
#image a = TrackCursor(renpy.displayable("background.png"), paramod=-20)
#default b = TrackCursor(renpy.displayable("background.png"), paramod=20)

另外,来自群友@blurred 设计的,应用手机传感器根据偏向角的视差(就是安卓没有鼠标,用陀螺仪来检测)
我这里也代为贴上了,由于各个设备不同,因此安卓的偏转参数需要自己测试调整(会自行检测当前设备是PC还是安卓)
[RenPy] 纯文本查看 复制代码
init python:
    import pygame
    import math
    
    # 倾角传感器管理器
    class TiltSensorManager:
        def __init__(self):
            self.pitch = 0.0  # 俯仰角(前后翻动)
            self.roll = 0.0   # 横滚角(左右倾斜)
            self.yaw = 0.0    # 偏航角(左右转动)
            self.initialized = False
            self.sensor_listener = None
            self.sensor_manager = None
            self.accel_values = [0, 0, 0]  # 加速度传感器
            self.magnetic_values = [0, 0, 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.accelerometer = self.sensor_manager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
                self.magnetometer = self.sensor_manager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)    
                if self.accelerometer is None or self.magnetometer is None:
                    print("设备缺少必要的传感器")
                    return
                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):
                        # 根据传感器类型存储数据
                        if event.sensor.getType() == Sensor.TYPE_ACCELEROMETER:
                            self.manager.accel_values = [event.values[0], event.values[1], event.values[2]]
                        elif event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD:
                            self.manager.magnetic_values = [event.values[0], event.values[1], event.values[2]]
                        # 计算倾角
                        self.manager.calculate_orientation()
                    @jnius.java_method('(Landroid/hardware/Sensor;I)V')
                    def onAccuracyChanged(self, sensor, accuracy):
                        pass
                self.sensor_listener = SensorListener(self)
                self.sensor_manager.registerListener(
                    self.sensor_listener,
                    self.accelerometer,
                    SensorManager.SENSOR_DELAY_GAME
                )
                self.sensor_manager.registerListener(
                    self.sensor_listener,
                    self.magnetometer,
                    SensorManager.SENSOR_DELAY_GAME
                )
                self.initialized = True
                print("倾角传感器初始化成功")
                
            except Exception as e:
                print(f"无法初始化倾角传感器: {e}")
                self.initialized = False
        
        def calculate_orientation(self):
            """根据加速度和地磁数据计算设备方向"""
            try:
                import jnius
                SensorManager = jnius.autoclass('android.hardware.SensorManager')
                # 计算旋转矩阵
                R = [0] * 9
                I = [0] * 9
                success = SensorManager.getRotationMatrix(R, I, self.accel_values, self.magnetic_values)
                if success:
                    # 获方向数据
                    orientation = [0] * 3
                    SensorManager.getOrientation(R, orientation)
                    # 转换为角都(弧度转角度)
                    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 stop(self):
            if self.sensor_manager and self.sensor_listener:
                try:
                    self.sensor_manager.unregisterListener(self.sensor_listener)
                except:
                    pass
    
    # 创建全局倾角传感器管理器
    tilt_sensor_manager = None
    if renpy.variant("mobile") and not (renpy.variant("pc") or renpy.variant("web")):
        tilt_sensor_manager = TiltSensorManager()
    
    # 老马出品的视差
    class TrackCursor(renpy.Displayable):
        def __init__(self, child, paramod=1.0, sensitivity=0.5, threshold=5.0, 
                    flip_sensitivity=1.0, dead_zone=10.0, **kwargs):
            super(TrackCursor, self).__init__()
            # 图片
            self.child = child
            # 移动参数
            self.paramod = paramod
            # 灵敏度控制
            self.sensitivity = sensitivity * 1.5
            # 倾斜阈值(度),超过此值才开始移动
            self.threshold = threshold
            # 翻转灵敏度(控制翻转方向的影响程度)
            self.flip_sensitivity = flip_sensitivity
            # 死区角度(度),在此范围内不响应翻转
            self.dead_zone = dead_zone
            
            # 输入数据
            self.input_x = 0
            self.input_y = 0
            # 渲染坐标
            self.x = 0
            self.y = 0
            # 时间轴
            self.old_time = 0
            self.delta_time = 0
            # 尺寸
            self.child_width = 0
            self.child_height = 0
            # 自适应渲染检查
            self.size_initialized = False
            
            # 判断平台
            self.is_mobile = renpy.variant("mobile") and not (renpy.variant("pc") or renpy.variant("web"))
            
            # 基准角度(初始位置)
            self.base_yaw = 0  
            self.base_pitch = 0 
            self.calibrated = False
            
            # 翻转状态跟踪
            self.last_flip_direction = 0  # 0: 未翻转, 1: 正向翻转, -1: 反向翻转
        
        def event(self, ev, x, y, st):
            if not self.is_mobile and ev.type == pygame.MOUSEMOTION:
                # 计算鼠标相对于屏幕中心的偏移量
                center_x = config.screen_width / 150
                center_y = config.screen_height / 5.5
                offset_x = (x - center_x) / self.paramod
                offset_y = (y - center_y) / self.paramod
                
                self.input_x = offset_x
                self.input_y = offset_y
            elif ev.type == pygame.MOUSEBUTTONDOWN and self.is_mobile:
                self.calibrate_sensors()
        
        def calibrate_sensors(self):
            """校准传感器,设置当前姿态为基准"""
            if tilt_sensor_manager and tilt_sensor_manager.initialized:
                self.base_yaw = tilt_sensor_manager.yaw
                self.base_pitch = tilt_sensor_manager.pitch
                self.calibrated = True
                print(f"传感器已校准: yaw={self.base_yaw}, pitch={self.base_pitch}")
        
        def calculate_flip_direction(self, angle_diff):
            """计算翻转方向并应用死区"""
            if abs(angle_diff) < self.dead_zone:
                return 0  # 在死区内,不响应
            
            # 计算翻转方向(1: 正向, -1: 反向)
            direction = 1 if angle_diff > 0 else -1
            
            # 如果翻转方向改变,应用翻转灵敏度
            if direction != self.last_flip_direction:
                self.last_flip_direction = direction
                return angle_diff * self.flip_sensitivity
            
            return angle_diff
        
        # 渲染显示对象
        def render(self, width, height, st, at):
            # 首次渲染时初始化子对象尺寸
            if not self.size_initialized:
                child_render = renpy.render(self.child, width, height, st, at)
                self.child_width, self.child_height = child_render.get_size()
                self.size_initialized = True
            
            render = renpy.Render(width, height)
            
            # 计算渲染间隔
            if self.old_time == 0:
                self.old_time = st
            self.delta_time = st - self.old_time
            self.old_time = st
            
            # 根据平台获取输入数据
            if self.is_mobile and tilt_sensor_manager and tilt_sensor_manager.initialized:
                if not self.calibrated:
                    self.calibrate_sensors()
                
                # 计算相对于基准的角度差异
                yaw_diff = tilt_sensor_manager.yaw - self.base_yaw
                pitch_diff = tilt_sensor_manager.pitch - self.base_pitch
                
                # 处理偏航角(左右转动)的360度跳变问题
                if yaw_diff > 180:
                    yaw_diff -= 360
                elif yaw_diff < -180:
                    yaw_diff += 360
                
                # 应用翻转方向检测和死区
                yaw_diff = self.calculate_flip_direction(yaw_diff)
                pitch_diff = self.calculate_flip_direction(pitch_diff)
                
                if abs(yaw_diff) > self.threshold:
                    # 偏航角(左右转动)控制左右移动
                    self.input_x = yaw_diff * self.sensitivity
                else:
                    self.input_x = 0

                if abs(pitch_diff) > self.threshold:
                    # 俯仰角(前后翻动)控制上下移动
                    self.input_y = pitch_diff * self.sensitivity
                else:
                    self.input_y = 0
                    
            elif not self.is_mobile:
                pass
            else:
                self.input_x = 0
                self.input_y = 0
            
            # 缓动公式
            self.x += (self.input_x - self.x) * 1.5 * self.delta_time * abs(self.paramod)
            self.y += (self.input_y - self.y) * 1.5 * self.delta_time * abs(self.paramod)
            
            draw_x = (width - self.child_width) / 2 + self.x
            draw_y = (height - self.child_height) / 2 + self.y        
            obj_render = renpy.render(self.child, width, height, st, at)
            render.blit(obj_render, (draw_x, draw_y))
            renpy.redraw(self, 0)
            return render

# image sky = TrackCursor(renpy.displayable("sky.png"), paramod=-25, sensitivity=1.0, flip_sensitivity=1.5, dead_zone=5.0)
# image mount = TrackCursor(renpy.displayable("mount.png"), paramod=-15, sensitivity=1.0, flip_sensitivity=1.2, dead_zone=8.0)

# paramod           # 移动幅度参数(PC)
# sensitivity       # 基本灵敏度
# threshold         # 倾斜阈值(度)超过这个角度才会发生偏转
# flip_sensitivity  # 翻转灵敏度
# dead_zone         # 死区角度(度)在这个角度范围内需要偏转threshold才会相应偏转,在这个范围外翻转任意角度都会偏转





评分

参与人数 1活力 +300 干货 +1 收起 理由
烈林凤 + 300 + 1 感谢分享!

查看全部评分

发表于 2025-9-12 17:29:21 | 显示全部楼层
之前那篇帖子已经是一年前写的了啊……时间过的真快(望天)
先前我所采用的方法主要就是为了避免使用过于复杂的python语句,尤其是要用到pygame库,更方便新手用renpy原生界面语句搓出自己想要的界面效果
这篇帖子所使用的方法显然利用率更高,用到了cdd和pygame库,确实更适合renpy糕手们去使用,但因为咱也不会使用cdd,所以只能用用老方法了(
回复 支持 1 抱歉 0

使用道具 举报

发表于 2025-9-27 03:19:17 | 显示全部楼层
烈林凤 发表于 2025-9-12 17:29
之前那篇帖子已经是一年前写的了啊……时间过的真快(望天)
先前我所采用的方法主要就是为了避免使用过于 ...

所以为什么不用DynamicDisplayable
回复 支持 抱歉

使用道具 举报

发表于 2025-9-27 12:47:21 | 显示全部楼层
Aaron栩生阿龙 发表于 2025-9-27 03:19
所以为什么不用DynamicDisplayable

因为当时不知道)
回复 支持 抱歉

使用道具 举报

发表于 2025-10-12 16:02:24 | 显示全部楼层
烈林凤 发表于 2025-9-27 12:47
因为当时不知道)

文档抄一百遍下次课上课检查
回复 支持 抱歉

使用道具 举报

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

本版积分规则

小黑屋|手机版|RenPy中文空间 ( 苏ICP备17067825号|苏公网安备 32092302000068号 )

GMT+8, 2025-10-29 22:19 , Processed in 0.054494 second(s), 27 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表