当前位置: 首页 > news >正文

Godot 4 插件 - Utility AI 研究

今天看到一个视频教学

Godot4 | 实现简单AI | Utility AI 插件_哔哩哔哩_bilibili

就看了一下。吸引我的不是插件,是AI这两个字母。这AI与Godot怎么结合?感觉还是离线使用,值得一看。

视频时间不长,15分钟左右,看得我云山雾罩,不过演示项目能直接下载(AI Demo.zip官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘)

下载下来,能运行,是个小游戏,不过逻辑没大看明白,可能以后看明白后会觉得很简单,但初接触,里面的弯弯绕那么多,一时不好理。

看介绍里还有一个插件自带Demo(godot-utility-ai-examples.zip官方版下载丨最新版下载丨绿色版下载丨APP下载-123云盘),感觉会简单一些。下载打开一看,果然简单很多。

插件自带Demo

因为Demo就一个场景AgentExample,且子节点就两个,这样就清爽了。

 不过运行一下,感觉没啥吸引力,就几个数字在那里变来变去。怎么能与AI挂上钩?

肯定是我理解的问题,再看一下

主场景的脚本很简单

func _ready():var needs: AgentNeeds = $Agent.needsneeds.food_changed.connect(%FoodBar._on_needs_changed)needs.fun_changed.connect(%FunBar._on_needs_changed)needs.energy_changed.connect(%EnergyBar._on_needs_changed)$Agent.state_changed.connect(%StateLabel._on_state_changed)

就是把几个进度条的显示与needs的相应信号绑定到一起了,每个显示的处理逻辑都是一样的

func _on_needs_changed(p_value: float) -> void:value = p_value

这好象没啥,数据正常显示。

哦,数据怎么来的?这个needs变量是AgentNeeds类型,从agent_needs.gd来看,这是一个Resource。

# Copyright (c) 2023 John Pennycook
# SPDX-License-Identifier: 0BSD
class_name AgentNeeds
extends Resourcesignal food_changed(value)
signal fun_changed(value)
signal energy_changed(value)@export var food := 0.5 : set = _set_food
@export var fun := 0.5 : set = _set_fun
@export var energy := 0.5 : set = _set_energyfunc _set_food(p_food: float) -> void:food = clamp(p_food, 0.0, 1.0)food_changed.emit(food)func _set_fun(p_fun: float) -> void:fun = clamp(p_fun, 0.0, 1.0)fun_changed.emit(fun)func _set_energy(p_energy: float) -> void:energy = clamp(p_energy, 0.0, 1.0)energy_changed.emit(energy)

Godot有点意思,在资源里还带有逻辑。这不闹嘛,还是脚本。在理解的领域,把资源与脚本画一个约等于符号。

这个资源有三个属性,对应三个写方法,然后会触发三个相应的信号。仅此而已。这还是没有看到数据的起源。

再看一下脚本情况,还剩下一个agent.gd,是绑定到Agent节点的脚本。难道这里还有入口?

哦,看到Agent节点下还有一个Timer节点,那想必应该一定是这个Timer节点在不断做啥事。打开脚本看下,果然

# Copyright (c) 2023 John Pennycook
# SPDX-License-Identifier: 0BSD
class_name Agent
extends Node2Dsignal state_changed(state)enum State {NONE,EATING,SLEEPING,WATCHING_TV,
}@export var needs: AgentNeeds
var state: State = State.EATINGvar _time_until_next_decision: int = 1@onready var _options: Array[UtilityAIOption] = [UtilityAIOption.new(preload("res://examples/agents/eat.tres"), needs, eat),UtilityAIOption.new(preload("res://examples/agents/sleep.tres"), needs, sleep),UtilityAIOption.new(preload("res://examples/agents/watch_tv.tres"), needs, watch_tv),
]func eat():state = State.EATING_time_until_next_decision = 5state_changed.emit(state)func sleep():state = State.SLEEPING_time_until_next_decision = 10state_changed.emit(state)func watch_tv():state = State.WATCHING_TV_time_until_next_decision = 1state_changed.emit(state)func _on_timer_timeout():# Adjust the agent's needs based on their state.# In a real project, this would be managed by something more sophisticated!if state == State.EATING:needs.food += 0.05else:needs.food -= 0.025if state == State.SLEEPING:needs.energy += 0.05else:needs.energy -= 0.025if state == State.WATCHING_TV:needs.fun += 0.05else:needs.fun -= 0.025# Check if the agent should change state.# Utility helps the agent decide what to do next, but the rules of the game# govern when those decisions should happen. In this example, each action# takes a certain amount of time to complete, but the agent will abandon# eating or sleeping when the associated needs bar is full.if ((state == State.SLEEPING and needs.energy == 1)or (state == State.EATING and needs.food == 1)):_time_until_next_decision = 0if _time_until_next_decision > 0:_time_until_next_decision -= 1return# Choose the action with the highest utility, and change state.var decision := UtilityAI.choose_highest(_options)decision.action.call()

