找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 97|回复: 2

[经验] 暴学CDD/附虚拟摇杆

[复制链接]
发表于 2025-4-9 16:27:24 | 显示全部楼层 |阅读模式

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

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

×
本帖最后由 leech 于 2025-4-14 21:52 编辑

叠甲
首先,我的编程基础为0,以下为纯摸索经验,如果有不对的地方望轻喷
基于个人需求写的东西
拉都拉了,就把这盘九转大肠捧出来给大家品鉴一下

事情的起因是这样的,我一直想做ACT游戏,奈何水平有限,不会py,用screen硬堆了一个角色移动系统

####最新的虚拟摇杆请查阅楼下。####

                               
登录/注册后可看大图


【屎山代码插件】简单rpg地图移动
https://www.renpy.cn/forum.php?mod=viewthread&tid=1652
(出处: RenPy中文空间)

然后因为当时群里讨论剧烈,大佬发布了他的码

如何实现按键控制人物行走(CDD实现)
https://www.renpy.cn/forum.php?mod=viewthread&tid=1654
(出处: RenPy中文空间)

因为这篇我认为还是挺亲我这种没有基础的愚民的,让我看到了“啊!我好像也不是学不会”的希望,然后我就开始暴学了cdd
以下是经验分享:
首先先说0基础的方面,曾经也有大佬推荐我看py文档,奈何基础很烂,真的一大半都看不懂,就在我半夜痛哭流涕刷小说的时候
我发现了有一本书“看漫画学PY”,书中言语尽是幼儿园水平,我反复看了两天,终于理解了“面向对象”“类”“函数”的基础概念
然后我就开始试图着手修改大佬的行走CDD顺带学习,让它成为我的样子
能稍微看懂函数和类之后,修改并不困难,因为我只是想把平面行走更改成横板而已
以下是理解:
[RenPy] 纯文本查看 复制代码
#定义一只狗,“类”型为狗(cdd)
default bigdog = Dog()

#这个是起手
init python:
    #这个是你创建的CDD,继承于renpy.Displayable(也就是留给你的接口)
    class Dog(renpy.Displayable):
        def __init__(self,**kwards):#这里填你传进来的参数
            super().__init__(**kwards)
            ########
            #这里填你要用到的量
            self.abc = 123
            ########
        
        #这个是渲染器》图像显示
        def render(self,width,height,st,at):
            render = renpy.Render(width,height)
            return render
        
        #这个是控制器》接收交互(键盘,点击,拖拽之类)
        def event(self,ev,x,y,st):
            pass

        #这里是其他随你写的函数什么的(数值计算什么的,反正就是调用来调用去)
        #def xxx()

然后,这只狗就是像 image 一样随你到处贴的可视组件了
在我更改大佬的行走CDD之后,我想要写一个虚拟摇杆来传递方向给这个行走的CDD
因为安卓端并没有键盘可以按,于是我写好了(在最下方我会放出)
但是在向行走CDD传递方向时,出现了摇杆无法交互的问题,然后我就开始往群里抱大腿
在热心群友@Koji 的帮助下发现了问题出在这一条里
[RenPy] 纯文本查看 复制代码
renpy.restart_interaction()

这一条函数的作用是刷新屏幕,但他的副作用是会重置init(不太记得是不是这么说)而且拦截鼠标
如果想要刷新界面应该用
[RenPy] 纯文本查看 复制代码
renpy.redraw(self,0)

这一条是只刷新可视组件
我进行了修改,然后新问题出现,使用这一条会出现坐标乱飞,丢帧的情况
我开始重构,放弃原有的代码,在好几天东抄西抄,轮流折磨G小姐D小姐之后
我得到了一个结构
[RenPy] 纯文本查看 复制代码
#定义一只狗,“类”型为狗(cdd)
default bigdog = Dog()

