开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

🔥花了3天空余时间写了个纯前端游戏《卡片天才》

发表于 2024-04-26

📌前言

后端平均一天一个接口 , 每天在岗位上闲的蛋疼。

在摸鱼的过程中玩了一款网页的文字沙盒游戏 ,玩了小一会枯燥乏味 ,满眼的文字非常抗拒 , 所以突发奇想为何不改造成有动画 , 有图片 ,有属性的卡片游戏呢?

思之而作 , 《卡片天才》应运而生 , 这是一款纯前端vue实现的卡片小游戏。

注意:游戏素材来源于网络 , 并未商用。

🍉玩法

  • 堆叠卡片产生不同的效应

没错 , 到这就介绍完了🥰 , 游戏主打的就是简单而不可思议 , 欢迎大家摸鱼时游玩并私信博主

相较游戏而言是非常简单的

但是作为纯使用前端vue实现来说是非常困难的

博主好几次都想不过来 , 需要大量的效应算法 , 以及阻断效应 , 和移动堆叠卡片重新匹配效应。

游戏画面

2024-04-25 17-38-37.gif

🍓技术

项目使用了帝莎编程 https://pc.dishait.cn/ , 这是一款vue3的开箱即用模板 , 不是打广告哈, 我并不是贡献者 , 我也是最近才发现的 , 模板中各种配置使用重复工作都写好了 , 可以直接上手。

  • 大致描述一下 vite5 + vue3.4 + ts + vueuse + pinia + router + unocss + icones 等等
  • 支持国际化以及暗黑模式

🍈开发思路

  1. 使用vueuse中的useDraggable实现拖拽以及拖拽前后的判读。
  2. 使用pinia并开启persist将游戏数据缓存起来
  3. 鼠标选中卡片时提高卡片层级,保证后选中的卡片在前选中的卡片高
  4. 卡片移动前解除卡片的效应,并编写算法根据移动卡片在卡组的位置,产生不同情况下的效应
  5. 卡片移动后与满足距离且最近的卡片组合,暂停卡组效应并根据最近卡片的组合情况产生新的效应。

🎉部分代码

移动前监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
ts复制代码onStart: (position, e) => {
// 移动者
let changeCards = props.card
// 更新卡片层级
data.updateZIndex(changeCards.id)
// 移动者组装项
let group =[...changeCards.group]
// 移动者在组中的位置
let groupIndex = group.findIndex(item=>item==changeCards.id)
// 移动者直接解除组装
changeCards.group = []
// 移动前后卡片组
let groupAll:number[][] = [[],[]]
// 卡片解除组装
if (group.length != 0) {
data.cards.forEach((item,index)=>{
// 属于卡片组&&不是本身
if (group.includes(item.id) && changeCards.id != item.id) {
// 组中卡
let card = data.cards[index]
// 组中卡在组中的位置
let cardIndex = group.findIndex(item=>item==card.id)
// 顶层卡片解除效应
if (card.id==group[0]) {
clearInterval(card.setInt)
card.percentage = 0
}
// 组中卡在移动卡前
if (cardIndex < groupIndex) {
// 少于两张卡
if (groupIndex <= 1) {
card.group = []
}else{
card.group = group.slice(0,groupIndex)
groupAll[0]=card.group
}
// 组中卡在移动卡后
}else{
// 少于两张卡
if (group.length - groupIndex <= 2) {
card.group = []
}else{
card.group = group.slice(groupIndex+1)
groupAll[1]=card.group
}
}
}
})
}
// 执行移动前后卡片组效应
groupAll.forEach((item)=>{
if (item.length!==0) {
groupEffect(item)
}
})
}

移动后监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
ts复制代码onEnd: (position, e) => {
// 移动者
let changeCards = props.card
// 记忆卡片最新位置
changeCards.x = x.value
changeCards.y = y.value
// 移动结束坐标
const endX = position.x
const endY = position.y
// 移动范围
const maxWidth: number = e.view?.innerWidth as number - 100
const maxHeight: number = e.view?.innerHeight as number - 100

// 卡片超出边界复位
x.value = Math.max(0, Math.min(endX, maxWidth))
y.value = Math.max(0, Math.min(endY, maxHeight))

// 符合距离条件卡片
let cardsAll:Card[] = []
// 卡片距离
let distance:number[] = []
// 给上述变量赋值
data.cards.forEach((item,index)=>{
// 去掉本身 , 隐藏 , 正在动画 , 末尾卡片非自身的卡片
if (item.id == changeCards.id || !item.show || item.animation!='' || ( item.group.length!=0 && item.group[item.group.length-1]!=item.id)) {
return
}
const xDistance = Math.abs(x.value - item.x)
const yDistance = Math.abs(y.value - item.y)
// 找到同时满足X , Y 坐标接近的卡片
if ( xDistance <= data.len.x && yDistance <= data.len.y ) {
distance.push(xDistance + yDistance)
cardsAll.push(item)
}
})
// 具有符合组合条件的卡片才进行组合
if (distance.length!=0) {
// 最近卡片索引
let minIndex = distance.findIndex(item => item == Math.min(...distance))
// 最近的卡片
let groupCard = cardsAll[minIndex]
// 记录组合 父级组合存在加入
let newGroup =groupCard.group.length!=0?[...groupCard.group,changeCards.id]:[groupCard.id,changeCards.id]

// 给新组合卡片重新赋值卡片组
data.cards.forEach((item,index)=>{
let card = data.cards[index]
// 解除原卡组效应
if (newGroup[0]==item.id) {
clearInterval(card.setInt)
card.percentage = 0
}
if (newGroup.includes(item.id)) {
card.group = newGroup
}
})
// 将卡片组合
x.value = groupCard.x
y.value = groupCard.y + 30

// 产生效应
groupEffect(newGroup)
}

// 记忆卡片最新位置
changeCards.x = x.value
changeCards.y = y.value

}

🍇后续思路

  • 除了更新更多卡片外 , 还将更新每张卡片的独特属性 ( 例如: 人类性别 – 年龄)
  • 金币系统以及战斗系统 (例如: 使用金币购买物品扩展卡片)
  • 繁殖系统以及继承系统 (例如: 将不同性别的人类产生效应)
  • 哥布林 ( 直接用博主照片 )

📚总结

发布这篇文章时只有两种效应和3张卡片 , 后续会更新创作。

可以查看更新日志和博主一起成长

欢迎各位留言以及私信。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python办公神器:教你使用python批量整理通知文件

发表于 2024-04-26

你是否遇到过这样的情况:你有一堆Word文件,每个文件都包含一些重要的信息,你需要把这些信息提取出来,整理到一个Excel表格中,方便查阅和管理。但是,手动打开每个文件,复制粘贴信息,又太费时费力,而且容易出错。

有没有什么办法可以让Python帮你自动完成这个任务呢?答案是肯定的!本文将教你如何用Python批量提取Word文件中的关键信息,并写入Excel文件中,只需几行代码,就可以节省大量的时间和精力。

准备工作

在开始编写代码之前,我们需要先准备一些必要的工具和文件:

  • Python环境:本文使用的是Python 3.8,你可以在下载安装。
  • openpyxl库:这是一个用于操作Excel文件的Python库,你可以使用pip install openpyxl命令安装。
  • python-docx库:这是一个用于操作Word文件的Python库,你可以使用pip install python-docx命令安装。
  • glob库:这是一个用于获取文件路径的Python库,一般已经内置在Python中,无需安装。
  • Word文件:这是我们要提取信息的源文件,本文以7个会议通知文件为例,每个文件都包含学习时间、学习内容、学习形式、主持人四项关键信息,文件名分别为会议通知1.docx,会议通知2.docx,…,会议通知7.docx,存放在Notice文件夹下。
  • Excel文件:这是我们要写入信息的目标文件,本文以一个空白的Excel文件为例,文件名为Meeting_temp.xlsx,存放在与Notice文件夹同一级的目录下。

获取文件路径

第一步,我们需要获取Notice文件夹下的所有Word文件的路径,这样我们才能对每个文件进行操作。这里我们可以使用glob库的glob函数,它可以根据通配符匹配文件路径,返回一个列表。例如,我们可以使用glob.glob('*.docx')来匹配当前目录下的所有以.docx结尾的文件。

为了方便后续的操作,我们先定义一个变量path,表示我们的工作目录,也就是Notice文件夹和Excel文件所在的目录,你可以根据实际情况修改。然后,我们使用glob.glob(path + r'\Notice\*.docx')来获取Notice文件夹下的所有Word文件的路径,保存在一个变量files中。注意,这里我们使用了r字符串,表示原始字符串,不会对反斜杠进行转义。

代码如下:

1
2
3
4
5
python复制代码import glob

path = r'C:\Users\xxx' # 路径为Notice文件夹和Excel文件所在的目录,可按实际情况更改
files = glob.glob(path + r'\Notice\*.docx') # 获取Notice文件夹下的所有Word文件的路径
print(files) # 打印文件路径列表,检查是否正确

输出如下:

1
text复制代码['C:\\Users\\xxx\\Notice\\会议通知1.docx', 'C:\\Users\\xxx\\Notice\\会议通知2.docx', 'C:\\Users\\xxx\\Notice\\会议通知3.docx', 'C:\\Users\\xxx\\Notice\\会议通知4.docx', 'C:\\Users\\xxx\\Notice\\会议通知5.docx', 'C:\\Users\\xxx\\Notice\\会议通知6.docx', 'C:\\Users\\xxx\\Notice\\会议通知7.docx']

解析Word文件

第二步,我们需要解析每个Word文件,获取需要的四个信息,即学习时间、学习内容、学习形式、主持人。这里我们可以使用python-docx库的Document类,它可以读取Word文件,并将其内容以段落(Paragraph)为单位进行划分。我们可以遍历每个段落,根据其文本内容,判断是否包含我们需要的信息,并提取出来。