在Timer的时钟事件中,根据当前的状态,修改相应属性值,这样界面上的数据就不断变化。

看代码时,发现还有个_time_until_next_decision变量,看名字其作用就是下决定的时间。真实逻辑是

	if _time_until_next_decision > 0:_time_until_next_decision -= 1return# Choose the action with the highest utility, and change state.var decision := UtilityAI.choose_highest(_options)decision.action.call()

即,_time_until_next_decision <= 0的情况下,会进行decision计算,否则不计算,保持现状。大概应该是这个意思。

但decision计算是要干啥?UtilityAI.choose_highest(_options),应该是在几个选项中选最优先的项,或者说是最紧要的项,最重要的项。可以看到_options的定义

@onready var _options: Array[UtilityAIOption] = [UtilityAIOption.new(preload("res://examples/agents/eat.tres"), needs, eat),UtilityAIOption.new(preload("res://examples/agents/sleep.tres"), needs, sleep),UtilityAIOption.new(preload("res://examples/agents/watch_tv.tres"), needs, watch_tv),
]

就三项,对于就eat、sleep、watch_tv三个逻辑,这些逻辑最终都会发出信号state_changed,该信号绑定到主场景脚本中的%StateLabel._on_state_changed,简单显示一下内容

func _on_state_changed(state: Agent.State) -> void:match state:Agent.State.EATING:text = "Eat"Agent.State.SLEEPING:text = "Sleep"Agent.State.WATCHING_TV:text = "Watch TV"

这下,基本弄明白了,核心就是定义_options选项,然后用UtilityAI.choose_highest(_options)取得目标选项,触发相应逻辑。

好象明白了,又好象没明白,仔细再琢磨一下,才发现,UtilityAI.choose_highest(_options)这个最重要的函数,它是怎么工作的?它凭啥能选出最紧要、重要的选项,这个过程,程序员能设计些什么?

这个答案肯定不能在UtilityAI的代码中去找,因为UtilityAI肯定是通用的处理方式,刚才这些选项是业务相关的,应该是程序员处理的事

回过头再看下_options的定义,里面有几个UtilityAIOption,带有一个tres参数。跟进查看源码,UtilityAIOption一共有三个参数:behavior、context、action

func _init(p_behavior: UtilityAIBehavior = null,p_context: Variant = null,p_action: Variant = null
):behavior = p_behaviorcontext = p_contextaction = p_action

而UtilityAI.choose_highest(_options)是一个类函数

static func choose_highest(options: Array[UtilityAIOption], tolerance: float = 0.0
) -> UtilityAIOption:# Calculate the scores for every option.var scores := {}for option in options:scores[option] = option.evaluate()# Identify the highest-scoring options by sorting them.options.sort_custom(func(a, b): return scores[a] < scores[b])# Choose randomly between all options within the specified tolerance.var high_score: float = scores[options[len(options) - 1]]var within_tolerance := func(o): return (absf(high_score - scores[o]) <= tolerance)return options.filter(within_tolerance).pick_random()

它分别通过各选项的option.evaluate()计算出各选项的实时值。然后从低到高排序,如果有容许误差(tolerance),则过滤筛选,可能结果不止一个,则pick_random随机选一个。

所以,还得看各选项option.evaluate()是如何工作的。

func evaluate() -> float:return behavior.evaluate(context)
func evaluate(context: Variant) -> float:var scores: Array[float] = []for consideration in considerations:var score := consideration.evaluate(context)scores.append(score)return _aggregate(scores)

各个behavior根据context进行计算,其各个考虑因子consideration(UtilityAIConsideration)分别计算得到结果,成为一个数列scores: Array[float],再根据aggregation类型确定最终结果的生成逻辑