#这个是起手
init python:
    #这个是你创建的CDD,继承于renpy.Displayable(也就是留给你的接口)
    class Dog(renpy.Displayable):
        def __init__(self,**kwards):#这里填你传进来的参数
            super().__init__(**kwards)
            ########
            #这里填你要用到的量
            self.abc = 123
            ########
        
        #这个是渲染器》图像显示
        def render(self,width,height,st,at):
            render = renpy.Render(width,height)
            ###################################<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            #                                 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            #  需要实时渲染的动画或函数调用      #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            #                                 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            ###################################<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            #⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
            renpy.redraw(self,0)#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
            return render
        
        #这个是控制器》接收交互(键盘,点击,拖拽之类)
        def event(self,ev,x,y,st):
            pass

        #这里是其他随你写的函数什么的(数值计算什么的,反正就是调用来调用去)
        #def xxx()

大概就是这样子,把所有需要实时渲染的动画,数值更新的函数调用放在“渲染器”或“控制器”下,最后再让它每帧重绘可视组件
成功的让它不会丢帧,坐标在后台乱飞(个人建议是复杂计算的放在”控制器“简单的放在”渲染器“)
在尝试好几次后,个人认为renpy.redraw(self,0)放在“渲染器”是最好的,因为它似乎是“终点”
另外,关于实时动画的思路我是参照了“行走CDD”的码
使用帧间隔来计算,这样子不会因为设备FPS不同而导致实时动画产生时间差或后台的坐标乱飞
然后,熬了好几天试错修改,我已经把我的ACT框架写好了,成就感挺高的,所以写了个贴,如果有谬误,也请指正