为了方便后续的操作,我们先定义一个变量number,表示序号,用于记录每个文件的顺序。然后,我们使用一个for循环,遍历files列表中的每个文件路径,使用Document类打开每个文件,保存在一个变量wordfile中。接着,我们定义一个空列表content_lst,用于存放学习内容,因为学习内容可能分散在多个段落中,我们需要将它们合并成一个字符串。然后,我们使用一个嵌套的for循环,遍历wordfile中的每个段落,根据其文本内容,判断是否包含我们需要的信息,并提取出来,保存在相应的变量中。具体的判断逻辑如下:

  • 如果段落的文本以“学习时间:”开头,那么我们就提取其后面的部分,保存在一个变量study_time中。
  • 如果段落的文本以“主持人:”开头,那么我们就提取其后面的部分,保存在一个变量host中。
  • 如果段落的文本以“学习形式:”开头,那么我们就提取其后面的部分,保存在一个变量study_type中。
  • 如果段落的文本的长度大于等于2,且第一个字符是数字,第二个字符是中文顿号“、”,那么我们就认为这是学习内容的一部分,将其添加到content_lst中。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码from docx import Document

number = 0 # 定义一个变量,表示序号
for file in files: # 遍历每个文件路径
wordfile = Document(file) # 打开每个文件
content_lst = [] # 定义一个空列表,用于存放学习内容
for paragraph in wordfile.paragraphs: # 遍历每个段落
if paragraph.text[0:5] == '学习时间:': # 如果段落的文本以“学习时间:”开头
study_time = paragraph.text[5:] # 提取其后面的部分,保存在study_time中
if paragraph.text[0:4] == '主持人:': # 如果段落的文本以“主持人:”开头
host = paragraph.text[4:] # 提取其后面的部分,保存在host中
if paragraph.text[0:5] == '学习形式:': # 如果段落的文本以“学习形式:”开头
study_type = paragraph.text[5:] # 提取其后面的部分,保存在study_type中
if len(paragraph.text) >= 2: # 如果段落的文本的长度大于等于2
if paragraph.text[0].isdigit() and paragraph.text[1] == '、': # 如果第一个字符是数字,第二个字符是中文顿号“、”
content_lst.append(paragraph.text) # 将其添加到content_lst中
content = ' '.join(content_lst) # 将content_lst中的元素用空格连接成一个字符串,保存在content中
print(study_time, content, study_type, host) # 打印提取的信息,检查是否正确

写入Excel文件

第三步,我们需要将提取的信息写入Excel文件中,这样我们就可以方便地查看和管理。这里我们可以使用openpyxl库的Workbook类和Worksheet类,它们可以创建和操作Excel文件和工作表。我们可以使用Workbook类的active属性,获取当前活动的工作表,保存在一个变量sheet中。然后,我们可以使用Worksheet类的cell方法,根据行号和列号,指定单元格,并给其赋值。

为了方便后续的操作,我们先定义一个变量excel_file,表示我们要写入的Excel文件的路径,你可以根据实际情况修改。然后,我们使用Workbook类创建一个Excel文件对象,保存在一个变量wb中。接着,我们使用wb的active属性,获取当前活动的工作表,保存在一个变量sheet中。然后,我们使用sheet的cell方法,给第一行的每一列赋值,作为表头,分别是序号、学习时间、学习内容、学习形式、主持人。接下来,我们使用一个for循环,遍历files列表中的每个文件路径,使用Document类打开每个文件,保存在一个变量wordfile中。然后,我们使用和上一步相同的逻辑,提取每个文件中的四个信息,保存在相应的变量中。最后,我们使用sheet的cell方法,给每一行的每一列赋值,分别是序号、学习时间、学习内容、学习形式、主持人。注意,这里我们需要将序号加1,因为第一行已经被表头占用了。最后,我们使用wb的save方法,保存Excel文件。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
python复制代码from openpyxl import Workbook

excel_file = path + r'\Meeting_temp.xlsx' # 定义Excel文件的路径,可按实际情况更改
wb = Workbook() # 创建一个Excel文件对象
sheet = wb.active # 获取当前活动的工作表
sheet.cell(row=1, column=1).value = '序号' # 给第一行第一列赋值,作为表头
sheet.cell(row=1, column=2).value = '学习时间' # 给第一行第二列赋值,作为表头
sheet.cell(row=1, column=3).value = '学习内容' # 给第一行第三列赋值,作为表头
sheet.cell(row=1, column=4).value = '学习形式' # 给第一行第四列赋值,作为表头
sheet.cell(row=1, column=5).value = '主持人' # 给第一行第五列赋值,作为表头
for file in files: # 遍历每个文件路径
wordfile = Document(file) # 打开每个文件
content_lst = [] # 定义一个空列表,用于存放学习内容
for paragraph in wordfile.paragraphs: # 遍历每个段落
if paragraph.text[0:5] == '学习时间:': # 如果段落的文本以“学习时间:”开头
study_time = paragraph.text[5:] # 提取其后面的部分,保存在study_time中
if paragraph.text[0:4] == '主持人:': # 如果段落的文本以“主持人:”开头
host = paragraph.text[4:] # 提取其后面的部分,保存在host中
if paragraph.text[0:5] == '学习形式:': # 如果段落的文本以“学习形式:”开头
study_type = paragraph.text[5:] # 提取其后面的部分,保存在study_type中
if len(paragraph.text) >= 2: # 如果段落的文本的长度大于等于2
if paragraph.text[0].isdigit() and paragraph.text[1] == '、': # 如果第一个字符是数字,第二个字符是中文顿号“、”
content_lst.append(paragraph.text) # 将其添加到content_lst中
content = ' '.join(content_lst) # 将content_lst中的元素用空格连接成一个字符串,保存在content中
number += 1 # 将序号加1
sheet.cell(row=number+1, column=1).value = number # 给每一行第一列赋值,作为序号
sheet.cell(row=number+1, column=2).value = study_time # 给每一行第二列赋值,作为学习时间
sheet.cell(row=number+1, column=3).value = content # 给每一行第三列赋值,作为学习内容
sheet.cell(row=number+1, column=4).value = study_type # 给每一行第四列赋值,作为学习形式
sheet.cell(row=number+1, column=5).value = host # 给每一行第五列赋值,作为主持人
wb.save(excel_file) # 保存Excel文件

完成任务

至此,我们已经完成了用Python批量提取Word文件中的关键信息,并写入Excel文件中的任务。你可以打开Meeting_temp.xlsx文件,查看结果,如下图所示:

你可以看到,我们成功地将7个Word文件中的四个信息提取出来,并按照序号、学习时间、学习内容、学习形式、主持人的顺序,写入Excel文件中,方便查阅和管理。

总结

本文教你如何用Python批量提取Word文件中的关键信息,并写入Excel文件中,只需几行代码,就可以节省大量的时间和精力。本文涉及到的主要技术点有:

  • openpyxl库:用于操作Excel文件,可以创建、读取、写入、修改Excel文件和工作表。
  • python-docx库:用于操作Word文件,可以读取、写入、修改Word文件和段落。
  • glob库:用于获取文件路径,可以根据通配符匹配文件路径,返回一个列表。

希望本文对你有所帮助,如果你有任何问题或建议,欢迎在评论区留言。谢谢!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Apache NiFi 200构建Python处理器

发表于 2024-04-26

Apache NiFi 最新版本中内置的 Python 处理器可以简化数据处理任务,增强灵活性并加快开发速度。

译自Apache NiFi 2.0.0: Building Python Processors,作者 Robert Kimani。

Apache NiFi是一个专门用于数据流管理的强大平台,它提供了许多旨在提高数据处理效率和灵活性的功能。其基于 Web 的用户界面为设计、控制和监控数据流提供了无缝体验。

NiFi 支持构建自定义处理器和扩展,使用户能够根据自己的特定需求定制平台。

凭借多租户用户体验,NiFi 确保多个用户可以同时与系统交互,每个用户都有自己的一组访问权限。

Python 处理器提供了一种强大的方式来扩展 NiFi 的功能,使用户能够在数据流中利用丰富的Python库和工具生态系统。在这里,我们将讨论将 Python 纳入 NiFi 工作流的优势,并探讨 Python 处理器可以简化数据处理任务、增强灵活性和加速开发的实际用例。

无论您是想集成机器学习算法、执行自定义数据转换还是与外部系统交互,在 Apache NiFi 中构建 Python 处理器都可以帮助您满足这些数据集成需求。

NiFi 的一个突出特点是其高度可配置的特性,允许用户根据其特定要求定制数据路由、转换和系统中介逻辑。NiFi 帮助用户实现他们想要的数据处理结果,例如优先考虑容错性而不是保证交付,或者针对低延迟而不是高吞吐量进行优化。

动态优先级确定允许实时调整流中的数据优先级,而运行时修改流的能力为适应不断变化的需求增加了一层灵活性。NiFi 还结合了反压机制来调节数据流速并防止过载,确保即使在不同的工作负载下也能平稳高效地运行。

NiFi 被设计为支持垂直和水平扩展。无论是扩展以利用单台机器的全部功能,还是使用零领导者集群模型进行扩展,NiFi 都可以适应任何规模的数据处理任务。

数据来源是另一个关键特性,它允许用户跟踪数据从其开始到最终目的地的旅程。这为审计、故障排除和确保整个过程中的数据完整性提供了宝贵的见解。

安全性在 NiFi 中至关重要,它支持 SSL、SSH、HTTPS 和加密内容以及其他安全措施。可插拔的细粒度基于角色的身份验证和授权机制确保对数据流的访问受到仔细控制,允许多个团队安全地管理和共享流的特定部分。