func _aggregate(scores: Array[float]) -> float:match aggregation:AggregationType.PRODUCT:return scores.reduce(func(accum, x): return accum * x)AggregationType.AVERAGE:return scores.reduce(func(accum, x): return accum + x) / len(scores)AggregationType.MAXIMUM:return scores.max()AggregationType.MINIMUM:return scores.min()push_error("Unrecognized AggregationType: %d" % [aggregation])return 0

这里用到Array.reduce函数,以前没用过这个函数,所以不太清楚这些代码的结果。但问下ChatGPT,了解了:

所以,最终的问题是:behavior中的各consideration是啥,怎么来的?

回到_options的定义

@onready var _options: Array[UtilityAIOption] = [UtilityAIOption.new(preload("res://examples/agents/eat.tres"), needs, eat),UtilityAIOption.new(preload("res://examples/agents/sleep.tres"), needs, sleep),UtilityAIOption.new(preload("res://examples/agents/watch_tv.tres"), needs, watch_tv),
]

应该从这三个tres中找答案。比如eat.tres

这就对上了,原来在这里定义了各要素:Aggregation为Product,表示最终结果连乘。不过只有一个Consideration,所以连不连的也就一样了。

sleep.tres、watch_tv.tres也同样理解。

这里面还有一点,就是各Consideration的定义,它是用图表示出来的,看起来很直观,其实不太好定量理解,这个既然是算法逻辑,那还是精确一些好理解,但画成图形,尤其是还有一大堆参数可调,就感觉不好控制了。不过目前暂看图形曲线,能看到IO大概关系,参数什么的暂不关心。

到此,整个流程清晰了:

1. Agent的Timer周期性(1s)处理:

1.1 每秒根据状态,调整needs的food、energy、fun三个属性,从而触发needs的三个信号。这三个信号绑定到界面的三个进度条,从而三个进度条显示相应属性值大小

1.2 决策时刻(秒)减1。如果<=0,则进行决策,决策结果会影响状态。而决策过程就是UtilityAI.choose_highest(_options),即各选项自行根据输入计算得到自己的输出,然后由UtilityAI筛选出目标选项。确定后,触发目标选项的action(分别动态赋值为agent.gd中的eat、sleep、watch_tv函数),更新相应状态并触发信号,由主场景的_on_state_changed函数显示相应的状态信息。

B站AI Demo

现在回来看B站的Demo项目。现在回来,直接看重点:agent的tres

一共有三个tres:attack、chase、run_away,那应该会有三个状态,结果是4个

enum State {IDLE,CHASE,RUN_AWAY,ATTACK,
}

这也不能说是理解错误,反而是十分正确与准确。

attack.tres是Product模式,一个Consideration,嗯,很好理解

chase.tres是Product模式,三个Considerations,run_away.tres是Product模式,四个Considerations,同样好理解。这些就是在各选项的实时计算时的依据。

下来就是看各选项的定义,肯定会与这三个tres有关

@onready var _options: Array[UtilityAIOption] = [UtilityAIOption.new(preload("res://Enemy/agent/attack.tres"), needs, attack),UtilityAIOption.new(preload("res://Enemy/agent/chase.tres"), needs, chase),UtilityAIOption.new(preload("res://Enemy/agent/run_away.tres"), needs, run_away)
]

 果真如此。这里的needs为输入,第三个参数将在相应的选项被选中后调用。

func idle():state = State.IDLEstate_changed.emit(state)func chase():state = State.CHASEstate_changed.emit(state)func run_away():state = State.RUN_AWAYstate_changed.emit(state)func attack():state = State.ATTACKstate_changed.emit(state)

一看就是熟悉的味道。不过翻遍了代码,也没看到state_changed的绑定处理函数。难道是没有用这个信号?原来视频里提醒过了:信号没有使用。那好吧,这就是只改变内部的状态,外部不需要显示或处理这个信号。

同样,不用猜,还会有一个Timer来处理。该Timer的时钟周期为0.4s:

func _on_timer_timeout() -> void:var needs_info = get_parent().get_ai_needs()for key in needs_info.keys():needs.set(key, needs_info[key])var decision := UtilityAI.choose_highest(_options)decision.action.call()

与自带Demo的区别在于,这里的_options中的needs输入,是从父场景中取得的get_parent().get_ai_needs(), 相当于父场景提供实时输入数据

func get_ai_needs() -> Dictionary:return {"my_hp": hp / enemy_hp,"player_hp": _player_node.hp / _player_node.max_hp,"partners": 1.0 if _partners > 3 else _partners / 3,"could_hit_player": _could_hit_player,"could_run_away": _could_run_away,}