最后是附上虚拟摇杆的CDD
[RenPy] 纯文本查看 复制代码
init python:
    # 导入数学库和pygame库
    import math
    import pygame

    # 定义虚拟摇杆类,继承自Ren'Py的可显示对象
    class VirtualJoystick(renpy.Displayable):
        # 初始化方法,设置摇杆参数
        def __init__(self, size=200, thumb_size=50, **kwargs):
            super(VirtualJoystick, self).__init__(**kwargs)  # 调用父类初始化
            
            # 摇杆参数设置
            self.size = size  # 摇杆背景圆盘的直径
            self.thumb_size = thumb_size  # 摇杆手柄(拇指按钮)的直径
            self.radius = (size - thumb_size) // 2  # 摇杆可移动的最大半径
            
            # 状态变量初始化
            self.dragging = False  # 是否正在拖拽摇杆
            self.offset = (0, 0)  # 拖拽时的鼠标偏移量
            self.position = (0, 0)  # 摇杆当前位置偏移量
            self.direction = (0, 0)  # 8方向向量(用于8方向移动控制)
            
            # 创建摇杆的视觉元素
            self.background = Solid("#66666666", xsize=size, ysize=size)  # 半透明灰色背景
            self.thumb = Solid("#ffffffcc", xsize=thumb_size, ysize=thumb_size)  # 半透明白色手柄
        
        # 渲染方法,绘制摇杆到屏幕
        def render(self, width, height, st, at):
            # 创建渲染对象
            render = renpy.Render(self.size, self.size)
            
            # 渲染背景圆盘
            background_render = self.background.render(self.size, self.size, st, at)
            render.blit(background_render, (0, 0))
            
            # 计算摇杆手柄的显示位置(中心位置+当前偏移)
            thumb_x = (self.size - self.thumb_size) // 2 + self.position[0]
            thumb_y = (self.size - self.thumb_size) // 2 + self.position[1]
            
            # 渲染摇杆手柄
            thumb_render = self.thumb.render(self.thumb_size, self.thumb_size, st, at)
            render.blit(thumb_render, (thumb_x, thumb_y))
            renpy.redraw(self, 0)  # 请求重绘
            return render
        
        # 事件处理方法,处理鼠标交互
        def event(self, ev, x, y, st):
            # 计算摇杆中心坐标
            center_x = self.size // 2
            center_y = self.size // 2
            # 计算相对中心的位置
            rel_x = x - center_x
            rel_y = y - center_y
            
            # 计算手柄当前显示位置
            thumb_x = (self.size - self.thumb_size) // 2 + self.position[0]
            thumb_y = (self.size - self.thumb_size) // 2 + self.position[1]
            
            # 鼠标左键按下事件处理
            if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
                # 检测点击是否在手柄范围内(修改点1)
                if (thumb_x <= x <= thumb_x + self.thumb_size and 
                    thumb_y <= y <= thumb_y + self.thumb_size):
                    self.dragging = True
                    # 计算鼠标与手柄的偏移量(修改点2)
                    self.offset = (x - thumb_x, y - thumb_y)
                    
                    return None  # 事件已处理
            
            # 鼠标左键释放事件处理
            elif ev.type == pygame.MOUSEBUTTONUP and ev.button == 1:
                self.dragging = False  # 结束拖拽
                self.position = (0, 0)  # 重置位置
                self.direction = (0, 0)  # 重置方向
                return None  # 事件已处理
            
            # 鼠标移动事件处理(拖拽中)
            elif ev.type == pygame.MOUSEMOTION and self.dragging:
                # 计算拖拽位置(修改点3:使用绝对坐标)
                drag_x = x - center_x - self.offset[0]
                drag_y = y - center_y - self.offset[1]
                
                # 计算距离并限制在最大半径内
                distance = (drag_x**2 + drag_y**2) ** 0.5
                if distance > self.radius:
                    drag_x = drag_x * self.radius / distance
                    drag_y = drag_y * self.radius / distance
                
                self.position = (drag_x, drag_y)  # 更新位置
                
                # 8方向判定(当距离超过半径20%时生效)
                if distance > self.radius * 0.2:
                    # 计算角度(0-360度)
                    angle = math.degrees(math.atan2(-drag_y, drag_x))
                    angle = (angle + 360) % 360
                    
                    # 8方向判定
                    if 22.5 <= angle < 67.5:  # 右上
                        self.direction = (1, -1)
                    elif 67.5 <= angle < 112.5:  # 上
                        self.direction = (0, -1)
                    elif 112.5 <= angle < 157.5:  # 左上
                        self.direction = (-1, -1)
                    elif 157.5 <= angle < 202.5:  # 左
                        self.direction = (-1, 0)
                    elif 202.5 <= angle < 247.5:  # 左下
                        self.direction = (-1, 1)
                    elif 247.5 <= angle < 292.5:  # 下
                        self.direction = (0, 1)
                    elif 292.5 <= angle < 337.5:  # 右下
                        self.direction = (1, 1)
                    else:  # 右
                        self.direction = (1, 0)
                else:  # 小幅度移动视为无方向
                    self.direction = (0, 0)
                return None  # 事件已处理
            
            return None  # 事件未处理
        
        def get_direction(self):
            #返回当前8方向向量返回值: 元组(x,y) 例如:(0,0)无方向,(0,-1)上,(1,0)右,(1,1)右下等
            return self.direction
        
        def get_normalized_position(self):
            #返回标准化后的摇杆位置返回值: 元组(x,y) 范围-1到1(-1,-1)表示左下,(1,1)表示右上等
            if self.radius > 0:
                return (self.position[0] / self.radius, self.position[1] / self.radius)
            return (0, 0)  # 默认返回无方向


default joy = VirtualJoystick()
# 在屏幕上显示摇杆
screen virtual_joystick():
    
    # 添加摇杆到界面
    add joy align (0.1, 0.8)
    
    # 显示当前方向(调试用)
    text "方向: [joystick.direction]" align (0.1, 0.7)
    text "标准化位置: [joystick.get_normalized_position()]" align (0.1, 0.75)

label start2:
    call screen virtual_joystick
    $ direction = joystick.direction
    "你摇动的方向是: [direction]"

    return



#查看我写的更多屎


【经验】暴学CDD的十四天/附虚拟摇杆
https://www.renpy.cn/forum.php?mod=viewthread&tid=1675

