找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 585|回复: 2

[原创] 暴学CDD/附虚拟摇杆

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

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

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

×
本帖最后由 Maz马 于 2025-10-13 18:31 编辑

叠甲
首先,我的编程基础为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] 纯文本查看 复制代码
# BY MAZ ACTSYS

# 输入模块  适配安卓的虚拟摇杆及键盘控制

init python:
    #移除原生相应的快捷键
    config.keymap["director"].remove("noshift_K_d")
    config.keymap["screenshot"].remove("noshift_K_s")
    config.keymap["toggle_fullscreen"].remove("noshift_K_f")
    config.keymap["dismiss"].append("noshift_K_f")
    config.keymap["dismiss"].remove("K_SPACE")
    config.keymap["focus_left"].append("anyrepeat_K_a")
    config.keymap["focus_right"].append("anyrepeat_K_d")
    config.keymap["focus_up"].append("anyrepeat_K_w")
    config.keymap["focus_down"].append("anyrepeat_K_s")
    config.keymap["button_select"].append("noshift_K_f")

    import math
    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 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,vjoy_bg=None,vjoy_fg=None,button_01_img=None,button_02_img=None,vjoypos=(100,700),b01pos=(1500,800),b02pos=(1600,800),keymap=None,**kwargs):
            super(VirtualJoystick,self).__init__(**kwargs)

            # 摇杆外观
            self.vjoy_bg = vjoy_bg or Solid("#66666666",xysize=(200,200))
            self.vjoy_fg = vjoy_fg or Solid("#ffffffcc",xysize=(50,50))
            self.button_01_normal = button_01_img or Solid("#666666cc",xysize=(70,70))
            self.button_02_normal = button_02_img or Solid("#666666cc",xysize=(70,70))
            self.button_01_pressed = Transform(self.button_01_normal,matrixcolor=BrightnessMatrix(-0.1))
            self.button_02_pressed = Transform(self.button_02_normal,matrixcolor=BrightnessMatrix(-0.1))

            # 尺寸设置
            self.vjoy_bg_size = 200
            self.vjoy_fg_size = 50
            self.vjoy_pos = vjoypos

            self.button_01_size = 70
            self.button_01_pos = b01pos
            self.button_01_text = 0

            self.button_02_size = 70
            self.button_02_pos = b02pos
            self.button_02_text = 0

            self.radius = (self.vjoy_bg_size - self.vjoy_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.vjoy_keys = {k: 0 for k in self.DEFAULT_KEYMAP.keys()}
            self.button_01 = False
            self.button_02 = False

            #自适应渲染检查
            self.sss_vjoy_check = False
        
        def render(self,width,height,st,at):

            # 首次渲染时检查尺寸
            # 注意,这个尺寸并不影响图片的渲染,而是使用传入图片的尺寸作为摇杆的响应区域
            if not self.sss_vjoy_check:

                # 重新获取尺寸
                self.vjoy_bg_size = get_displayable_size(self.vjoy_bg,(200,200))[0]
                self.vjoy_fg_size = get_displayable_size(self.vjoy_fg,(50,50))[0]
                self.button_01_size = get_displayable_size(self.button_01_normal,(70,70))[0]
                self.button_02_size = get_displayable_size(self.button_02_normal,(70,70))[0]
                
                # 更新相关计算
                self.radius = (self.vjoy_bg_size - self.vjoy_fg_size) // 2
                self.deadzone_radius = self.radius * 0.2
                
                self.sss_vjoy_check = True

            render = renpy.Render(width,height)

            # 渲染背景
            vjoy_bg_render = renpy.render(self.vjoy_bg,width,height,st,at)
            render.blit(vjoy_bg_render,(self.vjoy_pos[0],self.vjoy_pos[1]))
            # 渲染摇杆
            vjoy_fg_render = renpy.render(self.vjoy_fg,width,height,st,at)
            # displayable.render是displayable自身的,所以他的width,height会影响图片的渲染情况
            # renpy.render是renpy的,并不会影响调用的displayable,而是划了一块区域给displayable用
            # 这个区域并不影响渲染,因为渲染由displayable自身的render决定
            #
            # displayable.render中width,height传入的是图像尺寸。是渲染函数
            # renpy.render中width,height是组件的尺寸。是类似布局系统
            # 这里的逻辑我还没有彻底理清楚,但最终需要通过render()函数返回一个Render()对象
            vjoy_fg_x = self.vjoy_pos[0] + (self.vjoy_bg_size - self.vjoy_fg_size) // 2 + self.position[0]
            vjoy_fg_y = self.vjoy_pos[1] + (self.vjoy_bg_size - self.vjoy_fg_size) // 2 + self.position[1]
            render.blit(vjoy_fg_render,(vjoy_fg_x,vjoy_fg_y))

            # 渲染Q按钮
            button_01 = self.button_01_pressed if self.button_01 else self.button_01_normal
            q_render = renpy.render(button_01,width,height,st,at)
            render.blit(q_render,(self.button_01_pos[0],self.button_01_pos[1]))

            # 渲染Q按钮文本
            if self.button_01_text >= 0:
                q_text_render = renpy.render(Text(str(format(self.button_01_text, '.2f'))),width,height,st,at)
                q_text_width, q_text_height = q_text_render.get_size()
                q_draw_x = self.button_01_pos[0] + (self.button_01_size - q_text_width) / 2
                q_draw_y = self.button_01_pos[1] + (self.button_01_size - q_text_height) / 2
                render.blit(q_text_render,(q_draw_x,q_draw_y))

            # 渲染F按钮
            button_02 = self.button_02_pressed if self.button_02 else self.button_02_normal
            f_render = renpy.render(button_02,width,height,st,at)
            render.blit(f_render,(self.button_02_pos[0],self.button_02_pos[1]))
            
            # 渲染Q按钮文本
            if self.button_02_text >= 0:
                f_text_render = renpy.render(Text(str(format(self.button_02_text, '.2f'))),width,height,st,at)
                f_text_width, f_text_height = f_text_render.get_size()
                f_draw_x = self.button_02_pos[0] + (self.button_02_size - f_text_width) / 2
                f_draw_y = self.button_02_pos[1] + (self.button_02_size - f_text_height) / 2
                render.blit(f_text_render,(f_draw_x,f_draw_y))

            #调试说明
            if config.developer:
                debug = Text("W: [vjoy.vjoy_keys['w']] A: [vjoy.vjoy_keys['a']] S: [vjoy.vjoy_keys['s']] D: [vjoy.vjoy_keys['d']] Q: [vjoy.vjoy_keys['q']] F: [vjoy.vjoy_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):
            # 按钮位置
            button_01_rect = (self.button_01_pos[0],self.button_01_pos[1],self.button_01_size,self.button_01_size)
            button_02_rect = (self.button_02_pos[0],self.button_02_pos[1],self.button_02_size,self.button_02_size)
            # 摇杆偏移位置
            center_x = self.vjoy_pos[0] + self.vjoy_bg_size // 2
            center_y = self.vjoy_pos[1] + self.vjoy_bg_size // 2
            distance = ((x - center_x)**2 + (y - center_y)**2) ** 0.5
            
            # 处理鼠标事件
            if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
                # 点击按钮01
                if (self.button_01_pos[0] <= x < self.button_01_pos[0] + self.button_01_size and 
                    self.button_01_pos[1] <= y < self.button_01_pos[1] + self.button_01_size):
                    self.button_01 = True
                    self.vjoy_keys["q"] = 1
                
                # 点击按钮02
                if (self.button_02_pos[0] <= x < self.button_02_pos[0] + self.button_02_size and 
                    self.button_02_pos[1] <= y < self.button_02_pos[1] + self.button_02_size):
                    self.button_02 = True
                    self.vjoy_keys["f"] = 1
                # 点击摇杆
                if distance <= self.radius + self.vjoy_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:
                # 释放按钮01
                if self.button_01:
                    self.button_01 = False
                    self.vjoy_keys["q"] = 0
                # 释放按钮02
                if self.button_02:
                    self.button_02 = False
                    self.vjoy_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.vjoy_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.vjoy_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.vjoy_keys["w"] = 1
                        self.vjoy_keys["d"] = 1
                    elif 67.5 <= angle < 112.5:  # 上
                        self.vjoy_keys["w"] = 1
                    elif 112.5 <= angle < 157.5:  # 左上
                        self.vjoy_keys["w"] = 1
                        self.vjoy_keys["a"] = 1
                    elif 157.5 <= angle < 202.5:  # 左
                        self.vjoy_keys["a"] = 1
                    elif 202.5 <= angle < 247.5:  # 左下
                        self.vjoy_keys["a"] = 1
                        self.vjoy_keys["s"] = 1
                    elif 247.5 <= angle < 292.5:  # 下
                        self.vjoy_keys["s"] = 1
                    elif 292.5 <= angle < 337.5:  # 右下
                        self.vjoy_keys["d"] = 1
                        self.vjoy_keys["s"] = 1
                    else:  # 右
                        self.vjoy_keys["d"] = 1
            # 当摇杆没有被拖拽时才检查键盘,避免数据过快刷新且重复覆盖
            if not self.dragging:
                # 键盘事件处理(统一使用键名字符串)
                keys = pygame.key.get_pressed()
                for logic_key,pygame_key in self.keymap.items():
                    self.vjoy_keys[logic_key] = 1 if keys[pygame_key] else 0
            return None
        
        # 最终得到一个字典,self.vjoy_keys = {"w":0,"a":0,"s":0,"d":0,"q":0,"f":0,},0是未被按下,1是被按下了


default vjoy = VirtualJoystick()
#一共9个参数,摇杆区域背景图片,摇杆图片,Q按键图片,F按键图片,摇杆整体坐标(左上角),Q按钮坐标(左上角),F按钮坐标(左上角)
#示例,图片得传入renpy.displayable("xxx")
#default vjoy = VirtualJoystick(
#    renpy.displayable("xxx"),
#    renpy.displayable("xxx"),
#    renpy.displayable("xxx"),
#    renpy.displayable("xxx"),
#    (x,y),
#    (x,y),
#    (x,y),
#    keymap= 一个对应按键列表,可自定义传入键位
#    callback= 一个回调函数,应传入可以响应虚拟摇杆操作的事件方法
#)

# 示例使用
#screen virtual_joystick():
#    add vjoy
#
#label start:
#    call screen virtual_joystick



#查看我写的更多屎

在回放中影响当前游戏存档数据的方法
https://www.renpy.cn/forum.php?mod=viewthread&tid=1739

她的心里话!隐藏文本!
https://www.renpy.cn/forum.php?mod=viewthread&tid=1713

能力雷达图
https://www.renpy.cn/forum.php?mod=viewthread&tid=1719

暴学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

FontGroup() 游戏里全局使用两种字体的方法
https://www.renpy.cn/forum.php?mod=viewthread&tid=1491

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









评分

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

查看全部评分

发表于 2025-4-10 05:22:36 | 显示全部楼层
最近我也在学习CDD,你可以直接在群里找到我QQ,交流。
回复 支持 1 抱歉 0

使用道具 举报

 楼主| 发表于 2025-4-11 11:03:20 | 显示全部楼层
本帖最后由 Maz马 于 2025-10-13 18:30 编辑

此楼原消息是更新,现已整合进楼顶
回复 支持 抱歉

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-11-1 09:31 , Processed in 0.059012 second(s), 29 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

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