这个UtilityAI的任务好象就完成了:时钟中获取实时数据,判断目标选项,调用目标选项的action,其中完成内部的状态改变。

这是什么AI?感觉就是一个简单的逻辑

再看了一下Demo项目,感觉内容比较多,主要是碰撞相关内容处理、动画效果展示,还有就是路径规划。呃,路径规划_make_path,是不是AI的工作呢,看看源码,原来是NavigationAgent2D的功劳,与AI无关。

@onready var nav_agent: NavigationAgent2D = $NavigationAgent2Dfunc _make_path() -> void:match $Agent.state:1:nav_agent.target_position = _player_node.global_position2:var _partner_nodes = get_tree().get_nodes_in_group("enemy")if len(_partner_nodes) == 1:_could_run_away = 0.0else:var _partner = [null, INF]for _pt in _partner_nodes:if _pt == self:continuevar _partner_distance = global_position.distance_to(_pt.global_position)if _partner_distance < _partner[1]:_partner[0] = _pt_partner[1] = _partner_distancenav_agent.target_position = _partner[0].global_position_could_run_away = 1.0

但,好吧,说是AI就是AI吧,毕竟那些输出都是计算机算出来的

相关文章:

Godot 4 插件 - Utility AI 研究

今天看到一个视频教学 Godot4 | 实现简单AI | Utility AI 插件_哔哩哔哩_bilibili 就看了一下。吸引我的不是插件&#xff0c;是AI这两个字母。这AI与Godot怎么结合&#xff1f;感觉还是离线使用&#xff0c;值得一看。 视频时间不长&#xff0c;15分钟左右&#xff0c;看得…...

第八章:将自下而上、自上而下和平滑性线索结合起来进行弱监督图像分割

0.摘要 本文解决了弱监督语义图像分割的问题。我们的目标是在仅给出与训练图像关联的图像级别对象标签的情况下&#xff0c;为新图像中的每个像素标记类别。我们的问题陈述与常见的语义分割有所不同&#xff0c;常规的语义分割假设在训练中可用像素级注释。我们提出了一种新颖的…...

MySql忘记密码如何修改

前言 好久没用数据库的软件了&#xff0c;要用的时候突然发现密码已经忘记了&#xff0c;怎么试都不对&#xff0c;心态直接爆炸&#xff0c;上一次用还是22年6月份&#xff0c;也记不得当时用数据库干什么了&#xff0c;这份爆炸浮躁的心态值得这样记录一下&#xff0c;警示自…...

【NetCore】04-作用域与对象释放行为

文章目录 作用域 作用域由IServiceScope接口承载 对象释放 实现IDisposable接口类型释放 1.DI只负责释放由其创建的对象实例 2.DI在容器或子容器释放时&#xff0c;释放由其创建的对象实例 建议 1.避免在根容器获取实现IDisposable接口的瞬时服务 2.避免手动创建实现了IDispo…...

新材料技术的优势

目录 1.什么是新材料技术 2.新材料技术给人类带来了哪些便利 3.新材料技术未来的发展趋势 1.什么是新材料技术 新材料技术指的是通过科学和工程技术的手段开发和应用全新的材料&#xff0c;以满足特定的需求和应用。新材料技术是材料科学和工程领域的重要研究方向&#xff0…...

HTTPS、DNS、正则表达式

HTTPS原理 HTTPS&#xff08;Hypertext Transfer Protocol Secure&#xff09;是一种安全的通信协议&#xff0c;它基于HTTP协议&#xff0c;在数据传输过程中使用了加密技术来保护通信的安全性和完整性。HTTPS的工作原理主要包括以下几个步骤&#xff1a; 客户端发起HTTPS请求…...

MAC电脑设置charles,连接手机的步骤说明(个人实际操作)

目录 一、charles web端设置 1. 安装charles之后&#xff0c;先安装证书 2. 设置 Proxy-Proxy Settings 3. 设置 SSL Proxying 二、手机的设置 1. 安卓 2. ios 资料获取方法 一、charles web端设置 1. 安装charles之后&#xff0c;先安装证书 Help-SSL Proxying-Inst…...

百度文心一言接入教程-Java版