【屎山代码插件】简单rpg地图移动
https://www.renpy.cn/forum.php?mod=viewthread&tid=1652

【屎山代码插件】摇骰子小游戏
https://www.renpy.cn/forum.php?mod=viewthread&tid=1653

【屎山代码插件】猜球盅小游戏
https://www.renpy.cn/forum.php?mod=viewthread&tid=1574


粉身碎骨浑不怕,要留答辩在人间









评分

参与人数 3活力 +300 干货 +5 收起 理由
被诅咒的章鱼 + 300 + 3 鼓励拉屎!
烈林凤 + 1 加油!
ZYKsslm + 1 感谢分享!

查看全部评分

发表于 7 天前 | 显示全部楼层
最近我也在学习CDD,你可以直接在群里找到我QQ,交流。
回复 支持 1 抱歉 0

使用道具 举报

 楼主| 发表于 6 天前 | 显示全部楼层
本帖最后由 leech 于 2025-4-12 14:23 编辑

在koji的思路建议下,更新了一下思路
把键盘输入也整合到里面,成为一个完整输入系统,在调用时不用分别去判断输入系统,不过现在只支持wasd-qf六个输入,(街机的上下左右AB)
不过根据组合键最终输出的结果也有一定数量了,另外使用字典存储...也就是支持改键,可以直接改默认的字典,也可以用预留的参数传入
也可以让玩家自定义,不过自定义的界面就要自写了

另外写了素材的自适应,只要把摇杆背景图,摇杆,按键a,按键b传进去就行,不传的话就会使用默认的方块..不过布局位置还是要手动传入的。
[RenPy] 纯文本查看 复制代码
#移除原生相应的快捷键
init python:
    config.keymap["director"].remove("noshift_K_d")
    config.keymap["screenshot"].remove("noshift_K_s")
    config.keymap["toggle_fullscreen"].remove("noshift_K_f")
