|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
本帖最后由 Maz马 于 2026-8-17 20:35 编辑
26/8/17更新
(在此之前使用的注意新的版本,更新了样式的格式,滑窗算法减少渲染压力,是一次比较大的更新)
参考原帖出处:
https://nighten.itch.io/yet-another-phone-renpy
26/1/13
1.提取了参数,方便小白使用,我相信东一榔头西一棒槌的布局是大家最头疼的...
2.写了缩略图放大功能
26/4/13
3.自动匹配角色的头像 头像框 对话框,支持群聊,多角色对话
4.模拟聊天记录,再次进入同组信息会在上次阅读的最后一条继续,而之前的不会请求互动
26/8/17
5.注册新的音频通道,建立预设资源库,部分布局变量格式化,以供隔离和扩展
6.将调用环境黑箱化,避免在label内设一大堆环境变量
7.抽离多语言逻辑,现在配置时无关多语言,只按编写时的字面量运行,多语言自动支持
8.解决渲染树ID问题,新写滑窗算法,避免视口渲染大批量可视组件,减少性能压力
第一部分 系统
直接复制为一个新的rpy文件
原始代码插入了一些我自己的资源路径,请注意修改
[RenPy] 纯文本查看 复制代码 # ---------- 信息模拟系统 ----------
init python:
"""
Phone System (Ver 1.0 Ren'py 8.3.6)
Author: Maz
"""
# ---------- 样式配置 ----------
# 预设默认图
image phone_fg_default:
"#00000000"
image phone_bg_default:
"#000000dd"
image phone_icon_default:
"MAZ_PHONESYS/phone_icon_default.png"
image phone_icon_frame_default:
"#0000"
image phone_mesg_frame_default:
"MAZ_PHONESYS/phone_mesg_frame_default.png"
# 预设头像库
image phone_icon_frame_christmas:
"MAZ_PHONESYS/icon_frame/christmas.png"
define phone_icon_frames = {
"default" :("phone_icon_frame_default",(130,130)),
"christmas":("phone_icon_frame_christmas",(130,130)),}
# 预设气泡库
image phone_mesg_frame_hollow:
"MAZ_PHONESYS/mesg_frame/hollow.png"
image phone_mesg_frame_cat:
"MAZ_PHONESYS/mesg_frame/cat1.png"
pause 0.5
"MAZ_PHONESYS/mesg_frame/cat2.png"
pause 0.5
"MAZ_PHONESYS/mesg_frame/cat3.png"
pause 0.5
"MAZ_PHONESYS/mesg_frame/cat2.png"
pause 0.5
repeat
define phone_mesg_frames = {
"default" :("phone_mesg_frame_default", Borders(30,30,30,30), (20,10,20,15)),
"hollow" :("phone_mesg_frame_hollow", Borders(30,30,30,30), (20,10,20,15)),
"blackcat":("phone_mesg_frame_blackcat",Borders(103,66,30,30),(60,60,30,30)),}
# 预设字体库
# {字体标识符:字体路径}
define phone_text_fonts = {
"default" : gui.text_font,
"SourceHanSansLite":"SourceHanSansLite.ttf",}
default phone_xy = (0.5,0.5) # 手机界面在屏幕上的坐标
default phone_xysize = (600,1080) # 手机界面的尺寸(外框尺寸)
default phone_frame_xy = (0.5,0.56) # 聊天窗口在手机界面里的坐标
default phone_frame_xysize = (580,1000) # 聊天窗口的尺寸(比外框小)
default phone_frame_padding = (0,0,0,0) # 普通消息距离侧方边界的偏移
default phone_frame_spacing = 10 # 聊天消息之间的上下间隔
default phone_narr_width = 380 # 旁白消息的最大宽度
default phone_mesg_width = 380 # 普通消息的最大宽度
default phone_narr_sound = "MAZ_PHONESYS/sound/narr.mp3" # 默认旁白音效
default phone_send_sound = "MAZ_PHONESYS/sound/send.mp3" # 默认己方音效
default phone_recv_sound = "MAZ_PHONESYS/sound/recv.mp3" # 默认对方音效
default phone_item_sound = "MAZ_PHONESYS/sound/item.mp3" # 默认对方音效
default phone_text_font = phone_text_fonts['default'] # 文本字体
default phone_text_size = 27 # 文本大小
default phone_text_cps = 0 # 文本速度
default phone_bg = "phone_bg_default" # 默认背景图片
default phone_fg = "phone_fg_default" # 默认外框图片
default phone_icon = "phone_icon_default" # 默认头像图片
default phone_icon_frame = phone_icon_frames['default'] # 默认头像框图片 (图片,整个头像区域的大小)
default phone_narr_frame = phone_mesg_frames['default'] # 默认旁白气泡框 (图片,九方缩放的边距,文字和边框的边距)
default phone_item_frame = phone_mesg_frames['default'] # 默认选项气泡框
default phone_send_frame = phone_mesg_frames['default'] # 默认己方气泡框
default phone_recv_frame = phone_mesg_frames['default'] # 默认对方气泡框
default phone_bg_color = ["#000000",0.5] # 背景图像颜色 图片使用6位码 + 透明度
default phone_fg_color = ["#000000",0.5] # 外框图像颜色
default phone_narr_frame_color = ["#ffffff",1.0] # 旁白气泡颜色
default phone_item_frame_color = ["#ffffff",1.0] # 选项气泡颜色
default phone_send_frame_color = ["#ffffff",1.0] # 己方气泡颜色
default phone_recv_frame_color = ["#ffffff",1.0] # 对方气泡颜色
default phone_narr_text_color = "#000000ff" # 旁白文本颜色 文本使用8位码
default phone_send_text_color = "#000000ff" # 己方文本颜色
default phone_recv_text_color = "#000000ff" # 对方文本颜色
default phone_item_text_color = "#000000ff" # 选项文本颜色
default phone_narr_line_color = "#00000000" # 旁白描边颜色
default phone_send_line_color = "#00000000" # 己方描边颜色
default phone_recv_line_color = "#00000000" # 对方描边颜色
default phone_item_line_color = "#00000000" # 选项描边颜色
default phone_thl1 = "#ff0000ff" # 文本强调色1 phone_texthighlight 为方便编写文本标签使用缩写
default phone_thl2 = "#ffaf53ff" # 文本强调色2
default phone_thl3 = "#ff009dff" # 文本强调色3
default phone_thl4 = "#49ff67ff" # 文本强调色4
# 没有动画效果
transform phone_none_anim():
pass
# 头像动画效果
transform phone_icon_anim():
zoom 0.0
easein_back 0.5 zoom 1.0
# 旁白动画效果
transform phone_narr_anim():
alpha 0.0
yoffset -50
parallel:
ease 0.5 alpha 1.0
parallel:
easein_back 0.5 yoffset 0
# 选项动画效果
transform phone_item_anim():
alpha 0.0
xoffset 50
parallel:
ease 0.5 alpha 1.0
parallel:
easein_back 0.5 xoffset 0
# 己方动画效果
transform phone_send_anim():
alpha 0.0
xoffset 50
parallel:
ease 0.5 alpha 1.0
parallel:
easein_back 0.5 xoffset 0
# 对方动画效果
transform phone_recv_anim():
alpha 0.0
xoffset -50
parallel:
ease 0.5 alpha 1.0
parallel:
easein_back 0.5 xoffset 0
# ---------- 系统环境 ----------
default phone_mode = None # 区分正常NVL模式和手机组件的变量
define config.say_arguments_callback = Phone_MesgDict # 对话回调函数
define config.nvl_list_length = 999 # 最大对话记录数(在gui中也存在一条,这里直接覆写)
default phone_view_adj = PhoneAdjustment(20) # 滑窗对象,切片 20 条
default phone_mesglist = None # 控制当前使用哪个信息列表
default phone_mesgdata = {} # 信息组名字:信息列表
default phone_mesghash = None # 当前对话哈希
default phone_chardata = {} # 系统使用的角色数据库
default phone_callback = None # 预留数据更新的回调函数
default nvl_narr = NVLCharacter(kind=nvl) # 覆写旁白角色
default nvl_main = NVLCharacter(kind=nvl) # 套壳主控角色
init -1 python:
# 新建专用的音效通道
renpy.music.register_channel("phone",mixer="phone",loop=False,stop_on_mute=True,tight=False)
# 自定义 Adjustment 子类重写change(),提供滑窗算法切片 + 全量检测切片
class PhoneAdjustment(renpy.display.behavior.Adjustment):
def __init__(self,size):
super().__init__()
self.size = size # 切片大小,最多同时渲染的消息数
self.cutting = False # 脏标记,避免 change 递归
self.mark = 0 # 基准标记,当前窗口切片起始索引
self.last = 0 # 更新对齐,记录上次对话的总长度
# 滑窗算法切片
def change(self, value, end_animation=True):
if self.cutting:
return None
rv = super().change(value, end_animation)
# 向上逐条切片
if self.value == 0 and self.mark > 0:
self.cutting = True
self.mark -= 1
self.cutting = False
renpy.restart_interaction()
# 向下逐条切片
elif self.value == self.range and self.range > 0:
_max_mark = max(0, self.last - self.size)
if self.mark < _max_mark:
self.cutting = True
self.mark += 1
self.cutting = False
renpy.restart_interaction()
return rv
# 全量检测切片
def phone_cut_dialogue(self,dialogue):
_len = len(dialogue)
# 全量数据重置时重置对齐
if _len == 0:
self.last = 0
# 全量数据增长时更新对齐
if _len > self.last:
self.mark = max(0, _len - self.size)
self.last = len(dialogue)
# 始终切片
_start = max(0, min(self.mark, _len - self.size))
_slice = dialogue[_start:_start + self.size]
return _slice
# 已读信息记录字典
def Phone_MesgDict(who,interact=True):
# 如果当前流程phone_mesglist(信息组的名字)非None,即为应用了某个信息组
if store.phone_mesglist:
# phone_mesglist(信息组的名字)不在字典中,动态注册新的信息组列表
if store.phone_mesglist not in store.phone_mesgdata:
store.phone_mesgdata[store.phone_mesglist] = []
# 通过翻译系统获取当前对话哈希值,锁定唯一对话,避免说话角色相同,内容相同,但不是同一句话的情况
store.phone_mesghash = renpy.get_translation_identifier()
# 如果当前哈希,在当前的信息组列表中,并且不是上次阅读的最后一句话,跳过互动
if store.phone_mesghash in store.phone_mesgdata[store.phone_mesglist]:
if store.phone_mesghash != store.phone_mesgdata[store.phone_mesglist][-1]:
interact = False
# 否则,把哈希记录在当前的信息组列表
else:
store.phone_mesgdata[store.phone_mesglist].append(store.phone_mesghash)
# 当手机模式被打开,且没有跳过互动时,播放音效
if phone_mode == "phone" and interact == True:
if who.name == None:
renpy.music.play(phone_narr_sound, channel="phone")
elif who.name == store.nvl_main.name:
renpy.music.play(phone_send_sound, channel="phone")
elif who.name != store.nvl_main.name:
renpy.music.play(phone_recv_sound, channel="phone")
return (),{"interact":interact} # 返回需不需要互动的参数给renpy的say系统
# 可入参的showscreen文本标签
def show_screen_handler(target):
if "," in target:
screen_name,params_str = target.split(",",1)
params = {}
for pair in params_str.split(","):
if "=" in pair:
key,val = pair.split("=",1)
params[key.strip()] = val.strip()
renpy.show_screen(screen_name,**params)
else:
renpy.show_screen(target)
return True
config.hyperlink_handlers["show"] = show_screen_handler
def showscreen_tag(tag,argument,contents):
return [
(renpy.TEXT_TAG,f"a=show:{argument}"),
*contents,
(renpy.TEXT_TAG,"/a")]
config.custom_text_tags["showscreen"] = showscreen_tag
# 统一开关
def Phone_Switch(use=None,mainchar=None,chardata=None,mesglist=None):
if use:
store.phone_mode = "phone"
else:
store.phone_mode = None
if mainchar:
store.nvl_main = mainchar
if chardata:
store.phone_chardata = chardata
if mesglist:
store.phone_mesglist = mesglist
else:
store.phone_mesglist = None
return
# 删除指定消息组的已读记录(不传则删除当前消息组)
def Phone_Redata(mesglist=None):
if not mesglist:
mesglist = store.phone_mesglist
if mesglist and mesglist in store.phone_mesgdata:
del store.phone_mesgdata[mesglist]
# ---------- 界面框架 ----------
# 缩略放大
screen phone_cg(phonecg):
zorder 102
modal True
frame:
anchor (0.5,0.5)
xysize phone_xysize
pos phone_xy
background Frame("#000000")
foreground phone_fg
imagebutton:
align (0.5,0.5)
idle phonecg
action Hide("phone_cg")
# 消息界面
screen phone_dialogue(dialogue,items=None):
# 预留的回调接口
if callable(phone_callback):
$ phone_callback(dialogue)
# 劫持对话,绕过渲染树的必要判定(将必须持有id返回渲染树的当前对象移到看不到地方,以完成say生命周期)
if dialogue:
$ real_d = dialogue[-1]
text real_d.what id real_d.what_id:
pos (10086,10086)
color "#00000000"
outlines [(absolute(0),"#00000000")]
# 滑窗切片算法,劫持对话后,可随意安排注入对话列表
$ cut_dialogue = phone_view_adj.phone_cut_dialogue(dialogue)
zorder 101
frame:
anchor (0.5,0.5)
pos phone_xy
background Transform(phone_bg,matrixcolor=TintMatrix(Color(phone_bg_color[0])),alpha=phone_bg_color[1])
foreground Transform(phone_fg,matrixcolor=TintMatrix(Color(phone_fg_color[0])),alpha=phone_fg_color[1])
xysize phone_xysize
padding phone_frame_padding
viewport: # 可滚动区域
anchor (0.5,0.5)
pos phone_frame_xy
xysize phone_frame_xysize
draggable True
mousewheel True
# 视口一定会在边缘添加滚动条空间,即便你不使用滚动条这个空间也一定会被占据
# 因此添加滚动条尺寸的偏移才是“居中”
xoffset gui.scrollbar_size//2
yadjustment phone_view_adj
yinitial 1.0 # 跟踪底部
vbox:
xalign 0.5
spacing phone_frame_spacing
xfill True
use phone_contents(cut_dialogue)
# 选项部分
if items != None:
vbox:
xalign 1.0
xoffset -phone_icon_frame[1][0]//2
for i in items:
textbutton i.caption action i.action:
background Transform(
Frame(phone_item_frame[0],phone_item_frame[1]),
matrixcolor=TintMatrix(Color(phone_item_frame_color[0])),
alpha=phone_item_frame_color[1])
padding phone_item_frame[2]
xsize phone_mesg_width
text_font phone_text_font
text_size phone_text_size
text_color phone_item_text_color
text_outlines [(absolute(1),phone_item_line_color)]
text_xalign 0.5
text_prefer_emoji True
at phone_item_anim()
# 调出动态设置界面(扩展存在时才显示,无扩展可删除)
if renpy.has_screen("phone_setting"):
hbox:
align (1.0,1.0)
xoffset -60
yoffset -60
spacing 20
textbutton "设置" action ToggleScreen("phone_setting")
# 消息内容
screen phone_contents(dialogue):
zorder 101
# 遍历所有对话
for id_d,d in enumerate(dialogue):
# 先清除翻译系统的修改,以进行字符串匹配
if d.who == None:
$ temp_name = None
else:
$ temp_name = renpy.translate_string(d.who,None)
# 判断角色的身份,使用对应的动效和样式
if temp_name == None:
$ temp_mesg_trans = phone_narr_anim()
else:
if temp_name == nvl_main.name:
$ temp_mesg_trans = phone_send_anim()
$ temp_mesg_frame = phone_send_frame
$ temp_mesg_frame_color = phone_send_frame_color
$ temp_text_color = phone_send_text_color
$ temp_line_color = phone_send_line_color
$ temp_reverse = True
$ temp_align = 1.0
else:
$ temp_mesg_trans = phone_recv_anim()
$ temp_mesg_frame = phone_recv_frame
$ temp_mesg_frame_color = phone_recv_frame_color
$ temp_text_color = phone_recv_text_color
$ temp_line_color = phone_recv_line_color
$ temp_reverse = False
$ temp_align = 0.0
# 从角色数据库获取头像,头像框,对话框,如果为None或捕获不到,使用默认
$ temp_icon = phone_chardata.get(temp_name,{}).get("icon") or phone_icon
$ temp_icon_frame = phone_chardata.get(temp_name,{}).get("icon_frame") or phone_icon_frame
$ temp_mesg_frame = phone_chardata.get(temp_name,{}).get("mesg_frame") or temp_mesg_frame
# 动态调整样式时,控制历史条目不会重新被唤起
if d.current:
$ temp_cps = phone_text_cps
$ temp_icon_trans = phone_icon_anim()
else:
$ temp_cps = False
$ temp_icon_trans = phone_none_anim()
$ temp_mesg_trans = phone_none_anim()
# 旁白消息
if temp_name == None:
frame:
align (0.5,0.5)
background Transform(
Frame(phone_narr_frame[0],phone_narr_frame[1]),
matrixcolor=TintMatrix(Color(phone_narr_frame_color[0])),
alpha=phone_narr_frame_color[1])
padding phone_narr_frame[2]
at temp_mesg_trans
text d.what:# id d.what_id 因为已经劫持了say必须的id控件,所以此处不能再使用id,否则会出现id冲突,出现各种意外报错
align (0.5,0.5)
xsize phone_narr_width
font phone_text_font
size phone_text_size
color phone_narr_text_color
outlines [(absolute(1),phone_narr_line_color)]
slow_cps temp_cps
text_align 0.5
min_width 0
prefer_emoji True
# 角色消息
else:
hbox:
spacing 10
box_reverse temp_reverse
xalign temp_align
# 头像
fixed:
xysize temp_icon_frame[1]
at temp_icon_trans
add temp_icon align (0.5,0.5)
add temp_icon_frame[0] align (0.5,0.5)
vbox:
spacing 10
# 名称
text d.who:
size phone_text_size + 4
font phone_text_font
textalign temp_align
xalign temp_align
prefer_emoji True
# 气泡
frame:
background Transform(
Frame(temp_mesg_frame[0],temp_mesg_frame[1]),
matrixcolor=TintMatrix(Color(temp_mesg_frame_color[0])),
alpha=temp_mesg_frame_color[1])
padding temp_mesg_frame[2]
at temp_mesg_trans
# 消息文本
text d.what:# id d.what_id 因为已经劫持了say必须的id控件,所以此处不能再使用id,否则会出现id冲突,出现各种意外报错
align (0.5,0.5)
min_width 0
xsize phone_mesg_width
font phone_text_font
size phone_text_size
color temp_text_color
outlines [(absolute(1),temp_line_color)]
text_align temp_align
slow_cps temp_cps
prefer_emoji True
第二部分 使用方法
[RenPy] 纯文本查看 复制代码 # ---------- 最小示例 ----------
# 定义phone角色 注意,如果在Character中设置样式,会覆盖系统样式
default nvl_a = Character("我", kind=nvl)
default nvl_b = Character("他", kind=nvl)
default nvl_c = Character("她", kind=nvl)
# 角色外观数据库
# 角色名 → 头像/头像框/气泡框
# icon = 头像
# icon_frame = (图片, 整个头像区域大小)
# mesg_frame = (图片, Borders九方边距, 文字边距)
# 没有捕获到的角色会直接使用默认
default testdata = {
"她": {"icon": "phone_icon_default",
"icon_frame": ("phone_icon_frame_christmas",(130,130)),
"mesg_frame": ("phone_mesg_frame_hollow", Borders(30,30,30,30), (20,10,20,15)),},
}
label PHONESYS_test:
# 1. 打开手机模式,设定主控角色、角色外观数据库、消息组
$ Phone_Switch(use=True, mainchar=nvl_a, chardata=testdata, mesglist="test")
# 2. 发消息:旁白 / 对方 / 己方
nvl_narr "这是旁白的写法"
nvl_b "使用专用的角色开始对话"
nvl_a "这是另一边的角色在说话"
nvl_b "这样插入一张 emoji 或 小图{image=phone/emoji/clap.png}"
menu(nvl=True):
nvl_narr "选项菜单要入参 nvl=True"
"选项1":
pass
"选项2":
pass
nvl_b "然后是 一个可放大的图像,点击图像将会放大,但形状要自己裁切\n{showscreen=phone_cg,phonecg=phone cg01ex}{image=phone cg01}{/showscreen}"
nvl_b "phonecg=phone cg01ex 是放大的图片,image=phone cg01 是缩略图,都可以是帧动画,只要是声明的image"
nvl_c "现在尝试多角色对话"
nvl_c "支持{color=[phone_thl1]}着重色{/color}"
nvl_c "你也可以自由颜色,着重色使用特定变量的原因是以便模块整体扩展"
# 可使用 Phone_Redata() 清理对应消息组的聊天记录
# 3. 关闭手机模式,清空nvl界面
$ Phone_Switch(use=False)
nvl clear
return
第三部分 修改(和原生的项目工程对接,仅修改一句话)
以上部分可以直接建成新文件,以下是需要在源文件screen.rpy中修改的,只加一句话
[RenPy] 纯文本查看 复制代码 screen nvl(dialogue, items=None):
zorder 101
### 在这里增加应用手机系统的分支 ###
if nvl_mode == "phone":
use phone_dialogue(dialogue,items)
else:
### 以下是源代码只增加了缩进没有任何改动 ###
window:
style "nvl_window"
has vbox:
spacing gui.nvl_spacing
## 在 vpgrid 或 vbox 中显示对话框。
if gui.nvl_height:
vpgrid:
cols 1
yinitial 1.0
use nvl_dialogue(dialogue)
else:
use nvl_dialogue(dialogue)
## 显示菜单,如果给定的话。如果 config.narrator_menu 设置为 True,则菜单
## 可能显示不正确。
for i in items:
textbutton i.caption:
action i.action
style "nvl_button"
add SideImage() xalign 0.0 yalign 1.0
第四部分 扩展
因为布局是变量,因此扩展一个可以动态修改界面,属于是雕花了
[RenPy] 纯文本查看 复制代码 # ---------- 信息模拟系统 ----------
init python:
"""
Phone System EX (Ver 1.0 Ren'py 8.3.6)
Author: Maz
"""
# ---------- 样式扩展 ----------
# 占位背景图
image alphablack:
"#000000dd"
# 颜色环境
init python:
phone_setting_color_map = {
"BG Img" :"phone_bg_color",
"FG Img" :"phone_fg_color",
"Narr Img":"phone_narr_frame_color",
"User Img":"phone_send_frame_color",
"Char Img":"phone_recv_frame_color",
"Item Img":"phone_item_frame_color",
"Narr Txt":"phone_narr_text_color",
"User Txt":"phone_send_text_color",
"Char Txt":"phone_recv_text_color",
"Item Txt":"phone_item_text_color",
"Narr Out":"phone_narr_line_color",
"User Out":"phone_send_line_color",
"Char Out":"phone_recv_line_color",
"Item Out":"phone_item_line_color",
"Accent 1":"phone_thl1",
"Accent 2":"phone_thl2",
"Accent 3":"phone_thl3",
"Accent 4":"phone_thl4",}
phone_setting_color_preset = [
"#00000000","#00000080","#000000ff","#808080ff",
"#ffffffff","#fffbe5ff","#f3ffe5ff","#ece5ffff",
"#ff0000ff","#ffa600ff","#ffe600ff","#00ff00ff",
"#00ffffff","#0000ffff","#b700ffff","#ff6767ff",]
#颜色解析/格式化函数
def _color(val,mode="hex8"):
"""
val: str(3/4/6/8位hex) / list[hex,alpha] / list[h,s,v,alpha]
mode: "hex8"→8位hex "hexa"→[6位hex,alpha] "hsva"→[h,s,v,alpha]
"""
# 先把输入统一转成(hex6,alpha)
if isinstance(val,str):
c = Color(val)
hex6,alpha = c.hexcode[:7],c.alpha
elif isinstance(val,list):
if len(val) == 2:
hex6,alpha = val[0][:7],val[1]
elif len(val) == 4:
c = Color(hsv=(val[0],val[1],val[2]))
hex6,alpha = "#"+c.hexcode.replace("#","")[:6],val[3]
else:
hex6,alpha = "#ffffff",1.0
else:
hex6,alpha = "#ffffff",1.0
# 按mode输出
if mode == "hex8":
c = Color(hex6)
return "#"+c.hexcode.replace("#","")[:6]+"{:02x}".format(int(alpha*255))
elif mode == "hexa":
return [hex6,alpha]
elif mode == "hsva":
c = Color(hex6)
return [c.hsv[0],c.hsv[1],c.hsv[2],alpha]
return hex6,alpha
# 预览色块 读取目标数据,转hex8代码
def color_get_pre(target):
var = phone_setting_color_map[target]
return _color(getattr(store,var),"hex8")
# 预设色块 读取颜色代码,转hsva分量,赋值给目标
def color_set_pre(color):
store.phone_setting_h,store.phone_setting_s,store.phone_setting_v,store.phone_setting_a = _color(color,"hsva")
color_set()
# 选定目标 读取目标数据,转hsva分量
def color_select(target):
var = phone_setting_color_map[target]
store.phone_setting_h,store.phone_setting_s,store.phone_setting_v,store.phone_setting_a = _color(getattr(store,var),"hsva")
store.phone_setting_color_obj = target
# 修改目标 将hsva分量,匹配格式,赋值给目标
def color_set():
if not store.phone_setting_color_obj:
return
var = phone_setting_color_map[store.phone_setting_color_obj]
if "Img" in store.phone_setting_color_obj:
setattr(store,var,_color([store.phone_setting_h,store.phone_setting_s,store.phone_setting_v,store.phone_setting_a],"hexa"))
else:
setattr(store,var,_color([store.phone_setting_h,store.phone_setting_s,store.phone_setting_v,store.phone_setting_a],"hex8"))
# 将图像预设应用到当前主控角色
def set_icon_frame(data):
store.phone_chardata.setdefault(store.nvl_main.name, {})["icon_frame"] = data
def set_mesg_frame(data):
store.phone_chardata.setdefault(store.nvl_main.name, {})["mesg_frame"] = data
# 中转变量
default phone_setting_page = "Basic"
default phone_setting_color_obj = "BG Img"
default phone_setting_h = 0.0
default phone_setting_s = 0.0
default phone_setting_v = 0.0
default phone_setting_a = 0.0
# 界面样式
style phone_setting_text:
size 28
color "#ffffff"
style phone_setting_slider:
xpos 30
xsize 420
style phone_setting_button_text:
size 30
idle_color "#ffffff"
hover_color "#ffcc00"
selected_color "#ffcc00"
style switch_text is phone_setting_text
style switch_button:
xpos 30
foreground "gui/button/check_[prefix_]foreground.png"
style switch_button_text:
size 28
xpos 30
yalign 0.5
idle_color "#888888"
hover_color "#ffffff"
selected_color "#ffffff"
style switch_reverse_button:
size 28
xpos 30
idle_foreground "gui/button/check_selected_foreground.png"
hover_foreground "gui/button/check_hover_foreground.png"
selected_hover_foreground "gui/button/check_hover_foreground.png"
selected_idle_foreground "gui/button/check_idle_foreground.png"
style switch_reverse_button_text:
size 28
xpos 30
yalign 0.5
idle_color "#ffffff"
hover_color "#ffffff"
selected_hover_color "#ffffff"
selected_idle_color "#888888"
# 设置界面
screen phone_setting():
# 设置界面禁止回滚
on "show" action Function(renpy.block_rollback)
dismiss action ToggleScreen("phone_setting")
zorder 102
style_prefix "phone_setting"
# 布局区域
frame:
background Frame("alphablack")
align (phone_xy[0],0.0)
xysize (phone_xysize[0],400)
modal True
vbox:
xalign 0.5
# 选项区域
hbox:
xfill True
textbutton _("阅读") xalign 0.5 action SetVariable("phone_setting_page","Basic")
textbutton _("音频") xalign 0.5 action SetVariable("phone_setting_page","Audio")
textbutton _("颜色") xalign 0.5 action SetVariable("phone_setting_page","Color")
textbutton _("图像") xalign 0.5 action SetVariable("phone_setting_page","Image")
null height 10
# 内容区域
hbox:
xsize 800
xalign 0.5
if phone_setting_page == "Basic":
vbox:
style_prefix "switch"
xalign 0.0
text _("语言")
vbox:
hbox:
textbutton _("English") action Language("english")
null width 60
hbox:
textbutton _("中文") action Language(None)
null width 60
text _("快进")
vbox:
hbox:
textbutton _("未读文本") action Preference("skip","toggle")
null width 60
hbox:
textbutton _("忽略转场") action InvertSelected(Preference("transitions","toggle"))
null width 60
hbox:
textbutton _("选项后继续") action Preference("after choices","toggle")
null width 60
vbox:
style_prefix "phone_setting"
xalign 1.0
text _("自动前进时间")
hbox:
bar value Preference("auto-forward time")
null width 30
text _("文字速度")
hbox:
bar value VariableValue("phone_text_cps",60,step=1) style "phone_setting_slider"
null width 30
text _("文本大小 ([phone_text_size])")
hbox:
bar value VariableValue("phone_text_size",60,step=1) style "phone_setting_slider"
null width 30
text _("文本字体")
hbox:
null width 30
frame:
background Frame("alphablack")
xsize 420
textbutton _("[phone_text_font]") action CaptureFocus("font_drop")
elif phone_setting_page == "Audio":
vbox:
xalign 0.0
text _("通道")
textbutton _("全部启用") action Preference("all mute","toggle") style "switch_reverse_button"
vbox:
xalign 1.0
text _("信息")
hbox:
bar value Preference("phone volume")
null width 30
text _("音乐")
hbox:
bar value Preference("music volume")
null width 30
text _("音效")
hbox:
bar value Preference("sound volume")
null width 30
text _("语音")
hbox:
bar value Preference("voice volume")
null width 30
elif phone_setting_page == "Color":
vbox:
xalign 0.0
text _("对象")
hbox:
null width 30
frame:
background Frame("alphablack")
xsize 140
textbutton _("[phone_setting_color_obj]")action CaptureFocus("color_drop")
$ temp = color_get_pre(phone_setting_color_obj)
text _("预览")
hbox:
null width 30
fixed:
xysize(140,140)
add Solid(temp)
text "[temp]" align (0.5,1.0)
null width 30
grid 4 4:
spacing 5
for c in phone_setting_color_preset:
button:
xysize (30,30)
background Solid(c)
action Function(color_set_pre,c)
vbox:
xalign 1.0
text _("色相")
hbox:
bar value VariableValue("phone_setting_h",1.0,action=color_set) style "phone_setting_slider"
null width 30
text _("饱和度")
hbox:
bar value VariableValue("phone_setting_s",1.0,action=color_set) style "phone_setting_slider"
null width 30
text _("明度")
hbox:
bar value VariableValue("phone_setting_v",1.0,action=color_set) style "phone_setting_slider"
null width 30
text _("透明度")
hbox:
bar value VariableValue("phone_setting_a",1.0,action=color_set) style "phone_setting_slider"
null width 30
elif phone_setting_page == "Image":
vbox:
xalign 0.5
xsize 780
spacing 5
text _("头像框")
viewport:
ysize 110
xsize 780
draggable True
mousewheel True
hbox:
spacing 5
for name, data in phone_icon_frames.items():
vbox:
button:
xysize (100,100)
background Frame("alphablack")
foreground Frame(data[0])
action Function(set_icon_frame, data)
text _("气泡框")
viewport:
ysize 110
xsize 780
draggable True
mousewheel True
hbox:
spacing 5
for name, data in phone_mesg_frames.items():
vbox:
button:
xysize (100,100)
background Frame("alphablack")
foreground Frame(data[0], data[1])
action Function(set_mesg_frame, data)
if GetFocusRect("font_drop"):
dismiss action ClearFocus("font_drop")
nearrect:
focus "font_drop"
frame:
modal True
background Frame("alphablack")
xsize 420
vbox:
textbutton _("默认") action [SetVariable("phone_text_font",phone_text_fonts["default"]),ClearFocus("font_drop")]
textbutton _("思源黑体 细线") action [SetVariable("phone_text_font",phone_text_fonts["SourceHanSansLite"]),ClearFocus("font_drop")]
if GetFocusRect("color_drop"):
dismiss action ClearFocus("color_drop")
nearrect:
focus "color_drop"
frame:
modal True
background Frame("alphablack")
xsize 140
ysize 320
viewport:
draggable True
mousewheel True
vbox:
textbutton _("背景") action Function(color_select,"BG Img"),ClearFocus("color_drop")
textbutton _("外框") action Function(color_select,"FG Img"),ClearFocus("color_drop")
null height 10
textbutton _("旁白气泡") action Function(color_select,"Narr Img"),ClearFocus("color_drop")
textbutton _("用户气泡") action Function(color_select,"User Img"),ClearFocus("color_drop")
textbutton _("角色气泡") action Function(color_select,"Char Img"),ClearFocus("color_drop")
textbutton _("选项气泡") action Function(color_select,"Item Img"),ClearFocus("color_drop")
null height 10
textbutton _("旁白文本") action Function(color_select,"Narr Txt"),ClearFocus("color_drop")
textbutton _("用户文本") action Function(color_select,"User Txt"),ClearFocus("color_drop")
textbutton _("角色文本") action Function(color_select,"Char Txt"),ClearFocus("color_drop")
textbutton _("选项文本") action Function(color_select,"Item Txt"),ClearFocus("color_drop")
null height 10
textbutton _("旁白描边") action Function(color_select,"Narr Out"),ClearFocus("color_drop")
textbutton _("用户描边") action Function(color_select,"User Out"),ClearFocus("color_drop")
textbutton _("角色描边") action Function(color_select,"Char Out"),ClearFocus("color_drop")
textbutton _("选项描边") action Function(color_select,"Item Out"),ClearFocus("color_drop")
null height 10
textbutton _("强调色1") action Function(color_select,"Accent 1"),ClearFocus("color_drop")
textbutton _("强调色2") action Function(color_select,"Accent 2"),ClearFocus("color_drop")
textbutton _("强调色3") action Function(color_select,"Accent 3"),ClearFocus("color_drop")
textbutton _("强调色4") action Function(color_select,"Accent 4"),ClearFocus("color_drop")
上方是完整代码以及使用方式
下方新手可以完全不看,是我更新过程中,逐步解决的问题以及思路,有一定难度以及理解要求,
但假如遇到问题,可以看看下方尝试解决,实在解决不了可以留言
额外内容:聊天记录中的分支顺序问题
在聊天记录功能完成之后就有一个问题,假设你的聊天事件中具有外部开关管控的内容
[RenPy] 纯文本查看 复制代码
label phonelabel_xx:
$ nvl_mode = "phone" # 先打开手机模式
$ phone_message_list = "test" # 设置当前消息组的名称
nvl_narrator "1"
if ss:
nvl_narrator "2"
if cc:
nvl_narrator "3"
nvl_narrator "4"
$ phone_message_list = None # 重置当前的消息组
$ nvl_mode = None # 关闭手机模式(养成封闭习惯)
nvl clear # 清空nvl界面,释放缓存
return
那么聊天记录中呈现的将会是顺序显示1,2,3,4
而不是根据玩家解锁顺序把新的消息放在最下方
例如先不开开关,然后开cc,再开ss,我们期望的顺序应该是1,4,3,2
解决这个问题,我有一个方案,但暂时没有进行整合,这里先进行提出
那就是记录时间表,用队列形式进行排序,并且label的变更内容做成新的label,而不是单独的对话
[RenPy] 纯文本查看 复制代码 default phonelabel_xx_list = []
label phonelabel_xx:
$ nvl_mode = "phone" # 先打开手机模式
$ phone_message_list = "test"
nvl_narrator "1"
# 假设这里开始分支
if ss and "phonelabel_xx_1" not in phonelabel_xx_list:
$ phonelabel_xx_list.append("phonelabel_xx_1")
if cc and "phonelabel_xx_2" not in phonelabel_xx_list:
$ phonelabel_xx_list.append("phonelabel_xx_2")
if phonelabel_xx_list != []:
$ index = 0
$ renpy.jump(phonelabel_xx_list[index])
else:
pass
$ phone_message_list = None
$ nvl_mode = None
nvl clear
jump start
label phonelabel_xx_1:
$ nvl_mode = "phone" # 先打开手机模式
$ phone_message_list = "test"
"2"
$ index = phonelabel_xx_list.index("phonelabel_xx_1")+1
if index <= len(phonelabel_xx_list):
$ renpy.jump(phonejs_kbn[index])
else:
pass
$ phone_message_list = None
$ nvl_mode = None
nvl clear
return
label phonelabel_xx_2:
$ phone_message_list = "test"
"3"
$ index = phonelabel_xx_list.index("phonelabel_xx_2")+1
if index < len(phonelabel_xx_list):
$ renpy.jump(phonejs_kbn[index])
else:
pass
$ phone_message_list = None
$ nvl_mode = None
nvl clear
return
此时,聊天记录的顺序除了最开始的label,分支内容则由按照开关解锁的顺序队列进行,而不是依托于label语法糖的严格上下顺序。
这一点上使用有些微难度,但这本质是因为这个系统依托于renpy本身的语法糖,能够很好兼容say,nvl,choice,history等模块
假如完全独立于renpy的系统之外构筑一个使用类、函数注册等方法的系统,手动管理消息组列表,自然不需要这么麻烦
但也会失去renpy本身的便捷语法,同时还要独立接入其他非核心的模块(choice,history),这是另一个层次上的麻烦,而且在系统的架构上也更加复杂、
1/25之前更新处
[RenPy] 纯文本查看 复制代码 #在第二部分
default phone_avatars = {...}
#修改为
init python:
def update_phone_avatars():
store.phone_avatars = {...}
#在第一部分
# 手机消息显示组件
screen phone_nvltext(dialogue):
zorder 101
# 添加了这一句
$ update_phone_avatars()
原因:
游戏生命周期中,default在初始化时被决定,因此立即翻译函数并不会被执行,或只执行一次
[RenPy] 纯文本查看 复制代码 renpy.translate_string()
所以,这个字典会被锁死在最初的游戏语言,一旦切换语言,字典就会对不上,没有动态化
所以将其注册为函数,每次打开界面时运行,实现动态匹配多语言角色
4/13之前更新处
在第一部分添加了
[RenPy] 纯文本查看 复制代码 # say回调函数
define config.say_arguments_callback = phone_message_dict
init python:
# 手机字典集
def phone_message_dict(who,interact=True):
# 如果当前流程phone_message_list(信息组的名字)非None,即为应用了某个信息组
if store.phone_message_list:
# phone_message_list(信息组的名字)不在字典中,动态注册新的信息组列表
if store.phone_message_list not in store.phone_message_dict:
store.phone_message_dict[store.phone_message_list] = []
# 通过翻译系统获取当前对话哈希值,锁定唯一对话,避免说话角色相同,内容相同,但不是同一句话的情况
message_hash = renpy.get_translation_identifier()
# 如果当前哈希,在当前的信息组列表中,并且不是上次阅读的最后一句话,跳过互动
if message_hash in store.phone_message_dict[store.phone_message_list]:
if message_hash != store.phone_message_last[store.phone_message_list]:
interact = False
# 否则,把哈希记录在当前的信息组列表,并在另一个字典记录这句话的哈希,也就是形成映射
else:
store.phone_message_dict[store.phone_message_list].append(message_hash)
store.phone_message_last[store.phone_message_list] = message_hash
# 映射字典实际为
# {信息组名字:已读信息的列表}
# {信息组名字:上次阅读的最后一条信息}
return (), {"interact": interact} # 返回需不需要互动的参数给renpy的say系统
然后在使用时,写信息label时,用$ phone_message_list =“xx”来包裹(相同的字符串名字会被视作同一栏信息)
[RenPy] 纯文本查看 复制代码 label phonelabel_xx:
$ nvl_mode = "phone" # 先打开手机模式
$ phone_message_list = "test" # 设置当前消息组的名称
"聊天消息的内容"
$ phone_message_list = None # 重置当前的消息组
$ nvl_mode = None # 关闭手机模式(养成封闭习惯)
return
思考过程:
实际上聊天记录这个功能就是已读消息不需要互动直接跳过
恰好,原生say系统中,在角色定义里有一个参数为interact,决定了当前角色的对话需不需要用户互动,也就是“点击继续”的dismiss行为
我一直苦恼怎么把interact自动的,非手动覆盖传给say,毕竟一条条写判断也太糖了
实际尝试,因为最开始就有一个interact的方向,所以看源码和查阅文档后
发现文档中给出了一个配置项config.say_arguments_callback,这是一个回调函数,会在say发起时自动调用
那么问题就轻松解决了,只要记录一下哈希就可以了。
8/17之前更新处
思考过程:
陆陆续续写了不少组件和轮子了,有了一定的架构思维,直觉在数据层处理多语言很不对,于是很快就修改成了
“在输出业务前,把动态数据还原成原始数据再处理逻辑”的思路,于是很多的耦合自然解开了,
本来这里的逻辑应该抽成函数,但稍微思考,此处的逻辑是在判断样式而不是数据,留在screen更直观,就没有进行封装
[RenPy] 纯文本查看 复制代码 # 消息内容
screen phone_contents(dialogue):
zorder 101
# 遍历所有对话
for id_d,d in enumerate(dialogue):
# 先清除翻译系统的修改,以进行字符串匹配
if d.who == None:
$ temp_name = None
else:
$ temp_name = renpy.translate_string(d.who,None)
同理,在label开始前要进行一大堆环境设定,也很不合理,因此封装
[RenPy] 纯文本查看 复制代码 init -1 python:
# 统一开关
def Phone_Switch(use=None,mainchar=None,chardata=None,mesglist=None):
if use:
store.phone_mode = "phone"
else:
store.phone_mode = None
if mainchar:
store.nvl_main = mainchar
if chardata:
store.phone_chardata = chardata
if mesglist:
store.phone_mesglist = mesglist
else:
store.phone_mesglist = None
return
# 删除指定消息组的已读记录(不传则删除当前消息组)
def Phone_Redata(mesglist=None):
if not mesglist:
mesglist = store.phone_mesglist
if mesglist and mesglist in store.phone_mesgdata:
del store.phone_mesgdata[mesglist]
最后,滑窗算法理解起来并不难,主要问题出现在对信息列表切片时持续报错,最后发现问题并不是在列表上
而是renpy的nvl/say一类会被say函数使用的screen必须返回一个带有当前id的what到渲染树才能完成生命周期,因此用简单的办法(邪道)解决
[RenPy] 纯文本查看 复制代码 # 消息界面
screen phone_dialogue(dialogue,items=None):
# 预留的回调接口
if callable(phone_callback):
$ phone_callback(dialogue)
# 劫持对话,绕过渲染树的必要判定(将必须持有id返回渲染树的当前对象移到看不到地方,以完成say生命周期)
if dialogue:
$ real_d = dialogue[-1]
text real_d.what id real_d.what_id:
pos (10086,10086)
color "#00000000"
outlines [(absolute(0),"#00000000")]
#点击头像 查看我写的更多屎
粉身碎骨浑不怕,要留答辩在人间
|
|