原文链接 前言 前段时间由于种种原因我的AI BOT网站停运了数天&#xff0c;后来申请了百度的文心一言和阿里的通义千问开放接口&#xff0c;文心一言的接口很快就通过了&#xff0c;但是文心一言至今杳无音讯。文心一言通过审之后&#xff0c;很快将AI BOT的AI能力接入了文心…...

Games101学习笔记 - 基础数学

向量 向量&#xff1a;方向和长度&#xff0c;没有起始位置 向量长度&#xff1a;各个方向平方相加开方 单位向量&#xff1a;向量除向量的长度 点乘 在笛卡尔坐标系中的点乘计算&#xff1a; 几何意思&#xff1a; 表示一个向量在另一个向量上的投影点乘在图形学中应用&a…...

Linux进程的认识

查看进程指令proc/ps 注意哦, 我们经常使用的指令, 像ls, touch…这些指令在启动之后本质上也是进程 proc 是内存文件系统, 存放着当前系统的实时进程信息. 每一个进程在系统中, 都会存在一个唯一的标识符(pid -> process id), 就如同学生在学校里有一个专门的学号一样. 大…...

向量vector与sort()

运行代码&#xff1a; //向量与sort() #include"std_lib_facilities.h" //声明Item类 struct Item {string name;int iid;double value;friend istream& operator>>(istream& is, Item& ii);friend ostream& operator<<(ostream& o…...

Netty学习(三)

文章目录 三. Netty 进阶1. 粘包与半包1.1 粘包现象服务端代码客户端代码 1.2 半包现象服务端代码客户端代码 1.3 现象分析粘包半包缘由滑动窗口MSS 限制Nagle 算法 1.4 解决方案方法1&#xff0c;短链接方法2&#xff0c;固定长度方法3&#xff0c;固定分隔符方法4&#xff0c…...

c++学习(布隆过滤器)[23]

布隆 布隆过滤器&#xff08;Bloom Filter&#xff09;是一种概率型数据结构&#xff0c;用于判断一个元素是否可能存在于一个集合中。它使用多个哈希函数和位图来表示集合中的元素。 布隆过滤器的基本原理如下&#xff1a; 初始化&#xff1a;创建一个长度为m的位图&#xf…...

React的UmiJS搭建的项目集成海康威视h5player播放插件H5视频播放器开发包 V2.1.2

最近前端的一个项目&#xff0c;大屏需要摄像头播放&#xff0c;摄像头厂家是海康威视的&#xff0c;网上找了一圈都没有React集成的&#xff0c;特别是没有使用UmiJS搭脚手架搭建的&#xff0c;所以记录一下。 海康威视的开放平台的API地址&#xff0c;相关插件和文档都可以下…...

细讲TCP三次握手四次挥手(二)