# 初始化Python代码块(在Ren'Py初始化阶段执行)
init python:
    import math
    import pygame

    class VirtualJoystick(renpy.Displayable):

        # 将字符串按键映射到pygame常量
        DEFAULT_KEYMAP = {
            "w": pygame.K_w,
            "a": pygame.K_a,
            "s": pygame.K_s,
            "d": pygame.K_d,
            "q": pygame.K_q,
            "f": pygame.K_f,
        }
        def __init__(self,joy_bg=None,joy_fg=None,q_button_img=None,f_button_img=None,joypos=(100,700),qpos=(1500,800),fpos=(1600,800),keymap=None,**kwargs):
            super(VirtualJoystick, self).__init__(**kwargs)

            # 摇杆外观
            self.joy_bg = joy_bg or Solid("#66666666", xysize=(200,200))
            self.joy_fg = joy_fg or Solid("#ffffffcc", xysize=(50,50))
            self.q_button_normal = q_button_img or Solid("#666666cc", xysize=(70,70))
            self.f_button_normal = f_button_img or Solid("#666666cc", xysize=(70,70))
            self.q_button_pressed = Transform(self.q_button_normal,matrixcolor=BrightnessMatrix(-0.1))
            self.f_button_pressed = Transform(self.f_button_normal,matrixcolor=BrightnessMatrix(-0.1))

            # 尺寸设置
            self.joy_bg_size = 200
            self.joy_fg_size = 50
            self.joy_pos = joypos
            self.q_button_size = 70
            self.f_button_size = 70
            self.q_button_pos = qpos
            self.f_button_pos = fpos
            self.radius = (self.joy_bg_size - self.joy_fg_size) // 2
            self.deadzone_radius = self.radius * 0.2
            
            # 状态变量
            self.dragging = False
            self.position = (0, 0)
            self.offset = (0, 0)
            
            # 键位配置
            self.keymap = keymap or self.DEFAULT_KEYMAP.copy()
            
            # 初始化按键状态(使用逻辑键名)
            self.j_keys = {k: 0 for k in self.DEFAULT_KEYMAP.keys()}
            self.q_button = False
            self.f_button = False

            #自适应渲染检查
            self.sss_joy_check = False

        # 动态获取素材尺寸,没有则使用默认尺寸
        def get_displayable_size(self, 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 (getattr(displayable, 'width', default_size[0]), getattr(displayable, 'height', default_size[1]))
        
        def render(self, width, height, st, at):

            # 首次渲染时检查尺寸
            if not self.sss_joy_check:

                # 重新获取尺寸
                self.joy_bg_size = self.get_displayable_size(self.joy_bg, (200, 200))[0]
                self.joy_fg_size = self.get_displayable_size(self.joy_fg, (50, 50))[0]
                self.q_button_size = self.get_displayable_size(self.q_button_normal, (70, 70))[0]
                self.f_button_size = self.get_displayable_size(self.f_button_normal, (70, 70))[0]
                
                # 更新相关计算
                self.radius = (self.joy_bg_size - self.joy_fg_size) // 2
                self.deadzone_radius = self.radius * 0.2
                
                self.sss_joy_check = True

            render = renpy.Render(width, height)

            joy_bg_render = self.joy_bg.render(self.joy_bg_size, self.joy_bg_size, st, at)
            render.blit(joy_bg_render, (self.joy_pos[0], self.joy_pos[1]))
            
            joy_fg_x = self.joy_pos[0] + (self.joy_bg_size - self.joy_fg_size) // 2 + self.position[0]
            joy_fg_y = self.joy_pos[1] + (self.joy_bg_size - self.joy_fg_size) // 2 + self.position[1]
            joy_fg_render = self.joy_fg.render(self.joy_fg_size, self.joy_fg_size, st, at)
            render.blit(joy_fg_render, (joy_fg_x, joy_fg_y))

            # 渲染Q按钮
            q_button = self.q_button_pressed if self.q_button else self.q_button_normal
            q_render = q_button.render(self.q_button_size, self.q_button_size, st, at)
            render.blit(q_render, (self.q_button_pos[0], self.q_button_pos[1]))

            # 渲染F按钮
            f_button = self.f_button_pressed if self.f_button else self.f_button_normal
            f_render = f_button.render(self.f_button_size, self.f_button_size, st, at)
            render.blit(f_render, (self.f_button_pos[0], self.f_button_pos[1]))
            
            #调试说明
            if config.developer:
                debug = Text("W: [joy.j_keys['w']] A: [joy.j_keys['a']] S: [joy.j_keys['s']] D: [joy.j_keys['d']] Q: [joy.j_keys['q']] F: [joy.j_keys['f']]")
                debug_render = renpy.render(debug, width, height, st, at)
                render.blit(debug_render, (0, 0))

            renpy.redraw(self, 0)
            return render
        
        def event(self, ev, x, y, st):
            # 按钮位置
            q_button_rect = (self.q_button_pos[0], self.q_button_pos[1], self.q_button_size, self.q_button_size)
            f_button_rect = (self.f_button_pos[0], self.f_button_pos[1], self.f_button_size, self.f_button_size)

            center_x = self.joy_pos[0] + self.joy_bg_size // 2
            center_y = self.joy_pos[1] + self.joy_bg_size // 2
            distance = ((x - center_x)**2 + (y - center_y)**2) ** 0.5
            
            # 处理鼠标事件
            if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
                # 点击Q按钮
                if (self.q_button_pos[0] <= x < self.q_button_pos[0] + self.q_button_size and 
                    self.q_button_pos[1] <= y < self.q_button_pos[1] + self.q_button_size):
                    self.q_button = True
                    self.j_keys["q"] = 1
                
                # 点击F按钮
                if (self.f_button_pos[0] <= x < self.f_button_pos[0] + self.f_button_size and 
                    self.f_button_pos[1] <= y < self.f_button_pos[1] + self.f_button_size):
                    self.f_button = True
                    self.j_keys["f"] = 1
                # 点击摇杆
                if distance <= self.radius + self.joy_fg_size//2:
                    self.dragging = True
                    self.offset = (x - center_x, y - center_y)
                return None
            
            elif ev.type == pygame.MOUSEBUTTONUP and ev.button == 1:
                # 释放Q按钮
                if self.q_button:
                    self.q_button = False
                    self.j_keys["q"] = 0
                # 释放F按钮
                if self.f_button:
                    self.f_button = False
                    self.j_keys["f"] = 0
                # 释放摇杆
                if self.dragging:
                    self.dragging = False
                    self.position = (0, 0)
                    self.offset = (0, 0) 
                    # 重置方向键状态(使用字符串键名)
                    for key in ["w", "a", "s", "d"]:
                        self.j_keys[key] = 0
                return None 
            # 拖拽摇杆
            elif ev.type == pygame.MOUSEMOTION and self.dragging:
                rel_x = x - center_x - self.offset[0]
                rel_y = y - center_y - self.offset[1]
                
                distance = (rel_x**2 + rel_y**2) ** 0.5
                if distance > self.radius:
                    rel_x = rel_x * self.radius / distance
                    rel_y = rel_y * self.radius / distance
                
                self.position = (rel_x, rel_y)
                
                # 先重置所有方向键状态
                for key in ["w", "a", "s", "d"]:
                    self.j_keys[key] = 0
                # 角度计算
                if distance > self.deadzone_radius:
                    angle = math.degrees(math.atan2(-rel_y, rel_x))
                    angle = (angle + 360) % 360
                    # 根据角度设置方向键
                    if 22.5 <= angle < 67.5:  # 右上
                        self.j_keys["w"] = 1
                        self.j_keys["d"] = 1
                    elif 67.5 <= angle < 112.5:  # 上
                        self.j_keys["w"] = 1
                    elif 112.5 <= angle < 157.5:  # 左上
                        self.j_keys["w"] = 1
                        self.j_keys["a"] = 1
                    elif 157.5 <= angle < 202.5:  # 左
                        self.j_keys["a"] = 1
                    elif 202.5 <= angle < 247.5:  # 左下
                        self.j_keys["a"] = 1
                        self.j_keys["s"] = 1
                    elif 247.5 <= angle < 292.5:  # 下
                        self.j_keys["s"] = 1
                    elif 292.5 <= angle < 337.5:  # 右下
                        self.j_keys["d"] = 1
                        self.j_keys["s"] = 1
                    else:  # 右
                        self.j_keys["d"] = 1
            # 当摇杆没有被拖拽时才检查键盘,避免数据过快刷新且重复覆盖
            if not self.dragging:
                # 键盘事件处理(统一使用键名字符串)
                keys = pygame.key.get_pressed()
                for logic_key, pygame_key in self.keymap.items():
                    self.j_keys[logic_key] = 1 if keys[pygame_key] else 0
            return None
        
        def get_joy_input(self):
            return self.j_keys


default joy = VirtualJoystick()
#一共7个参数,摇杆区域背景图片,摇杆图片,Q按键图片,F按键图片,摇杆整体坐标(左上角),Q按钮坐标(左上角),F按钮坐标(左上角)
#示例,图片得传入renpy.displayable("xxx")
#default joy = VirtualJoystick(
#    renpy.displayable("xxx"),
#    renpy.displayable("xxx"),
#    renpy.displayable("xxx"),
#    renpy.displayable("xxx"),
#    (x,y),
#    (x,y),
#    (x,y)
#)
#
# 返回的值是一个字典,我声明的摇杆是joy,所以返回的就是joy.j_keys = {"w":0,"a":0,"s":0,"d":0,"q":0,"f":0,},0是未被按下,1是被按下了

# 在屏幕上显示摇杆
screen virtual_joystick():
    # 添加摇杆到界面
    add joy

label start2:
    call screen virtual_joystick
    jump start2
回复 支持 抱歉

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-4-17 00:32 , Processed in 0.050931 second(s), 27 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

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