NiFi 的设计理念受到基于流的编程和分阶段事件驱动架构等概念的启发,提供了几个引人注目的优势:

  • 直观的可视化界面,用于设计和管理数据流,提高生产力和易用性。
  • 异步处理模型,支持高吞吐量和自然缓冲,以适应波动的负载。
  • 内置并发管理,抽象了多线程编程的复杂性。
  • 强调组件的可重用性和可测试性,促进模块化和稳健的设计方法。
  • 本机支持反压和错误处理,确保数据处理管道中的稳健性和可靠性。
  • 全面了解数据流动态,实现有效的监控和故障排除。

为什么在 Apache NiFi 中使用 Python 构建?

Apache NiFi是一个用于数据摄取、转换和路由的强大工具。NiFi 中的 Python 处理器提供了一种灵活的方式来扩展其功能,特别是对于处理非结构化数据或与外部系统(如 AI 模型或云原生向量数据库Milvus等向量存储)集成。

在处理Cloudera Data Flow等工具可提取的非结构化文件类型时,Python 处理器对于实现解析和操作数据的自定义逻辑而言至关重要。例如,你可以使用 Python 从文本文件中提取特定信息,对文本数据执行情感分析或者在进行进一步分析之前对图像进行预处理。

另一方面,结构化文件类型通常可以使用 NiFi 的内置处理器进行处理,而无需自定义 Python 代码。NiFi 提供了广泛的处理器,用于处理 CSV、JSON、Avro 等结构化数据格式,以及用于与数据库、API和其他企业系统进行交互。

当你需要与 AI 模型或 Milvus 等其他外部系统进行交互时,Python 处理器提供了一种便捷的方式,可以将此功能集成到你的 NiFi 数据流中。对于文本到文本、文本到图像或文本到语音处理等任务,你可以编写 Python 代码与相关模型或服务进行交互,并将此处理合并到你的 NiFi 管道中。

Python:NiFi 2.0.0 中的新时代

Apache NiFi 2.0.0对该平台进行了一些重大改进,尤其是在 Python 集成和性能增强方面。将 Python 脚本无缝集成到 NiFi 数据流中的能力为使用各种数据源和利用生成式 AI 的强大功能开辟了广泛的可能性。

在此版本之前,虽然可以在 NiFi 中使用 Python,但灵活性可能受到限制,并且执行 Python 脚本可能不像用户希望的那样精简。然而,使用最新版本,Python 集成得到了极大改善,允许在 NiFi 管道中更无缝地执行 Python 代码。

此外,对 JDK 21+ 的支持带来了性能改进,使 NiFi 更快、更高效,尤其是在处理多线程任务时。这可以显著提高 NiFi 数据流的可扩展性和响应能力,尤其是在处理大量数据或复杂处理任务时。

引入诸如将进程组作为无状态运行和规则引擎用于开发辅助等功能进一步增强了 NiFi 的功能和可用性,为开发人员提供了更多灵活性和工具来构建强大的数据流管道。

一个示例处理器:Watson SDK 到基础 AI 模型

此 Python 代码定义了一个名为的 NiFi 处理器,它与 IBM WatsonX AI 服务进行交互,以根据输入提示生成响应。请注意,对于 NiFi 2.0.0,Python3.10+ 是最低要求。

下面我们分解一下代码,并解释各个部分。

导入

1
2
3
4
javascript复制代码import json
import re
from nifiapi.flowfiletransform import FlowFileTransform, FlowFileTransformResult
from nifiapi.properties import PropertyDescriptor, StandardValidators, ExpressionLanguageScope

以下是脚本的必要导入:

  • json 和 re 分别是 Python 的用于分别处理 JSON 数据和正则表达式的内置模块。
  • FlowFileTransform 和 FlowFileTransformResult 是与 NiFi 处理相关的自定义模块 (nifiapi.flowfiletransform) 的类。
  • PropertyDescriptor、StandardValidators 和 ExpressionLanguageScope 是用于定义处理器属性的另一个自定义模块 (nifiapi.properties) 的类。

类定义

1
2
kotlin复制代码class CallWatsonXAI(FlowFileTransform):
...
  • 这将定义一个名为 CallWatsonXAI 的类,它扩展 了FlowFileTransform 类,该类处理 NiFi 中的数据转换。

处理器详细信息

1
2
3
4
5
6
arduino复制代码processor_details = {
'name': 'Call WatsonX AI',
'version': '2.0.0-M2',
'description': 'Calls IBM WatsonX AI service to generate responses based on input prompts.',
'tags': ['watsonx', 'ai', 'response', 'generation'],
}
  • 定义处理器的详细信息,例如版本、描述和标记。但请注意,2.0.0-M2 是当前版本。

属性描述符

1
2
3
4
5
6
7
8
9
10
ini复制代码PROMPT_TEXT = PropertyDescriptor(
name="Prompt Text",
description="Specifies whether or not the text (including full prompt with
context) to send",
required=True,
validators=[StandardValidators.NON_EMPTY_VALIDATOR],

expression_language_scope=ExpressionLanguageScope.FLOWFILE_ATTRIBU
TES
)
  • 定义可以为该处理器设置的特性。在这种情况下,有 PROMPT_TEXT、WATSONXAI_API_KEY 和 WATSONXAI_PROJECT_ID。

构造函数

1
2
3
4
5
ruby复制代码def __init__(self, **kwargs):
super().__init__()
self.property_descriptors.append(self.PROMPT_TEXT)
self.property_descriptors.append(self.WATSONXAI_API_KEY)
self.property_descriptors.append(self.WATSONXAI_PROJECT_ID)
  • 初始化处理器类并将属性描述符附加到属性列表中。

getPropertyDescriptors 方法

1
2
ruby复制代码def get_property_descriptors(self):
return self.property_descriptors
  • 该方法由 NiFi 处理器要求,用于获取属性列表。

transform 方法

1
2
ruby复制代码def transform(self, context, flowfile):
...
  • 该方法负责处理数据。方法接收包含关于处理器执行环境的信息的上下文对象和包含将处理的数据的流文件对象。

IBM WatsonX 集成

1
2
3
javascript复制代码from ibm_watson_machine_learning.foundation_models.utils.enums import 
ModelTypes
from ibm_watson_machine_learning.foundation_models import Model
  • 导入 IBM Watson 机器学习模块。
1
2
3
4
5
6
7
8
9
scss复制代码prompt_text = 
context.getProperty(self.PROMPT_TEXT).evaluateAttributeExpressions(flowfil
e).getValue()
watsonx_api_key =
context.getProperty(self.WATSONXAI_API_KEY).evaluateAttributeExpressions(
flowfile).getValue()
project_id =
context.getProperty(self.WATSONXAI_PROJECT_ID).evaluateAttributeExpres
sions(flowfile).getValue()

通过 NiFi 处理器属性获取输入值,例如提示文本、WatsonX API 密钥和项目 ID。

1
2
3
4
5
6
7
8
9
ini复制代码model_id = ModelTypes.LLAMA_2_70B_CHAT
gen_parms = None
project_id = project_id
space_id = None
verify = False

model = Model(model_id, my_credentials, gen_parms, project_id, space_id, verify)
gen_parms_override = None
generated_response = model.generate(prompt_text, gen_parms_override)
  • 配置并调用 IBM WatsonX 模块来根据提示文本生成响应。

输出处理

1
2
ini复制代码attributes = {"mime.type": "application/json"}
output_contents = json.dumps(generated_response)
  • 定义输出属性,将生成的响应转换为 JSON 格式。

日志记录和返回

1
lua复制代码self.logger.debug(f"Prompt: {prompt_text}")
  • 记录提示文本。
1
2
ini复制代码return FlowFileTransformResult(relationship="success", 
contents=output_contents, attributes=attributes)

返回转换结果,并指示转换是否成功并提供输出数据和属性。

预打包的 Python 处理器

NiFi 2.0.0 附带了一组多样化的 Python 处理器,它们提供了广泛的功能。

  • Pinecone 的 VectorDB 接口:此处理器促进了与Pinecone(一种矢量数据库服务)的交互,使用户能够高效地查询和存储数据。
  • ChunkDocument:此处理器将大型文档分解为较小的块,使其适合于处理和存储,尤其是在可能应用大小限制的矢量数据库中。
  • ParseDocument:此处理器似乎非常通用,能够解析各种文档格式,如 Markdown、PowerPoint、Google Docs 和Excel,提取文本内容以供进一步处理或存储。
  • ConvertCSVtoExcel:顾名思义,此处理器将数据从 CSV 格式转换为 Excel 格式,为数据交换和处理提供了灵活性。
  • DetectObjectInImage:此处理器似乎利用深度学习技术进行图像中的对象检测,使用户能够分析图像数据并提取有价值的见解。
  • PromptChatGPT:此处理器听起来很有趣——它与 ChatGPT 或类似的会话式 AI 模型集成,使用户能够根据提示生成响应或参与对话。
  • PutChroma 和 QueryChroma:这些处理器与Chroma(一种针对大型语言模型 (LLM) 的开源数据库)相关。它们促进了 Chroma 数据库或类似系统中的数据存储(PutChroma)和检索/查询(QueryChroma)。

结论

在 Apache NiFi 中优先考虑 Python 集成标志着弥合数据工程师和数据科学家之间差距的一个重要里程碑,同时扩展了该平台的多功能性和适用性。

通过使 Python 爱好者能够在 Python 中无缝开发 NiFi 组件,开发周期得到简化,从而加速了数据管道和工作流的实施。

对于NiFi 中的 Python 处理器来说,这是一个激动人心的时刻,为生态系统做出贡献可能非常有价值。开发和共享 Python 处理器可以扩展 NiFi 的功能,并解决特定用例。

要开始使用 NiFi,用户可以参考快速入门指南进行开发,并参考NiFi 开发人员指南以获取有关如何为该项目做出贡献的更全面信息。