TCP/IP 协议族 应用层 应用层( application-layer &#xff09;的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程&#xff08;进程&#xff1a;主机中正在运行的程序&#xff09;间的通信和交互的规则。 对于不同的网络应用需要不同的应用层协议…...

LeetCode Top100 Liked 题单(序号19~)

19. Remove Nth Node From End of List 题意&#xff1a;给一个链表&#xff0c;删除从尾数起的第n个结点&#xff0c;返回头节点。 我的思路 指针到最后&#xff0c;数出来有多少个&#xff0c;之从前向后数&#xff0c;再删掉节点 代码 10ms Beats 16.06% class Solution…...

qssh使用

到官网下载qssh的源码QSsh-botan-1&#xff0c;使用qtcreator打开后&#xff0c;直接编译&#xff0c;即可得到qssh的库 头文件将QSsh-botan-1\src\libs\ssh目录下的.h文件拷到include文件夹下&#xff0c;即为库头文件。 qssh有个问题&#xff0c;如果你将qssh的类放在子线程…...

持续部署CICD

目录 &#xff08;1&#xff09;CICD的开展场景 &#xff08;2&#xff09;项目实际应用 CICD 是持续集成&#xff08;Continuous Integration&#xff09;和持续部署&#xff08;Continuous Deployment&#xff09;简称。指在研发过程中自动执行一系列脚本来降低开发引入 bug…...

ARM 循环阻塞延迟函数

串行驱动的关键是双方能够按照既定的时序进行检测、设置相关引脚上的电平&#xff0c;比如单总线、I2c这样基本的可以用GPIO模拟的时序协议&#xff0c;需要主从双方&#xff0c;必须在链路接口内严格按照微妙级的延迟单位进行时序同步。 所以&#xff0c;在这种对时间要求很敏…...

Spark的DataFrame和Schema详解和实战案例Demo

1、概念介绍 Spark是一个分布式计算框架&#xff0c;用于处理大规模数据处理任务。在Spark中&#xff0c;DataFrame是一种分布式的数据集合&#xff0c;类似于关系型数据库中的表格。DataFrame提供了一种更高级别的抽象&#xff0c;允许用户以声明式的方式处理数据&#xff0c…...

WPF线程使用详解:提升应用性能和响应能力

在WPF应用程序开发中&#xff0c;线程的合理使用是保证应用性能和响应能力的关键。WPF提供了多种线程处理方式&#xff0c;包括UI线程、后台线程、Task/Async Await和BackgroundWorker。这些方式与传统的Thread类相比&#xff0c;更加适用于WPF框架&#xff0c;并能够简化线程操…...

ava版知识付费平台免费搭建 Spring Cloud+Spring Boot+Mybatis+uniapp+前后端分离实现知识付费平台

提供私有化部署&#xff0c;免费售后&#xff0c;专业技术指导&#xff0c;支持PC、APP、H5、小程序多终端同步&#xff0c;支持二次开发定制&#xff0c;源码交付。 Java版知识付费-轻松拥有知识付费平台 多种直播形式&#xff0c;全面满足直播场景需求 公开课、小班课、独…...

libuv库学习笔记-basics_of_libuv

Basics of libuv libuv强制使用异步和事件驱动的编程风格。它的核心工作是提供一个event-loop&#xff0c;还有基于I/O和其它事件通知的回调函数。libuv还提供了一些核心工具&#xff0c;例如定时器&#xff0c;非阻塞的网络支持&#xff0c;异步文件系统访问&#xff0c;子进…...

【Vuvuzela 声音去噪算法】基于流行的频谱减法技术的声音去噪算法研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

Vue + Element-ui组件上传图片报错问题解决方案

在前端开发中&#xff0c;我们经常需要模拟网络请求以进行单元测试或开发调试。而在模拟网络请求时&#xff0c;我们常常会使用到MockXMLHttpRequest对象。MockXMLHttpRequest对象是一个用于模拟XMLHttpRequest对象的工具&#xff0c;它提供了一种简单的方式来模拟网络请求&…...

java商城系统和php商城系统对比

java商城系统和php商城系统是两种常见的电子商务平台&#xff0c;它们都具有一定的优势和劣势。那么&#xff0c;java商城系统和php商城系统又有哪些差异呢&#xff1f; 一、开发难度 Java商城系统和PHP商城系统在开发难度方面存在一定的差异。Java商城系统需要使用Java语言进…...

某制造企业基于 KubeSphere 的云原生实践

背景介绍 随着业务升级改造与软件产品专案的增多&#xff0c;常规的物理机和虚拟机方式逐渐暴露出一些问题&#xff1a; 大量服务部署在虚拟机上&#xff0c;资源预估和硬件浪费较大&#xff1b;大量服务部署在虚拟机上&#xff0c;部署时间和难度较大&#xff0c;自动化程度…...

Electron 学习_BrowserWindow

BrowserWindow创建并控制浏览器窗口(主进程) 条件&#xff1a;在 app 模块 emitted ready 事件之前&#xff0c;您不能使用此模块。 1.在加载页面时&#xff0c;渲染进程第一次完成绘制时&#xff0c;如果窗口还没有被显示&#xff0c;渲染进程会发出 ready-to-show 事件 。 在…...

Docker学习笔记,包含docker安装、常用命令、dockerfile、docker-compose等等

&#x1f600;&#x1f600;&#x1f600;创作不易&#xff0c;各位看官点赞收藏. 文章目录 Docker 学习笔记1、容器2、Docker 安装3、Docker 常用命令4、Docker 镜像5、自定义镜像5.1、镜像推送到阿里云5.2、镜像私有库 6、数据卷7、Docker 软件安装8、Docker File8.1、常见保…...

解决 “Module build failed (from ./node_modules/babel-loader/lib/index.js)“ 错误的方法

系列文章目录 文章目录 系列文章目录前言一、错误原因&#xff1a;二、解决方法&#xff1a;三、注意事项&#xff1a;总结 前言 在前端项目开发中&#xff0c;如果使用了 Babel 来转译 ES6 语法&#xff0c;有时会遇到错误信息 “Module build failed (from ./node_modules/b…...