本文在云云众生(yylives.cc/)首发,欢迎大家访问。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据结构:树与二叉树(概念和特性) 前言 树 二叉树 小结

发表于 2024-04-26

前言

在计算机科学的领域中,树与二叉树是极为重要且基础的结构。它们不仅具有理论上的深度与魅力,更在实际应用中发挥着关键作用。理解树与二叉树的概念、特性和操作方法,是掌握众多数据结构与算法的基石。

通过对树与二叉树的深入研究和总结,我们能够更好地洞察数据的组织与管理方式,为解决各种复杂问题提供有力的工具和思路。

树

树的定义

树是一种具有层次结构的数据结构,它由结点组成,有且仅有一个根结点,每个结点可以有零个或多个子结点,除根结点外,子结点与父结点通过边相连,树可以有多个分支,没有子结点的结点为叶子结点。树是n(n>0)个结点的有限集。当n=0时,称作空树。树的根结点没有前驱,除根结点外的所有结点有且仅有一个前驱,树所有的结点可以有零个或多个后继。

任意混淆的地方:

特性 特性 特性
度为m的树 任意结点的度<=m 至少有一个结点的度=m 一定是非空树,至少有m+1个结点
m叉树 任意结点的度<=m 允许所有结点的度都<m 可以是空树

树的特性

  1. 树中的结点数等于所有结点数的度数加1。

解释:结点数 = 所有结点数的度数 + 1;你也可以这样认为:结点数=树的所有边数+1。
2. 度为m的树中第i层上至多有mi−1m^{i-1}mi−1 个结点(i大于等于1)。

解释:依次举例就可以看透规律。
3. 高度为h的m叉树至少有h个结点。

解释:因为高度为h,每一层必须有一个结点。
4. 高度为h的m叉树至多有(mkm^{k}mk-1)/(m-1)个结点。

解释:把每层个数相加,形成一个等比数列求和就可以得到这个式子。
5. 高度为h、度为m的树至少有(h+m-1)个结点。

解释:高度为h:h层中每层都有一个结点;度为m:树中至少有一个结点的度为m;要求至少的结点,我们就可以去除其他结点只保留h个保持高度,在一层中最少要保持有m个结点,因为在有m个结点的那一层中有一个结点是用来保持高度为h的,这样就可以得到h+m-1的式子了。
6. 具有n个结点的m叉树的最小高度为⌈log⁡m(n(m−1)+1)⌉ \lceil\log_m(n(m-1)+1)\rceil⌈logm(n(m−1)+1)⌉。

解释:(==前h-1层有(mh−1m^{h-1}mh−1-1)/(m-1)个结点 < n <= 前h层(mhm^{h}mh-1)/(m-1))个结点==,我们对这个不等式进行操作,把h变量放在两边得:==h-1 < log⁡m(n(m−1)+1)\log_m(n(m-1)+1)logm(n(m−1)+1) <=h==,最终得出最小高度为⌈log⁡m(n(m−1)+1)⌉ \lceil\log_m(n(m-1)+1)\rceil⌈logm(n(m−1)+1)⌉。

二叉树

二叉树的定义

二叉树是一种重要的数据结构,它是由结点组成的树状结构。每个结点最多有两个子结点,即左子结点和右子结点。二叉树可以是空树,即没有任何结点;也可以由一个根结点以及它的左子树和右子树构成。二叉树的的子树有左右之分,其次序不能任意颠倒。

特殊的二叉树

  1. 满二叉树:
    • 定义:一颗高度为h,且含有2k−12^{k}-12k−1个结点的二叉树称为满二叉树。
    • 特点:对于编号为i的结点
      • 双亲编号为⌊i2⌋\lfloor\frac{i}{2}\rfloor⌊2i⌋。
      • 若有左孩子,则左孩子编号为2*i。
      • 若有右孩子,则右孩子编号为2*i-1。
  2. 完全二叉树:
    • 定义:高度为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时称为完全二叉树。
    • 特点:
      • 叶子结点只可能在层次最大的两层上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。
      • 若有度为1的结点,则只可能有一个,且该结点只有左结点没有右结点。
      • 按层次编号后,一旦出现某编号为i的结点为叶子节点或只有左孩子,则编号大于i的结点都是叶子结点。
      • 若n(结点数)为奇数,则每个分支结点都有左孩子和右孩子。
      • 若n(结点数)为偶数,则编号最大的分支结点只有左孩子,没有右孩子,其他分支结点左右孩子都有。
  3. 二叉排序树:
    • 定义:左子树上所有的关键字均小于根结点的关键字;右子树上的所有结点的关键字均大于根结点的关键字;左子树和右子树又各是一颗二叉排序树。
  4. 平衡二叉树:
    • 定义:树上任一结点的左子树和右子树的深度之差不超过1。

二叉树的性质

  1. 非空二叉树上的叶子结点数等于度为2的结点数加1。

过程:n0为度为0的结点数、n1为度为1的结点数、n2为度为2的结点数、n为总结点数

* n0+n1+n2=n
* n1+2\*n2+1=n (树的特性:结点数 = 所有结点数的度数 + 1) 结合2个等式可得:n2+1=n0
  1. 非空二叉树上第k层上至多有2k−12^{k-1}2k−1个结点。
  2. 高度为h的二叉树至多有2k−12^{k}-12k−1个结点。
  3. 对完全二叉树按从上到下,从左到右的顺序依次编号1,2,……,n,则有一下关系:
* 当i>1时,结点i的双亲的编号为⌊i2⌋\lfloor\frac{i}{2}\rfloor⌊2i⌋,即当i为偶数时,其双亲的编号为i/2,它是双亲的左孩子;当i为奇数时,其双亲的编号为(i-1)/2,它是双亲的右孩子。
* 当2i<=n时,结点i的左孩子编号为2i,否则无左孩子。
* 当2i+1<=n时,结点i的右孩子编号为2i+1,否则无右孩子。
* 结点i所在层次为⌊log⁡2i⌋+1\lfloor\log\_2 i\rfloor+1⌊log2i⌋+1。
  1. 具有n个(n>0)结点的完全二叉树的高度为⌈log⁡2(n+1)⌉\lceil\log_2(n+1)\rceil ⌈log2(n+1)⌉或⌊log⁡2i⌋+1\lfloor\log_2 i\rfloor+1⌊log2i⌋+1。

过程:h为所在高度

* 通过(前h-1层结点数)2h−1−12^{h-1}-12h−1−1<n<=2h−12^{h}-12h−1(前h层结点数),化简可得h-1<log⁡2(n+1)\log\_2(n+1)\\log2(n+1)<=h,因为h为正数可得h=⌈log⁡2(n+1)⌉\lceil\log\_2(n+1)\rceil ⌈log2(n+1)⌉。
* 上式(前h-1层结点数+1)2h−12^{h-1}2h−1<=n<2h2^{h}2h(前h层结点数-1),化简可得h-1<=log⁡2n\log\_2n\\log2n<h,因为h为正数可得h=⌊log⁡2i⌋+1\lfloor\log\_2 i\rfloor+1⌊log2i⌋+1。
  1. 对于完全二叉树,可以由结点数n推出度为0,1和2的结点个数为n0、n1和n2。
* 完全二叉树最多只有一个度为1的结点,即n1为0或1。
* 因为n2+1=n0,所以n2+n0 = 2\*n2 +1为奇数。
* 由上面两点可得:当完全二叉树有2k(偶数)个结点,则n1=1 ,n0=k,n2=k-1;当完全二叉树有2k-1(奇数)个结点,则n1=0,n0=k,n2=k-1。

小结

了解了树与二叉树的概念与特性,我们就可以深入了解树的遍历和树的应用等。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用于YouTube推荐的深度神经网络YouTube DNN

发表于 2024-04-26

这篇论文是最近参加组会看的一篇新论文,论文虽然是2016年出的论文,但是它是YouTube发表的,且是应用在YouTube这样超级大的平台上的一篇工业界的推荐系统的论文,我读完之后也觉得论文有一些可取之处的,所以和大家分享一下我的组会ppt汇报。

YouTube是世界上最大的视频创作及分享平台,其视频推荐面临的主要问题有:

  • Scale(用户量巨大):现有的推荐算法通常在小数据集有良好的表现,但是在YouTube这样规模的数据集上未必表现良好。
  • Freshness(新鲜度):YouTube系统每秒钟都有大量新视频上传(实时性),推荐系统应该能够快速的对视频及用户行为作出反馈,并平衡新老视频的综合推荐。
  • Noise(噪声): 用户的历史行为往往是稀疏的并且不完整的,同时视频本身很多数据都是非结构化的,这两点对推荐算法的鲁棒性提出了挑战。

Youtube的用户推荐场景自不必多说,作为全球最大的UGC(用户生成内容)视频网站,需要在百万量级的视频规模下进行个性化推荐。由于候选视频集合过大,考虑online系统延迟问题,不宜用复杂网络直接进行推荐,所以Youtube采取了两层深度网络完成整个推荐过程:

  • Candidate Generation Model 即候选生成模型(也叫召回模型)
  • Ranking Model 排序模型

下图是本文的推荐系统模型架构图,视频池子中包含有亿级别个视频,当经过第一层候选生成层模型时,这层主要是做粗略的筛选,视频被过滤为只有百级别个视频,再次经过第二层模型时,这层会将视频进行打分排序,然后筛选出前几十个视频,输出到用户的视频主页上,供用户观看,如下图所示:

  1. CANDIDATE GENERATION 候选生成模型

这一层模型用两个字概括就是“粗糙”,将视频池子中的数百亿的视频,经过第一层筛选得到数百个视频,传入到下一层模型进一步训练。

下图是候选生成模型的的模型架构图,从下往上看,将浏览历史、搜索历史特征嵌入为高层embedded向量,还会加入其他有关用户画像的特征,如地理位置、年龄、性别等特征,将这些人口统计学信息归一化到[0,1]范围中,与前面的特征进行拼接,然后将所有的特征喂给上层的神经网络模型,经过三层神经网络后,我们可以看到softmax函数。这里YouTube把这个问题看作为用户推荐next watch的问题,是一个多分类的问题。

候选集生成建模为一个“超大规模多分类”问题其分类公式如下图式所示:

在时间t时,用户U在其特定的上下文C,对指定的视频wt准确的划分到第i类中。

其中u是用户和其上下文的高维embedded嵌入,v是视频的高维embedded嵌入。

针对该架构,有如下几点值得关注:

(1) 主要特征的处理1:watch and search 浏览历史和搜索历史。

视频存在于手机等设备上,是一串稀疏的,变长的视频ID序列,通过固定的词汇表,经过加权平均,可以基于其重要性或时间,得到一串稠密的,定长的观看向量,输入到上层神经网络中,如下图所示:

主要特征的处理2:example age 视频上传时间

如下图所示,横坐标是视频上传的时间,轴坐标是类别概率,在没有加入到example age特征时,可以注意到这条蓝色的线,一直表现的很平平,加入了此特征,类别的概率更加符合按经验分布,说明这个特征的重要性。

(2) 上下文选择

用户观看视频时,遵循一种asymmetric co-watch非对称共同观看,用户在浏览视频时,往往都是序列式的,开始看一些比较流行的逐渐找到细分的视频。

如下图所示,(a)图是利用上下文信息预估中间的一个视频,(b)图是利用上下文信息,预估下一次浏览的视频,显然,预测下一次浏览的视频比预测中间的一个视频要简单的多。

通过实验也可以发现,图(b)的方式在线上A/B test中表现更佳,而实际上,传统的协同过滤类算法,都是隐含采用图(a)方式,忽略了不对称的浏览方式。

(3) 特征集以及深度的实验

如下图所示,神经网络模型采用的是经典的“tower”模式搭建网络,所有的视频和搜索历史都embedded到256维的向量中,开始在input层直接全连接到256维的softmax层,然后依次增加网络深度(+512–>+1024–>+2048–> …)

如下图所示,横轴表示深度神经网络的训练深度,纵轴表示平均精度,自变量是特征的数量,随着特征数量的增加,其平均精度呈上涨趋势,当深度神经网络达到第三层时,其平均精度不在出现”大幅度“增加。

当然考虑到CPU计算及用户可等待时长,深度神经网络的层数也不宜过多,基于实验,神经网络取3层就够了,除非以后的计算上来了才无限的增加深度神经网络的训练层数。

  1. RANKING 排序模型

将上一层得到的几百个模型经过排序模型,评分排序,输出前面的几十个视频到用户的手机上,供用户观看。

这一层模型架构乍一看和第一层候选生成层模型一样,但是,又有一些区别。增加了很多其他的更多精细的特征,用来进行个性化的筛选。

会有人说,干嘛废那劲啊,直接一个弄一个深度神经网络模型不就行了吗,将所有的视频一起打分排序,然后输出前几十个视频到用户的主页上展示,供用户观看不就行了。

这样确实可以,一个模型解决,多方便呀。但是,你可能没有考虑一个问题,就是现在的CPU有这么高的计算能力供你计算吗?对所有的用户,所有的视频分别进行排序打分,服务器能不能承受的住,CPU计算会不会暴,就算计算能够承受下来,那会消耗多少时间,在线刷视频的用户等不等得起,毫无疑问,用户没有这么多耐心,等这个推荐系统跑完,就会换别的软件了。

而且,实在是没有必要,连第一层粗排都过不了的视频,在打分排序阶段,分数肯定太低,排不到前面去的。

由上图所示,排序模型架构图也是自下而上的观看,新加入的特征有当前要计算的视频ID,用户光看过的最后N个ID,用户的语言,视频的语言作为语言嵌入,还有自上次观看同频道视频的时间,该视频已经曝光给该用户的次数等特征,将这些特征拼接起来,然后喂给上层的神经网络模型,训练时用加权逻辑回归为每一个视频独立打分,按照得分排序。

针对排序DNN,有如下几点需要关注:

(1) 用户对频道的喜爱程度可以通过用户对该频道的视频的点击次数来度量,用户在该频道的上次观看时间等。

(2) 类别特征embedding:作者为每一个类别特征维度生成一个独立的embedding空间。值得注意的是,对于相同域的特征可以共享embedding,其优势在于加速迭代,降低内存开销。

(3) 连续特征归一化:DNN对于输入的分布及特征的尺度敏感。作者设计一种积分函数将特征映射为一个服从[0,1]分布的变量。该积分函数为:

(4) 建模预期观看时长:正样本:视频被展示并被点击;按照观看时长进行加权负样本:视频被展示但未被点击;单位权重作者采用一种加权逻辑回归基于交叉熵损失进行训练。逻辑回归几率为:

那么学习概率是:E[T]*(1+P) ≈ E[T] [预期观看时间,点击率]

(5)第5个特征#previous impressions一定程度上引入了exploration的思想,避免同一个视频持续对同一用户进行无效曝光。一般来说,用户观看完一个视频后,再次观看同视频的概率大大降低,因此并不需要再次推送此视频给用户。

  1. 隐藏层实验

下图是作者进行的神经网络模型隐藏层层数的实验,可以看到随着隐藏层的层数和宽度的增加,其对于每个用户的加权损失降低,到实验达到1024–>512–>256这个网络时,其损失不在“大幅度”的减少,故选择这个配置的隐藏层即可,损失为34.6%。

而对于1024–>512–>256这个网络,测试的不包含归一化后根号和平方的版本,loss增加了0.2%。而如果把weighted LR替换成LR,效果下降达到4.1%。

  1. CONCLUSIONS 总结

本文最突出的贡献在于如何结合业务实际和用户场景,选择等价问题,实现推荐系统。

首先,深度协同过滤模型能够有效地吸收更多特征,并对它们与深度层的相互作用进行建模,优于以前在YouTube上使用的矩阵分解方法。

其次,作者对特征的处理策略充满智慧。比如,对example age特征的加入,消除了对过去的固有偏见,并允许模型表示流行视频的时间依赖行为。

最后,排序阶段,对评估指标的选择能够结合业务,取期望观看时间进行训练。

  1. 个人的一些想法

这篇论文是2016年,YouTube发表的推荐系统的会议文章。

YouTube的推荐系统仅仅使用了两个模型,使用深度学习神经网络的方法进行的推荐,整体来说模型的稍微有点简单了,但是胜在快。自己使用了一下YouTube这个软件,发现其推荐算法确实有点缺点,你刷什么视频,就一直给你推荐相关视频,过于单一了,就单从推荐算法来说,还是国内的某些软件上推荐的好。可是毕竟是16年的推荐算法了,且是首次使用深度学习神经网络应用在推荐系统上,而且是已经在工业上实施的论文,可以公开让我们读到,还是很让我增长见识的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

cocos creator从零开发幸运转盘

发表于 2024-04-26

开发环境

  • Cocos Creator 2.4.8,安装参考 cocos creator安装
  • TypeScript

创建项目

创建项目

资源导入

新建textures目录并把资源导入。

资源导入

场景设置

新建scripts目录并在其下新建Game脚本。

新建脚本1

新建脚本2

新建scenes目录并在其下新建game场景。

新建场景

双击game场景打开场景,Canvas节点宽高设置成750x1334并适配宽度,再把Game脚本挂载上去。

Canvas设置

UI 布局

Canvas下新建空节点并重命名为table。拖动textures/背景到table节点下并重命名为bg。table节点下新建空节点并重名为root。拖动textures/奖项底到root节点下并重命名为item,X属性设置为-160,Y属性设置为160。item节点下新建Label组件并重命名为text。拖动textures/标识到table节点下并重命名为run。拖动textures/开始按钮到table节点下并重命名为btnStart并挂载Button组件。

item1

item2

开始按钮

代码生成奖项

编辑scripts/Game脚本,内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
typescript复制代码const { ccclass } = cc._decorator


@ccclass
export default class LuckyTable extends cc.Component {
private runNode: cc.Node

private itemNum: number = 12 //奖项数量

private _items: cc.Node[] = []

private _idxLast: number = 0


protected onLoad(): void {
this.runNode = this.node.getChildByName('table').getChildByName('run')

this.initItem()

this._idxLast = this.randRange(0, this._items.length - 1)

this.runNode.setPosition(this._items[this._idxLast].getPosition())

const btnStart = this.node.getChildByName('table').getChildByName('btnStart')
btnStart.on('click', this.onBtnStart, this)
}


private onBtnStart() {
cc.log('点击了开始按钮')
}


private initItem() {
const prefab = this.node.getChildByName('table').getChildByName('root').children[0]
let lastPos = prefab.getPosition()

for (let i = 0; i < this.itemNum; i++) {
const item = cc.instantiate(prefab)
item.getChildByName('text').getComponent(cc.Label).string = `${i + 1}`

lastPos = this.getItemPosition(i, item, lastPos)

item.parent = prefab.parent
item.setPosition(lastPos)

this._items.push(item)
}

prefab.destroy()
}


private getItemPosition(idx: number, item: cc.Node, lastPos: cc.Vec2): cc.Vec2 {
if (idx == 0) return lastPos

const sep = 6 //奖项间隔
const pos = cc.Vec2.ZERO

if (idx >= 1 && idx <= 3) {//上
pos.y = lastPos.y
pos.x = lastPos.x + (item.width + sep)
} else if (idx >= 4 && idx <= 6) {//右
pos.x = lastPos.x
pos.y = lastPos.y - (item.height + sep)
} else if (idx >= 7 && idx <= 9) {//下
pos.y = lastPos.y
pos.x = lastPos.x - (item.width + sep)
} else if (idx >= 10 && idx <= 11) {//左
pos.x = lastPos.x
pos.y = lastPos.y + (item.height + sep)
}

return pos
}


// 整数范围随机 [min, max]
private randRange(min: number, max: number): number {
return Math.round(Math.random() * (max - min) + min)
}
}

运行程序后,发现奖项生成好了,标识也随机显示了,点击点击抽奖按钮也输出了日志。

运行程序

生成奖项

实现转圈效果

编辑scripts/Game脚本,内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
typescript复制代码const { ccclass } = cc._decorator


@ccclass
export default class LuckyTable extends cc.Component {
private runNode: cc.Node

private itemNum: number = 12 //奖项数量
private initSpeed: number = 5000 //初始运行速度

private _items: cc.Node[] = []
private _itemLen: number //两个奖项的距离(宽高一样)

private _idxLast: number = 0

private _isClick: boolean = false

private _runTotal: number //这次转圈总共转的格子
private _runNum: number //这次转圈转了几个格子了

private _runSpeed: number //实时运行速度(最开始跟 initSpeed 一样,每运行一个格子减去 _itemSpeed)
private _itemSpeed: number //每个格子的运行速度


protected onLoad(): void {
this.runNode = this.node.getChildByName('table').getChildByName('run')

this.initItem()

this._itemLen = this._items[0].getPosition().sub(this._items[1].getPosition()).len()

this._idxLast = this.randRange(0, this._items.length - 1)

this.runNode.setPosition(this._items[this._idxLast].getPosition())

const btnStart = this.node.getChildByName('table').getChildByName('btnStart')
btnStart.on('click', this.onBtnStart, this)
}


private onBtnStart() {
if (this._isClick) return
this._isClick = true

const idxTarget = this.randRange(0, this._items.length - 1)
const num = this._idxLast <= idxTarget ? idxTarget - this._idxLast : this._items.length - (idxTarget - this._idxLast)
const round = this.randRange(3, 5)

this._runTotal = this._items.length * round + num
this._runNum = 1

this._runSpeed = this.initSpeed
this._itemSpeed = this.initSpeed / (this._runTotal + 2) //这里至少要加 1, 不然最后一个格子没速度了

cc.log(`${this._idxLast + 1} =====> ${(this._idxLast + this._runTotal) % this._items.length + 1}`)

this.run()
}


private run() {
const time = this._itemLen / this._runSpeed

const idx = (this._idxLast + this._runNum) % this._items.length
const targetItem = this._items[idx]

cc.tween(this.runNode).
to(time, { x: targetItem.x, y: targetItem.y }).
call(() => {
if (this._runNum >= this._runTotal) {
this._isClick = false
this._idxLast = idx
return
}

this._runNum++
this._runSpeed -= this._itemSpeed

this.run()
}).start()
}

private initItem() {
const prefab = this.node.getChildByName('table').getChildByName('root').children[0]
let lastPos = prefab.getPosition()

for (let i = 0; i < this.itemNum; i++) {
const item = cc.instantiate(prefab)
item.getChildByName('text').getComponent(cc.Label).string = `${i + 1}`

lastPos = this.getItemPosition(i, item, lastPos)

item.parent = prefab.parent
item.setPosition(lastPos)

this._items.push(item)
}

prefab.destroy()
}


private getItemPosition(idx: number, item: cc.Node, lastPos: cc.Vec2): cc.Vec2 {
if (idx == 0) return lastPos

const sep = 6 //奖项间隔
const pos = cc.Vec2.ZERO

if (idx >= 1 && idx <= 3) {//上
pos.y = lastPos.y
pos.x = lastPos.x + (item.width + sep)
} else if (idx >= 4 && idx <= 6) {//右
pos.x = lastPos.x
pos.y = lastPos.y - (item.height + sep)
} else if (idx >= 7 && idx <= 9) {//下
pos.y = lastPos.y
pos.x = lastPos.x - (item.width + sep)
} else if (idx >= 10 && idx <= 11) {//左
pos.x = lastPos.x
pos.y = lastPos.y + (item.height + sep)
}

return pos
}


// 整数范围随机 [min, max]
private randRange(min: number, max: number): number {
return Math.round(Math.random() * (max - min) + min)
}
}

再次运行程序并点击点击抽奖按钮,程序已经开始由快转慢转圈了。

资源和代码获取

关注干货悦读公众号,点击源码菜单获取下载链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SDL2(Simple DirectMedia Layer

发表于 2024-04-26

SDL2(Simple DirectMedia Layer 2)游戏开发概述(一)

创建一个横版格斗闯关游戏是一个综合性的项目,涉及到游戏设计、图形处理、输入管理、音频处理等多个方面。

使用C语言和SDL2是一个很好的选择,因为SDL2提供了跨平台的多媒体开发功能,非常适合做2D游戏。

下面是一个基本的项目架构概述:

  1. 项目初始化

环境搭建:确保你的开发环境中已经安装了SDL2及其相关库。

你可能还需要安装SDL_image、SDL_ttf等扩展库以支持图片加载和文字渲染。

项目结构:

src/:存放所有的源代码文件。

assets/:包含图像、音频、字体等资源。

include/:自定义头文件,如游戏对象、系统接口等。

bin/:编译后的可执行文件(根据实际情况可能在不同的目录下)。

  1. 核心模块

2.1 初始化与清理

main.c:程序入口,负责SDL的初始化、窗口创建、事件循环的启动以及游戏结束时的资源清理。

2.2 图形与窗口管理

graphics.c/h:处理窗口管理、渲染上下文的创建、背景绘制、精灵管理等。

2.3 输入处理

input.c/h:处理键盘、鼠标输入,识别玩家的操作,如移动、攻击等。

2.4 游戏对象与实体

entity.c/h:定义游戏角色(玩家、敌人)、静止物体等的基本结构和行为。

包括但不限于生命值、位置、动画状态等属性。

2.5 动画与状态机

animation.c/h:管理角色动画,根据角色状态(如行走、攻击、受伤)切换动画帧。

2.6 物理引擎(简易)

physics.c/h:处理简单的碰撞检测、移动限制等。

2.7 关卡管理

level.c/h:定义关卡结构,包括地形、敌人分布、目标点等。

2.8 音频管理

audio.c/h:管理背景音乐和音效的播放。

  1. 游戏循环

游戏的核心是主循环,通常在main.c中实现,结构如下:

初始化:SDL初始化、窗口创建、加载资源。

事件处理:检查并响应SDL事件,如键盘输入、窗口关闭请求。

更新:更新游戏状态,如角色位置、动画帧、碰撞检测。

渲染:清除屏幕、绘制游戏元素、更新屏幕。

延迟与同步:确保游戏运行在目标帧率。

清理:释放所有资源,关闭SDL。

  1. 开发建议

分步实现:从最基本的显示一个窗口和背景开始,逐步添加角色、动画、输入响应等。

模块化:保持代码模块化,每个部分负责单一职责,便于维护和扩展。

性能考虑:注意内存管理和渲染效率,避免不必要的资源重复加载。

测试:频繁测试各个模块,确保稳定性和兼容性。

  1. 示例代码

基于上述架构,我将提供一些核心部分的简要示例代码。请注意,这些只是基础示例,实际项目中需要更详细的错误处理、资源管理等。

1. 初始化SDL和窗口

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
c复制代码#include <SDL2/SDL.h>
#include "graphics.h"
#include "input.h"

int main(int argc, char* argv[]) {
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
printf("SDL could not initialize! SDL Error: %s\n", SDL_GetError());
return -1;
}

SDL_Window* window = SDL_CreateWindow("横版格斗游戏",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
800, 600, SDL_WINDOW_SHOWN);
if (window == NULL) {
printf("Window could not be created! SDL Error: %s\n", SDL_GetError());
SDL_Quit();
return -1;
}

// 假设你有初始化图形和输入的函数
initGraphics(window);
initInput();

bool quit = false;
SDL_Event e;

while (!quit) {
while (SDL_PollEvent(&e) != 0) {
handleEvent(&e, &quit); // 假设这是处理事件的函数
}

updateGame(); // 更新游戏状态
renderGame(); // 渲染游戏画面
SDL_Delay(16); // 简单的帧率控制
}

// 清理资源
cleanupGraphics();
SDL_DestroyWindow(window);
SDL_Quit();

return 0;
}

2. 简易图形初始化

graphics.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
c复制代码void initGraphics(SDL_Window* window);
void renderGame();
void cleanupGraphics();
``

graphics.c

```c
#include "graphics.h"
#include <SDL2/SDL_Renderer.h>

SDL_Renderer* gRenderer = NULL;

void initGraphics(SDL_Window* window) {
gRenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (gRenderer == NULL) {
printf("Renderer could not be created! SDL Error: %s\n", SDL_GetError());
SDL_DestroyWindow(window);
SDL_Quit();
}
}

void renderGame() {
SDL_SetRenderDrawColor(gRenderer, 0xFF, 0xFF, 0xFF, 0xFF);
SDL_RenderClear(gRenderer);
// 这里应该调用渲染角色、背景等的函数
SDL_RenderPresent(gRenderer);
}

void cleanupGraphics() {
SDL_DestroyRenderer(gRenderer);
}

注意:实际开发中,handleEvent, updateGame, 和 renderGame 需要具体实现,

涉及到游戏逻辑、状态管理、动画更新等复杂内容。

上述代码没有包含错误处理的完整细节,实际应用中应确保对每一个可能的失败操作进行检查。

资源加载(如纹理、音频)和管理也非常重要,但为了简化示例未展示。

对于更复杂的逻辑,如动画管理、物理引擎、游戏对象的完整生命周期管理,

你可能需要更多的模块和更细致的设计。

我们可以进一步探讨如何实现游戏中的关键组件,比如简单的角色移动和输入处理。

这里提供一个简化的角色移动示例和输入处理的逻辑,这将建立在之前的基础之上。

3. 简化角色移动

首先,你需要定义一个角色结构体,并在游戏状态中跟踪它。

这里仅展示概念,实际应用中角色类可能会包含更多属性和方法。

entity.h

1
2
3
4
5
6
c复制代码typedef struct {
SDL_Rect position; // 位置
int speed; // 移动速度
} Entity;

Entity player;

input.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c复制代码#include "input.h"
#include <SDL2/SDL.h>
#include "entity.h"

void handleEvent(SDL_Event* e, bool* quit) {
switch (e->type) {
case SDL_QUIT:
*quit = true;
break;
case SDL_KEYDOWN:
switch (e->key.keysym.sym) {
case SDLK_LEFT:
player.position.x -= player.speed;
break;
case SDLK_RIGHT:
player.position.x += player.speed;
break;
// 可以添加更多键位控制
}
break;
default:
break;
}
}

4. 更新游戏状态

game_update.c

1
2
3
4
5
6
7
8
c复制代码#include "game_update.h"
#include "entity.h"

void updateGame() {
// 这里可以加入更复杂的逻辑,如AI、碰撞检测等
// 目前简单展示角色移动
// 注意:实际游戏中需要边界检查,防止角色移出屏幕
}

5. 渲染角色

render_game.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码#include "render_game.h"
#include "entity.h"
#include <SDL2/SDL_Renderer.h>

void renderGame(SDL_Renderer* renderer) {
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF); // 设置背景颜色
SDL_RenderClear(renderer);

// 假设我们有一个简单的精灵渲染函数
// 实际中会用到SDL_Texture和SDL_RenderCopy
// 这里仅示例,直接绘制矩形代表角色
SDL_Rect dstRect = player.position;
SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF); // 白色代表角色
SDL_RenderFillRect(renderer, &dstRect);

SDL_RenderPresent(renderer);
}

请记住,这只是一个非常基础的示例,用于展示如何结合输入处理、游戏状态更新和渲染来实现角色的移动。

实际开发中,你将需要处理更复杂的逻辑,如动画、碰撞检测、游戏状态管理、更高效的渲染技术等。

此外,随着项目的增长,良好的代码组织和模块化设计变得至关重要。

接下来,我们可以深入到游戏的其他重要方面,比如资源管理、简单的动画系统和碰撞检测。

这些是构建一个稍微复杂游戏时不可或缺的部分。

6. 资源管理

资源管理对于游戏非常重要,尤其是在加载和释放纹理、音频等资源时。

下面是一个简单的纹理管理器概念。

resource_manager.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
c复制代码#include <SDL2/SDL.h>

#define TEXTURE_MAX 100
SDL_Texture** textures = NULL;
int texture_count = 0;

int loadTexture(const char* path, SDL_Renderer* renderer);
void unloadAllTextures();
resource_manager.c


#include "resource_manager.h"

int loadTexture(const char* path, SDL_Renderer* renderer) {
if (texture_count >= TEXTURE_MAX) {
printf("Too many textures loaded.\n");
return -1;
}
SDL_Texture* newTexture = SDL_LoadTexture(renderer, path);
if (newTexture == NULL) {
printf("Unable to load image %s! SDL Error: %s\n", path, SDL_GetError());
return -1;
}
textures[texture_count++] = newTexture;
return texture_count - 1; // 返回纹理索引
}

void unloadAllTextures() {
for (int i = 0; i < texture_count; ++i) {
SDL_DestroyTexture(textures[i]);
}
texture_count = 0;
}

7. 简单动画系统

animation.h

1
2
3
4
5
6
7
8
9
10
c复制代码typedef struct {
SDL_Texture** frames; // 纹理数组
int frameCount;
int currentFrame;
int frameDelay;
Uint32 lastUpdateTime;
} Animation;

void initAnimation(Animation* anim, SDL_Texture** frames, int count, int delay);
void updateAnimation(Animation* anim, SDL_Renderer* renderer);

animation.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include "animation.h"
#include <SDL2/SDL.h>

void initAnimation(Animation* anim, SDL_Texture** frames, int count, int delay) {
anim->frames = frames;
anim->frameCount = count;
anim->currentFrame = 0;
anim->frameDelay = delay;
anim->lastUpdateTime = 0;
}

void updateAnimation(Animation* anim, SDL_Renderer* renderer) {
Uint32 currentTime = SDL_GetTicks();
if (currentTime - anim->lastUpdateTime > anim->frameDelay) {
anim->lastUpdateTime = currentTime;
anim->currentFrame = (anim->currentFrame + 1) % anim->frameCount;
}
// 假设frames[anim->currentFrame]已正确设置
SDL_RenderCopy(renderer, anim->frames[anim->currentFrame], NULL, &anim->rect);
}

8. 碰撞检测

简单的矩形碰撞检测示例。

collision.h

1
c复制代码bool checkCollision(SDL_Rect rectA, SDL_Rect rectB);

collision.c

1
2
3
4
5
6
7
8
c复制代码#include "collision.h"

bool checkCollision(SDL_Rect rectA, SDL_Rect rectB) {
return (rectA.x < rectB.x + rectB.w &&
rectA.x + rectA.w > rectB.x &&
rectA.y < rectB.y + rectB.h &&
rectA.h + rectA.y > rectB.y);
}

这些组件是构建横版格斗游戏的基础。

实际开发中,还需要考虑如何高效组织这些组件、

如何实现更复杂的AI、游戏菜单、得分系统、音效管理等。

随着项目复杂度的增加,保持代码的清晰和模块化是非常重要的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter 小册 状态管理已上架

发表于 2024-04-26

继路由导航完成后,随着状态管理探索完毕,应用程序构建中的最后一块拼图已经到手。Flutter 实战系列小册再加一名成员:《Flutter 状态管理: 源码探索与实战》

s.juejin.cn/ds/iYwHaVxJ…

image.png


1. 目前 Flutter 小册一览

  • [免费]《Flutter 入门教程》

Flutter 框架层基础: 售价 3.5 元

  • 《Flutter 绘制指南 - 妙笔生花》
  • 《Flutter 动画探索 - 流光幻影》
  • 《Flutter 手势探索 - 执掌天下》
  • 《Flutter 滑动探索 - 珠联璧合》
  • 《Flutter 布局探索 - 薪火相传》
  • 《Flutter 渲染机制 - 聚沙成塔》
  • 《Flutter 语言基础 - 梦始之地》

Flutter 实战系列: 售价 9.9 元

  • 《Flutter 实战:正则匹配应用》
  • 《Flutter 路由导航: 源码探索与实战》
  • 《Flutter 状态管理: 源码探索与实战》

Flutter 实战系列的四部曲也将迎来最终篇。


2. 路由小册的内容介绍

本册主要探索 Flutter 状态管理的使用与底层原理实现,其中精心设计 5 个由简入难的功能需求,作为探索状态管理的切入点。主要有 4 个部分:

image.png

  • 第一部分:引言与监听通知机制

从状态的含义开始说起,逐渐引入状态管理概念。理解状态管理涉及的核心内容,以及其存在的价值。另外,在真正介绍状态管理之前,全面分析监听体制机制的功能与价值,它们像齿轮一样,嵌在状态管理的底层中,啮合 驱动着状态管理的运作。

image.png

  • 第二部分:探索四大类库使用

本模块将基于流行的四大状态管理类库,了解 provider、flutter_bloc、flutter_riverpod、get 的基本使用方式。并通过四个由简入难的需求,探讨状态管理的具体价值,以及在此基础上如何对功能需求代码进行层次划分。

image.png

  • 第三部分:实践应用案例

本模块将通过一个较为全面的案例,介绍一下异步数据操作过程中如何管理状态数据。包括加载列表、增加、删除、修改、下拉刷新和上拉加载更多、搜索、搜索历史维护等异步任务。最后还会基于 sqlite 将数据通过本地数据库存储,进一步理解功能需求代码分层架构的意义。

image.png

  • 第四部分:四大类库源码探索

前面是从使用的层面探讨状态管理的价值,最后一个模块将深入四个类库的源码,区分析状态管理的本质。从而可以知其所以然,了解了源码的实现方式,你就可以站在更高的角度去审视状态管理的内涵。使用时更加得心应手。

image.png


3. 本册的阅读环境

Flutter 环境版本对状态管理没什么太大的影响,而且目前主流的状态管理库已经区域稳定。本册将对四个主流的状态管理库进行探索和分析:

image.png

1
2
3
4
arduino复制代码Flutter 3.19.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 300451adae (12 days ago) • 2024-03-27 21:54:07 -0500
Engine • revision e76c956498
Tools • Dart 3.3.3 • DevTools 2.31.1

本册所有的源码,放在 github 中。阅读时建议结合项目代码 istate :

github.com/toly1994328…

image.png

源码中案例的演进独立包装,其中的 main 文件可以独立运行。另外,可在介绍信息中查看对应步骤的情况:

image.png


4. 本册具体案例功能需求

本册精心设计了五个由简到繁的五个功能需求,来探讨状态管理在应用开发中的使用。

  • 深浅主题色切换

深浅主题色是一个比较简单的需求,而且可以体现状态管理的价值。该功能需求中,在 主页 和 设置页 可以设置深浅主题色。在实现过程中,探讨同一个状态数据,如何在多处组件中触发修改:

浅色 深色 首页也提供切换操作
b0b701b672f7eee95ea07e94e08f771.jpg 5bd0c0cfbbe53b6aecab9ac32d61852.jpg cf5df9a9515ba4a5cc6d4ef9efc2caf.jpg

  • 计数器需求

计数器作梦最开始的地方,承载着 Flutter 开发者最纯粹的回忆。本册中将对计数器的需求进行拓展:

  • 点击增加计数器值
  • 可设置步长
  • 重置计数器
  • 数据持久化存储。
计数器界面 设置步长 设置界面中可访问和操作数据
ba348cd17dbc981dca76aa3a47c9671.jpg 7cb3439909653362a248e6cd38b9a96.jpg 29f4f6c744b96b8a6e8f8aad5d0dd39.jpg

  • github search 搜索

通过搜索 github 仓库,探讨网络请求的处理、以及搜索过程中的状态数据变化。并依次探讨对于一个功能需求的代码结构拆分:

未搜索 搜索中 搜索成功
a63c21e3b823ecfb6bf4d74849a11e1.jpg ce6e902f1309d67d7459fe7dbc2d756.jpg 754ab5db244284ab5e18a5fa9c21071.jpg

  • 类库测试案例

在探讨四个状态管理库使用的过程中,会写一些小的测试代码。可以对比它们在完成同一功能需求,存在的差异性和共通点。以及探索源码时的调试测试案例。这里将其放在了第四个 Tab 中:

我的界面 功能案例页 调试测试页
f0b5dffc1ecc3406d23c8932a7e942b.jpg 6cbc407ea1209cd6e164aed93f4bc87.jpg 19ebe129a744e0b80afbfb6c22997f7.jpg

  • 秘钥管理综合需求

本着以小见大的考量,本册设计了一个比较综合的案例。从一个记录秘钥的简单的功能入手,逐步展开拓展功能需求。其中包括:

  • 秘钥生成器的操作。
  • 秘钥列表的增删改查交互与数据异步操作。
  • 列表数据的刷新、加载更多功能。
  • 搜索、搜索历史的维护。
  • 数据库持久化等。
需要提交 提交中 提交成功
image.png image.png c2f0d4fcae9a5ba75fa9e0fff7d8ad0.jpg

删除与修改交互:

删除的 ui 交互 删除时 loading 状态 修改操作
65.gif 66.gif 70.gif

下拉刷新和加载更多

下拉刷新成功 下拉刷新失败 上拉加载更多
61.gif 62.gif 64.gif

搜索和历史记录:

跳转到搜索页 搜索功能 删除历史
73.gif 74.gif 76.gif

感谢大家一直以来的支持和陪伴,后期还会带来更精彩的内容~

e5497a6729131a4611e90c7b1495ab7.jpg

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我的 ReactNative 开发利器 常用命令总结

发表于 2024-04-26

微信公众号:小武码码码

作为一名 ReactNative 开发者,我每天都要与各种命令行工具打交道。从创建新项目到调试运行,从安装依赖到打包发布,几乎每个环节都离不开命令行的帮助。

为了解决这个问题,我决定系统性地梳理和总结我在 ReactNative 开发中常用的命令,并将其整理成一份备忘录,方便随时查阅。下面,我将详细分享我的总结过程和一些重点命令,希望能给大家的开发工作带来一些帮助。

  1. 项目初始化与依赖管理

1.1 创建新项目

使用 ReactNative CLI 创建新项目是我们最常执行的操作之一。命令如下:

1
csharp复制代码npx react-native init AwesomeProject

这行命令会在当前目录下创建一个名为 AwesomeProject 的新 ReactNative 项目。

1.2 安装项目依赖

在项目根目录下,运行以下命令安装所有依赖包:

1
2
3
bash复制代码npm install
# 或者
yarn install

如果你只需要安装单个依赖,可以使用:

1
2
3
csharp复制代码npm install lodash
# 或者
yarn add lodash

1.3 升级 ReactNative 版本

当需要升级项目的 ReactNative 版本时,可以使用:

1
2
3
java复制代码npm install react-native@latest
# 或者
yarn upgrade react-native@latest

这会将 ReactNative 升级到最新的稳定版本。你也可以指定具体的版本号,如 react-native@0.66.4。

  1. 开发与调试

2.1 启动 Metro 服务器

Metro 是 ReactNative 的 JavaScript 打包器。在开发过程中,我们需要运行 Metro 服务器来实时监听代码变化并重新打包。启动 Metro 的命令是:

1
java复制代码npx react-native start

2.2 运行应用

在 Metro 服务器运行的前提下,我们可以在模拟器或真机上运行 ReactNative 应用。命令如下:

1
2
3
arduino复制代码npx react-native run-android
# 或者
npx react-native run-ios

这会自动打开模拟器或在已连接的设备上安装并启动应用。

2.3 调试应用

ReactNative 提供了强大的调试工具,包括 Chrome 开发者工具、React Developer Tools 等。启用调试模式的命令是:

1
java复制代码npx react-native debug

这会打开一个调试页面,你可以在 Chrome 开发者工具中进行断点调试、网络监控等操作。

  1. 打包与发布

3.1 生成 Android 的发布 APK

要为 Android 生成发布版的 APK 文件,可以使用以下命令:

1
2
bash复制代码cd android
./gradlew assembleRelease

打包后的 APK 文件位于 android/app/build/outputs/apk/release 目录下。

3.2 生成 iOS 的发布 IPA

对于 iOS 应用,我们需要先在 Xcode 中进行发布配置,然后再使用 xcodebuild 命令打包:

1
2
bash复制代码xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -sdk iphoneos -configuration Release archive -archivePath $PWD/build/MyApp.xcarchive
xcodebuild -exportArchive -archivePath $PWD/build/MyApp.xcarchive -exportPath $PWD/build/MyApp.ipa -exportOptionsPlist ExportOptions.plist

其中,ExportOptions.plist 文件包含了发布选项的配置,如代码签名、打包方式等。

  1. 小记

当然,ReactNative 的命令体系还有许多其他的内容,如 Link、Upgrade 等,受限于篇幅,这里就不一一展开了。我建议大家在日常开发中多总结和分享,让这些宝贵的经验成为团队的财富。

以上就是我对 ReactNative 常用命令的总结和分享。如果你有其他补充或心得,欢迎留言交流!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

自带天气APP太臃肿?手写一个换掉它(2)

发表于 2024-04-26

在自带天气APP太臃肿?手写一个换掉它中,打造了一个基本可以用的天气APP,实现了实时位置天气查询,搜索添加指定区域等功能。当然也实现了亮色和暗色两种主题,可以根据手机的主题自动切换(这里被获取darkMode坑的不行,不能在每个页面都获取,只有一个页面的回调函数被调用,最终只能塞全局变量里面了),但是我对这个颜色其实是不太满意的。虽然本意是想打造一个轻量,简约的天气APP,但是现在的感觉太单调了,尤其是实时天气那里。于是我想着给这个实时天气加个背景颜色,一种天气对应一种颜色,让这个天气APP更好看一些。

image-20240425205823391.png

经过一番思考后,我决定开始动手修改。

首先,我想一进来看到的就是实时天气,不想看到下面的那些24小时天气,7日天气什么的,第一眼印象就是要简单,简约,如果有需要看下面那些信息,就往上滑动就行了,这个实现起来比较简单,将实时天气的高度设置成100%就行了。

image-20240425211730724.png

嗯,看着简单了许多。接下来就是设置背景色了,背景需要根据不同天气显示不同颜色,我加了一个枚举类,里面设置了大致的几种天气场景和颜色,只要在查询天气的时候根据天气情况去找到对应的场景枚举返回给页面显示就行了,还有把状态栏和导航栏的颜色换掉,和背景色一致,让整体更加沉浸,查阅资料,harmonyos提供了这样的API可以直接用,不过状态栏和导航栏的前景色API6就不能设置了,得API8才行,状态栏和导航栏的前景色就先放着。

image-20240425214118334.png

看起来好一些了,然后需要设置其他场景的背景色,但是这个调色比较麻烦,因为电脑上预览的颜色和手机上显示的颜色有一点色差,每次改一个颜色编译一下很费劲,于是乎我想做一个颜色配置的页面,可以直接在手机上实时预览调整,这样效率会高很多,预览页面需要满足以下几点:

  • 可以选择不同的天气场景,包含晴天,多云,阴天,雨天,雪天,雾天,冰雹,扬尘这几类
  • 可以选择白天还是夜晚,白天和夜晚可以设置不同的背景色
  • 可以针对红,绿,蓝三基色配置
  • 可以保存配置好的颜色,优先使用自定义的颜色,如果没有自定义,则使用默认的背景色
  • 上半部分实时预览配置效果

天启APP-背景色配置-1714056786622-2.png

上半部分的实时预览只要把天气页面的组件拿过来用就可以了,下半部分就是几个按钮和三基色的slider,也比较简单,实现效果如下:

天气APP-背景色.gif

到这,背景色配置功能就好了。

接下来,就是给各个场景加上动画,设想了这几种场景的动画

  • 晴天:白天一个发光的太阳,夜晚一个发光的月亮和一些随机的星星
  • 多云:白天几个随机的云彩缓慢移动,夜晚几个随机的云彩缓慢移动和一些随机的星星
  • 阴天:没有什么东西,就纯背景色
  • 雨天:下落的雨水,矩形,速度比较快
  • 雪天:下落的雪花,圆形,速度比较慢
  • 雾天:没有什么东西,就纯背景色
  • 冰雹:下落的冰块,菱形,速度中等
  • 扬尘:没有什么东西,就纯背景色

还有就是让布局撑到状态栏和导航栏,不然下雨下雪这种不是从屏幕最上方开始的,就很怪,做好后就是这样

天气APP-背景动画.gif

应用到天气页面后,就是这样

天气APP-动画效果.gif

天气背景这块大致就这么多了,后面主要就是小修小补的了,修复BUG,还有解决性能的问题。本来就是要做一个轻量的APP,所以功能上变动不会太大了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…151617…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%