0%

有两种适用于对称密钥分组密码的攻击——线性密码分析和差分密码分析,基于基本替换置换网络密码basic Substitution-Permutation Network cipher,被选为AES的Rijndael密码是从基本的SPN结构派生的

A Basic Substitution-Permutation Network Cipher(基本的替换-置换网络密码)

Substitution替换

Our cipher

  1. 16bit datablock——>4个4bit sub-blocks

    • 一个sub-block是一个4*4的S-Box的输入
    • 4*4的S-Box指的是4bit输入到4bit输出的的替换
    • 用一张16个4bit值表示的表
    • 特点:非线性,即输出bit是不能用对输入bits的线性操作表示
  2. 所有S-Boxes使用相同的nolinear mapping

    在DES里每一轮使用的所有S-Boxes不同,但所有轮次使用相同的S-Boxes集合(相同几个S-Boexes但顺序不一样)

    • 线性和差分密码分析的攻击同样适用于:使用一种mapping或者所有S-Boxes使用不同mappings

Permutation置换

置换:bit位置的置换

Key Mixing

  • To:实现密钥混合——在与轮相关的key bits(也叫子密钥)和每轮输入的数据块之间使用按位异或
  • To:最后一轮替换不会轻易被分析者忽略——在最后一轮替换之后也应用subkey mixing
  • 密码的主密钥master key——密钥调度key schedule——>一轮的子密钥subkey

Decryption

解密也采用SPN的形式

  1. 解密网络中的S盒中使用的映射是加密网络中映射的逆
    • 所以为了使SPN允许解密,所有的S-Box必须是双射的,即输入和输出一对一映射
  2. 子密钥以相反的顺序应用
  3. 总之,数据流反过来就是了

Differential Cryptanalysis

——差分密码分析在基本SPN密码中的应用

Overview of Basic Attack基本攻击概述

差分密码分析利用明文差异和差异在密码最后一轮中高概率出现的特点,即通过观察成对的明文和相应的密文之间的差异来揭示加密算法的内部结构


X’——>Y’,X”——>Y”
ΔXi = Xi’ ⊕ Xi” (按位异或)
ΔX = [ΔX1 ΔX2 … ΔXn]
ΔY = [ΔY1 ΔY2 … ΔYn]

  • 对于理想化的随机密码,给定ΔX,出现ΔY的概率是1/2^n
  • 差分密码利用:对于给定的ΔX,会有一个Δy出现的概率很高。这样一对(ΔX,ΔY)视为一个differential

差分密码分析是一种选择明文攻击,这意味着攻击者能够选择输入、检查输出,以此来获得密钥。
攻击者将选择一对输入X’X”,满足特定的ΔX,因为对于这个ΔX,会有一个ΔY高概率出现


本文研究一个differential的构造:by检查高可能差分特征(differential characteristics

差分:一对输入的明文差异 + 高概率出现的密文差异

差分特征:一轮的输入和输出差异

  • differential characteristics:一轮的输入和输出差异的序列
  • 一轮的输出差异对应于下一轮的输入差异
  • 使用highly likely differential characteristic让我们有机会获得进入最后一轮密码的信息,To derive bits from the last layer of the subkeys

To 构造 highly likely differential characteristics

  1. 检查单个S-Box的properties,并使用这些属性来确定complete differential characteristic——考虑S-Box输入和输出的差异,来确定一个high probability的差异对
  2. 对每轮的S盒差异对进行组合,使一轮的output difference bits对应下一轮的input difference bits——>找到一个high probability differential(明文差分+the difference of the input to the last round最后一轮的输入)
  3. 由于子密钥涉及两个data sets(作用在输入X’和作用在输入X”),异或之后子密钥实际上会被抵消掉,所以他们并不出现在最终的差分表达式中。因此,差分密码分析的优势:允许绕过子密钥进行直接求解

Analyzing the Cipher Component密码组件分析

To检查S-box的difference pairs

对于4*4的S盒:4位输入,4位输出——16个X的值

只需要考虑X’的16个值+给定ΔX — 可以得到X” = X’ ⊕ ΔX
例:∆X值为1011、1000和0100时:表中,∆X = 1011出现∆Y = 0010的次数是16个可能值中的8个(即概率为8/16);在给定∆X = 1000的情况下,∆Y = 1011的出现次数为4/16

我们可以将S-box的完整数据用差值分布表制成表格,其中行表示∆X值,列表示∆Y值。差值分布表如下,表中的每个元素表示给定输入差∆X的对应输出差∆Y值的出现次数

差值分布表的性质

  1. 一行和一列所有元素的和是2^n=16
  2. 所有元素值都是偶数:因为ΔX对应于成对的输入(X’⊕X”和X”⊕X’),所以一个ΔX对应成对相同的ΔY,所以ΔX对应于任意的ΔY个数为偶数

理想的S-box,差值分布表所有元素都为1,与性质矛盾,所以理想的S-box是无法实现的

密钥对S盒差分的影响

无密钥时,输入为X,输出为Y
有密钥时,输入为X⊕K=W
所以输入的差分 ΔW = [ΔW1 ΔW2 … ΔWn] (ΔWi = Wi’ ⊕ Wi”)
ΔWi = Wi’ ⊕ Wi” = (Xi’ ⊕ Ki) ⊕ (Xi” ⊕ Ki) = X’ ⊕ X” = ΔXi
所以密钥位对于输入差值没有影响,也就是说:有Key的S-Box和无Key的S-Box具有相同的差值分布表

Constructing Differential Characteristics构建差分特征

一旦为SPN中的S盒编译了差分信息,就可以得到整个密码的有用差分特性的数据

通过连接适当的S盒,在每轮中构造某些S盒差分对的差分特征,使得差分涉及到最后一轮S盒的明文对和数据位的输入,可以通过恢复最后一轮之后的子密钥位的子集来攻击密码

差分特征构造的例子

image-20241210185038908

考虑一个涉及S12、S23、S32和S33的差分特性:

∆P:明文差异

∆Ui:第i轮S盒的输入

∆Vi:第i轮S盒的输出

image-20241210185345969

∆P = ∆U1 = [ 0000 0000 1011 0000 ]

∆V1 = [ 0000 0010 0000 0000 ]

对于给定∆P(假设所有轮次的S盒的差分对之间是独立的,所以概率直接相乘):

有8/16概率得∆U2 = [ 0000 0000 0100 0000]

有8/16 * 6/16 = 3/16的概率得∆U3 = [ 0000 0010 0010 0000]

有8/16 * 6/16 * 6/16 * 6/16 = 27/1024的概率得∆U4 = [ 0000 0110 0000 0110]

在密码分析过程中,许多对明文∆P = [ 0000 0000 1011 0000 ]将被加密,会有27/1024的高概率出现所示的差异特征,def:满足∆P的明文对称为 right pairs, 没有这种特征的明文对称为 wrong pairs

Extracting Key Bits提取密钥位

一旦以足够大的概率发现R轮密码的R−1轮差分特征,就可以通过恢复最后一轮子密钥来攻击密码。在我们的示例中,将从子密钥K5中提取比特,将跟在被非零差异影响的最后一轮S盒之后的密钥认为是target partical subkey

过程包括:尝试目标子密钥位的所有可能值,将其与密文进行异或并通过S盒反向运行数据,如果得到的值和期望值一样,计数+1,最大计数对应的是正确的子密钥。

对示例密码的攻击:

  1. 差分特性在最后一轮影响了S42和S44的输入,所以[K55,K56,K57,K58,K5 13,K5 14,K5 15,K5 16]是目标子密钥位
  2. 尝试与∆P对应的每个密文对,对于每个密文对都尝试目标子密钥位的256个值(2^8),通过S42和S44反向运行得到S盒的输入,与期望的∆U4比较,一样计数+1
  3. 最大的计数被认为是正确的值

注意:没有必要对每个密文对执行部分解密。因为最后一轮的输入差只影响2个S盒,因此当特征发生时(即输入的是 right pairs),S41和S43的密文位差必须为零,通过这个条件过滤掉一些 wrong pairs,不用进行后续过程

实际操作:通过生成5000个明文/密文对(对应∆P),模拟得到256个值对应的概率,以下是部分值:

image-20241210194612774

概率最大的是[2 4],是正确的subkey,但并不等于理想的27/1024

因为有几个因素会影响计数与理论期望不同:

the partial decryption for different partial subkeys 不同部分子密钥的部分解密

the imprecision of the independence assumption required for determination of the characteristic probability 确定特征概率所需的独立性假设的不精确性

the concept that differentials are composed of multiple differential characteristics 微分是由多个微分特征组成的概念

Complexity of the Attack攻击的复杂度

def:对于差分密码分析,我们将具有非零输入差分(因此是非零输出差分)的特征所涉及的S盒称为有源S盒(active S-boxes)

一般来说,active S-boxes的微分概率越大,完整密码的特征概率就越大。同时,active S-boxes越少,特征概率越大。

在考虑密码分析的复杂性时,我们考虑进行攻击所需的数据。也就是说,假设能够获取ND个明文,我们就能够处理它们(攻击者能够获得足够的明文对,并使用这些数据进行分析和攻击)

确定攻击所需的明文对数量的经验法则:

image-20241210200033293

公式正确性的理解:正确配对出现几次就足以使正确的目标部分子密钥的计数显著大于错误目标部分子密钥的计数。由于正确配对大约每 1/pD 个明文对就会出现一次,因此在实际应用中,通常可以使用 1/pD 的小倍数的选定明文对来成功地进行攻击。

c是一个小常数,pD 是 R 轮密码的 R−1 轮的差分特征概率(假设每个活动 S-box 中的差分对是独立的),则:

image-20241210200145460


抗差分密码分析的策略主要集中在 S-box 的性质上(即最小化 S-box 的差分对概率)以及寻找结构来最大化活动 S-box 的数量。Rijndael(即 AES)就是一个为提高抗差分密码分析能力而设计的良好密码示例。


注意假设的前提条件:差分特征概率的计算通常假设 S-box 是独立的,但实际情况中 S-box 之间可能存在依赖关系。因此,差分特征概率是一个估算值,虽然在实际中,很多密码的差分特征概率计算仍然是相当准确的。


最重要的是,具有相同输入差分和输出差分(即相同的差分)的不同差分特征可以组合起来,表示一个比单个差分特征所隐含的更大的差分概率。

即多个差分特征(尽管它们各自表示相同的输入和输出差分)可以一起作用,从而导致一个比单个差分特征计算得到的差分概率更高。这是因为不同的差分特征可能会相互“增强”彼此的效果,从而使得综合概率更大。这里提到的概念类似于线性密码分析中的“线性包络”(linear hulls)——多条线性关系(或者特征)结合起来,可以形成更强的攻击效果。

所以,为了证明密码对差分密码分析的安全性,必须证明所有可能的差分组合的概率都低于某个可接受的阈值,而不仅仅是单个差分特征的概率低于某个可接受的阈值。

不过,通常可以合理假设,当一个差分特征具有较高的概率时,它会主导差分的发生,而且该特征的概率可以很好地近似差分的实际概率。

参考文献

Heys, Howard M. “A tutorial on linear and differential cryptanalysis.” Cryptologia 26.3 (2002): 189-221.

TCP知识点

1. TCP和UDP的区别

  1. 连接:TCP是面向连接的,在传输数据前要先建立连接;UDP是无连接的
  2. 服务对象:TCP是一对一的两点服务;UDP支持一对一、一对多、多对多的通信
  3. 可靠性:TCP是可靠交付数据,能保证数据无差错、无丢失、无重复、按序到达接收方;UDP是尽最大努力交付,不保证可靠性
  4. 拥塞控制和流量控制:TCP会通过拥塞控制和流量控制来决定数据的发送效率;而UDP没有,即使网络拥堵,也不会影响UDP的发送效率
  5. 首部开销:TCP在不适用选项字段时头部长20B;UDP头部固定是8B
  6. 传输方式:TCP是面向字节流的传输;UDP是基于报文的传输
  7. 分片策略不同:TCP的数据如果大于MSS,会在传输层进行分片,主机收到后也在传输层组装TCP数据包;UDP的数据如果大于MTU,会在IP层进行分片,主机收到后,在IP层组装完数据
  8. 应用场景:
    1. TCP能保证数据的可靠性交付,经常用于:FTP文件传输、HTTP/HTTPS
    2. UDP的处理高效简单,经常用于:
      • 总包量少的通信,如DNS
      • 视频、音频等多媒体通信
      • 广播通信

2. TCP建立连接

(1)三次握手过程

TCP 三次握手

  1. 初始阶段,客户端和服务端都处于 CLOSE 状态,服务端开始主动监听某个端口,处于 LISTEN 状态

  2. 客户端请求建立连接,会随机初始化序号 client_isn

    填充报文头部:将 client_isn 置于TCP首部的序号字段,把SYN位标志为1

    将报文发送给服务端

    进入 SYN_SENT 状态

  3. 服务端收到SYN报文,随机初始化序号 sever_isn

    填充报文头部:将 sever_isn 置于序号字段,将 client_isn + 1 填充到确认应答号字段,将SYN和ACK位置为1

    将报文发送给客户端

    进入 SYN_RCVD 状态

  4. 客户端收到SYN-ACK报文,回应最后一个应答报文

    填充报文头部:序号字段为 client_isn ,确认应答号字段为 sever_isn + 1,将ACK位置为1

    可以携带客户端到服务端的数据

    将报文发送给服务端

    进入 ESTABLISHED 状态

  5. 服务端收到ACK报文,也进入 ESTABLISHED 状态

TCP的连接状态在linux中通过 netstat -napt 命令查看

(2)为什么是三次握手
  1. 三次握手才能阻止历史连接/冗余连接
    • 场景示例:客户端发送了一个SYN(isn=10)报文后宕机,而且这个报文还被网络堵塞了;客户端重启之后重新向服务端发送一个新的SYN(isn=20)报文,旧的报文比新的报文先到达。先看三次握手的情况:SYN(10)到达服务端之后,服务端回复SYN+ACK(ack=11)报文,客户端收到之后发现不是自己期望收到的确认号(21)就会回RST报文,服务端收到RST报文后释放连接;如果只有两次握手的话,服务端收到SYN报文后就进入 ESTABLISHED 状态,就会给对方发送数据,造成资源浪费
    • 总结:两次握手无法阻止历史连接/冗余连接,每接收到一个SYN报文就会建立一次连接,因为服务端没有中间状态 SYN_RCVD 来给客户端终止历史连接,导致服务端可能建立一个历史连接或者是无效冗余连接,造成资源浪费
  2. 同步双方初始化序列号
    • TCP通信双方都得维护一个序列号,作用
      1. 去除重复数据
      2. 根据序列号按序接收
      3. 可以通过对方回复的ACK中的确认应答号来标识哪些数据包已被接收
    • 只有三次握手,一来一回才能确保双方都发送自己的序列号并且确认自己的序列号已被对方接收,并且服务端对SYN的回复以及通知对方自己序列号的报文可以合并,所以是三次握手
(3)序列号问题
  1. 序列号作用

    1. 方便接收方去除重复数据
    2. 方便接收方根据序列号按序组装报文
    3. 接收方可以根据收到的报文中的确认应答字段知道发送的报文中哪些已经被接收
  2. 为什么每次建立连接的初始化序列号要求不一样

    1. 主要原因:防止历史报文被下一个相同四元组连接接收
    2. 背景:TCP四次挥手中的TIME_WAIT状态回持续2MSL时长,历史报文会在网络中消失。但是我们不能保证每次连接都能通过四次挥手来正常关闭连接,例如:服务端断电重启,回复客户端RST报文,这次建立的连接终止,然后服务端和客户端又建立了相同的四元组连接。
    3. 假如每次序列号都是从0开始,大概率会导致旧的报文序列号正好在接收端的接收窗口内,会被正常接收,造成数据错乱;所以每次初始化序列号不一样能够很大程度上避免历史报文被下一个相同的四元组连接接收
  3. 序列号的初始化方法

    ISN = M + F(四元组)

    • 计时器:每隔4微秒+1
    • F是一个Hash算法,根据四元组生成一个随机值

    ​ 随机数基于时钟计时器递增,基本上不会随机成一样的初始化序列号

  4. 序列号回绕问题

    1. 问题:序列号是一个32位的无符号数,当到达最大值之后会循环回到0,无法仅根据序列号来判断新老数据
    2. 解决方法:TCP时间戳
      1. TCP时间戳是一个默认开启的选项,作用:1)便于精确计算RTT,2)防止序列号回绕
      2. 原理:连接双方维护一次最近收到的数据包的时间戳,当收到新数据包时会读取时间戳并跟维护的最新时间戳比较,如果不是递增的,就说明该数据包时过期的,直接丢弃这个数据包
(4)IP层分片,为什么TCP层还需要MSS?
  1. MTU:网络包的最大长度,一般是1500B;MSS:除去IP头和TCP头,容纳的TCP数据的最大长度
  2. 如果传输层不分片,到IP层再分片的话,那对于传输层来说发送的是一整个报文,接收的也是一整个报文。如果发生丢失触发超时重传,整个IP报文的所有分片都得重传。而在传输层分片的话,只需要重传丢失的报文分片

3. TCP断开连接

(1)四次挥手过程

客户端主动关闭连接 —— TCP 四次挥手

  1. 客户端打算关闭连接,向服务端发送 FIN 报文,之后进入 FIN_WAIT_1 状态
  2. 服务端收到 FIN 报文后,回复 ACK 报文,进入 CLOSE_WAIT 状态
  3. 客户端收到 ACK 报文后,进入 FIN_WAIT_2 状态
  4. 等待服务端处理完数据之后,向客户端发送 FIN 报文,进入 LAST_ACK 状态
  5. 客户端收到 FIN 报文后,回复 ACK 报文,进入 TIME_WAIT 状态
  6. 服务端收到 ACK 报文后,就进入 CLOSE 状态,至此服务端连接关闭
  7. 客户端在经过2MSL时间后,自动进入 CLOSE 状态,客户端连接也关闭

注意:主动关闭连接的才有 TIME_WAIT 状态

(2)为什么是四次挥手

关闭连接时,客户端向服务端发送 FIN 报文,只能标识,客户端不再发送数据了但还能接收数据

服务端收到并回复 ACK 报文后,可能还有数据需要处理和发送,只有等到服务端不再发送数据时,才会发送 FIN 报文。

所以,服务端通常需要等待完成数据的处理和发送,所以 ACKFIN 报文一般都会分开发送,因此需要四次挥手

(3) TIME_WAIT 状态

TIME_WAIT 等待时间 = 2MSL

  • MSL:报文最大生存时间
  • 网络中可能存在来自发送方的数据包,被接收方接收后又会回复响应报文,一来一回需要等待2MSL时间。所以相当于至少允许报文丢失一次。
  • 从客户端接收到 FIN 发送 ACK后开始计时,如果在 TIME_WAIT 时间内客户端的 ACK 没有传输到服务端,服务端重传 FIN 报文,客户端重新回复 ACK ,并且 2MSL 重新计时
(4)为什么需要 TIME_WAIT 状态
  1. 防止历史连接中的数据被后面相同四元组的连接错误的接收
    • 如果没有 TIME_WAIT 状态或者时间过短,比如说,上一个连接中最后发送的报文被网络延迟了,后面双方建立了新的相同的四元组连接,当被阻塞的报文到达接收方后,如果正好处于接收窗口内会被正常接收,导致数据错乱
    • 因此设计 TIME_WAIT 状态,并会持续2MSL时间,足以让两个方向上的数据包在网络中自然消失
  2. 保证被动关闭连接的一方,能被正常关闭
    • TIME_WAIT 作用:等待足够的时间以确保最后的ACK能被服务器接收,从而正常关闭
    • 场景示例:如果没有 TIME_WAIT ,客户端发送完ACK报文就进入关闭状态,如果这个ACK报文丢失,服务端会重传FIN报文,但是这个时候客户端处于关闭状态,没有办法正常回复ACK,会回复RST报文。服务端收到RST报文后因为异常而进入CLOSE状态
(5)TIME_WAIT 过多的危害、如何优化
  1. 危害:
    • 占用系统资源
    • 占用端口资源
  2. 优化思路:资源复用
(6)服务器出现大量 TIME_WAIT 状态的原因

TIME_WAIT 是主动关闭连接方才会出现的状态,所以服务器出现大量的TIME_WAIT 状态,说明服务器主动断开了很多TCP连接

Q:什么场景下服务端会主动断开连接

  1. HTTP没有使用长连接(任意一方没有开启长连接就会使用短连接)

    当完成一次TCP通信后就会关闭,一般是由服务端主动关闭TCP连接,服务端就会出现TIME_WAIT 状态

    因此,当没有启用长连接时,服务端在处理完一个http请求后就会主动关闭连接,导致出现大量TIME_WAIT 状态

  2. HTTP长连接超时

    服务器会指定HTTP的超时时间,如果这段时间内客户端没有再发起新的请求,服务端就会关闭连接,出现TIME_WAIT 状态

    所以,服务端出现大量TIME_WAIT 状态可能是大量的客户端建立TCP连接后长时间没有发送数据,导致服务端超时主动关闭连接,出现大量TIME_WAIT 状态

  3. HTTP长连接请求数量达到上限

    服务器会定义一条长连接上最大能处理的客户端请求数量,当超过最大限制时就会主动关闭连接,从而出现TIME_WAIT 状态

4. 重传机制、滑动窗口、流量控制和拥塞控制

(1)重传机制

TCP实现可靠传输的方式之一,是通过序列号与确认应答

如果数据包丢失,会用重传机制

常见的重传机制:

  1. 超时重传:当超过一定时间没有收到确认应答报文,就会重传数据

    超时重传时间RTO略大于RTT

    问题:每次都要时间等待,引入快速重传

  2. 快速重传:不以时间为驱动,而是以数据为驱动

    工作方式:收到三个相同的ACK报文就会重传该报文

    • 三次相同的ack报文:说明该报文对方还没收到,不用等到超时再重传
(2)滑动窗口
  • 确认应答的缺点:等待应答后再发送下一个数据包,效率低,网络吞吐量小,引入窗口

  • 窗口:无需等待确认应答,可以连续发送数据的最大值

(3)流量控制

发送方根据接收方实际接收能力控制发送的数据量

(4)拥塞控制
  • 背景:网络发生拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失,发送方就会重传数据,导致网络的负担更重,进入恶循环

  • 拥塞控制的目的:避免发送方的数据填满整个网络

  • 拥塞窗口cwnd:发送方维护的一个状态变量,根据网络的拥塞程度动态变化

    • 变化规则:网络没有拥塞就增大,网络出现拥塞就减少
  • 发送窗口swnd = min(cwnd, rwnd)

  • 四个算法

    1. 慢启动:初始化为1,指数性增长

      当拥塞窗口达到慢启动门限,启用拥塞避免算法

    2. 拥塞避免:cwnd线性增长

      一直增长以后,网络慢慢进入拥塞状态,就会出现丢包,触发重传机制,也就是进入了拥塞发生算法

    3. 拥塞发生

      重传机制由两种:超时重传和快速重传

      • 发生超时重传(RTO时间内没收到确认应答报文),就会使用拥塞发生算法:门限值=拥塞窗口*1/2,拥塞窗口恢复初始值

        接着就开始慢启动算法

      • 发生快速重传(连续收到三次相同的ACK报文):拥塞窗口=拥塞窗口*1/2,门限值=拥塞窗口

        接着进入快恢复算法

    4. 快恢复

      • 拥塞窗口=门限值+3
      • 重传丢失的数据包

“fatal: Could not read from remote repository.”解决方法

今天上传博客遇到报错如下:

image-20240821174111048

解决方法:

执行

1
ssh-add ~/.ssh/id_rsa

如果遇到报错如下

image-20240821174204943

则执行

1
2
3
eval `ssh-agent -s`

ssh-add ~/.ssh/id_rsa

再次上传无报错


后来又出现了相同报错

解决方法

  1. 修改本地.ssh

    1
    ssh-keygen -t rsa -C "邮箱"
  2. 打开C:\Users\你的用户名\.ssh\id_rsa.pub,复制id_rsa.pub文件里的全部内容。

  3. 打开GitHub的设置,打开ssh公钥,粘贴生成

再次上传正常

UObject的实例化

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
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/Classes/Engine/DataTable.h"
#include "UObject/NoExportTypes.h"
#include "MyObject.generated.h"

USTRUCT(BlueprintType)
struct FMyDataTableStruct: public FTableRowBase
{
GENERATED_USTRUCT_BODY()
// 声明构造函数
FMyDataTableStruct();

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyTestDataTableStruct")
float Health;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyTestDataTableStruct")
FString Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyTestDataTableStruct")
int Level;
};

/**
*
*/
UCLASS()
class MYPROJECT2_API UMyObject : public UObject
{
GENERATED_BODY()
public:
// 声明结构体
UPROPERTY()
FMyDataTableStruct MyDataTableStruct;
};

// MyObject.cpp
// 实现构造函数
FMyDataTableStruct::FMyDataTableStruct()
{
Health = 10.0f;
Name = TEXT("zhangsan");
Level = 1;
}

// MyPawn.h
// 声明变量
UPROPERTY()
UMyObject* MyTestObject;

// MyPawn.cpp
void AMyPawn::BeginPlay()
{
Super::BeginPlay();

TSubclassOf<UMyObject> MySubClassObject = UMyObject::StaticClass();
// Object实例化
MyTestObject = NewObject<UMyObject>(GetWorld(), MySubClassObject);
// 判断是否实例化成功
if (MyTestObject) {
UE_LOG(LogTemp, Warning, TEXT("MyTestObject is %s"), *MyTestObject->GetName());
// 获取结构体变量
UE_LOG(LogTemp, Warning, TEXT("My Health is %f"), MyTestObject->MyDataTableStruct.Health);
UE_LOG(LogTemp, Warning, TEXT("My Name is %s"), *MyTestObject->MyDataTableStruct.Name);
UE_LOG(LogTemp, Warning, TEXT("My Level is %d"), MyTestObject->MyDataTableStruct.Level);
}
}

image-20240821152243312

UGameInstance的实例化

GameInstance全局唯一单例,这个在引擎初始化的时候就已经生成,一直存在到引擎关闭

作用:

  1. 引擎初始化与关闭时执行的逻辑
  2. 为游戏保全全局数据:比如上一个关卡的信息需要在下一个关卡使用时我们用 GameInstance 保存数据但是只是临时数据,游戏结束则消失,如果想要本地持久保存数据需要用 SaveGame

引擎中创建 MyGameInstance

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
// MyGameInstance.h
// 类中声明
public:
// 声明构造函数
UMyGameInstance();
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "MyInstance")
FString MyAPPID;
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "MyInstance")
FString MyUserID;
UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = "MyInstance")
FString MyName;

// MyGameInstance.cpp
// 实现构造函数
UMyGameInstance::UMyGameInstance()
{
MyAPPID = TEXT("123");
MyUserID = TEXT("456");
MyName = TEXT("zhangsan");
}

// MyPawn.h
#include "MyGameInstance.h"
// 声明一个Instance变量
UPROPERTY()
UMyGameInstance* MyInstance;

// MyPawn.cpp
// MyInstance实例化
MyInstance = Cast<UMyGameInstance>(GetWorld()->GetFirstLocalPlayerFromController()->GetGameInstance());
if (MyInstance) {
UE_LOG(LogTemp, Warning, TEXT("MyInstance is %s"), *MyInstance->GetName());
UE_LOG(LogTemp, Warning, TEXT("MyAPPID is %s"), *MyInstance->MyAPPID);
UE_LOG(LogTemp, Warning, TEXT("MyUserID is %s"), *MyInstance->MyUserID);
UE_LOG(LogTemp, Warning, TEXT("MyName is %s"), *MyInstance->MyName);
}

运行

编辑—>项目设置—>地图与模式—>游戏实例—>游戏实例类—>MyGameInstance

image-20240821170426505

枚举UENUM

UENUM生成枚举的反射数据,通过反射将枚举暴露给蓝图,进行C++和蓝图之间的通信

1
2
3
4
5
6
7
8
9
10
11
12
13
// 生成枚举的反射数据,通过反射将枚举暴露给蓝图,进行C++和蓝图之间的通信
// BlueprintType:在蓝图中创建的变量类型可以选择这个枚举
UENUM(BlueprintType)
namespace MyEnumType {
enum MyCustomEnum {
Type1,
Type2,
Type3
};
}
// 在类中声明变量
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyEnum");
TEnumAsByte<MyEnumType::MyCustomEnum> MyCustomEnum;
1
2
3
4
5
6
7
// 枚举声明的另一种方式
UENUM(BlueprintType)
enum class EMtTestEnum :uint8 {
OneType UMETA(DisplayName = "OneType"),
TwoType UMETA(DisplayName = "TwoType"),
ThreeType UMETA(DisplayName = "ThreeType"),
};

image-20240821130846119+

BlueprintType在蓝图中创建的变量类型可以选择这个枚举

image-20240821130940475

结构体USTRUCT

USTRUCT()生成结构体的反射数据,通过反射将结构体的变量或者方法暴露给蓝图

BlueprintType在蓝图中创建的变量类型可以选择这个结构体

ps:命名必须以F开头

1
2
3
4
5
6
7
8
9
10
11
12
// 生成结构体的反射数据,通过反射将结构体的变量或者方法暴露给蓝图
// BlueprintType:在蓝图中创建的变量类型可以选择这个结构体
USTRUCT(BlueprintType)
struct FMyStruct // 命名必须以F开头
{
GENERATED_USTRUCT_BODY()

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyTestStruct")
int32 Health;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyTestStruct")
FString MyName;
};
1
2
3
// 声明一个结构体的变量
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyCustomStruct")
FMyStruct MyCustomStruct;

使用结构体接收数据表格

创建 Object

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Engine/Classes/Engine/DataTable.h"

USTRUCT(BlueprintType)
struct FMyDataTableStruct: public FTableRowBase
{
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyTestDataTableStruct")
float Health;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyTestDataTableStruct")
FString Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyTestDataTableStruct")
int Level;
};

创建数据表格,保存为 csv 格式

image-20240821145917602

在项目中拖入文件

UPROPERTY宏、属性说明符和元数据说明符

UPROPERTY宏

UPROPERTY宏:把C++里的变量暴露给蓝图,实现在蓝图中访问C++变量,达到C++和蓝图通信的效果

属性说明符

1
2
3
4
5
6
7
8
9
// 仅在类默认设置可见
UPROPERTY(VisibleDefaultsOnly) // 虚幻C++生成反射数据,在蓝图中可以找到相应的变量
int32 VisibleDefaultsOnlyInt;
// 仅在实例化细节面板可见
UPROPERTY(VisibleInstanceOnly)
FString VisibleInstanceOnlyString;
// 类默认设置和实例化细节面板都可见
UPROPERTY(VisibleAnywhere)
FVector VisibleAnywhereVector;
1
2
3
4
5
6
7
8
9
// 仅在类默认设置里面编辑
UPROPERTY(EditDefaultsOnly)
int32 EditDefaultsOnly;
// 仅在实例化细节面板编辑
UPROPERTY(EditInstanceOnly)
FString EditInstanceOnlyString;
// 类默认设置和实例化细节面板都编辑
UPROPERTY(EditAnywhere)
FVector EditAnywhereVector;
1
2
3
4
5
6
7
// 仅在蓝图中可读
UPROPERTY(EditAnywhere,BlueprintReadOnly)
int32 BlueprintReadOnlyInt;
// 在蓝图中可读可写,即可以获取和设置变量
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 BlueprintReadWriteInt;
// BlueprintReadOnlyInt只能获取,BlueprintReadWriteInt既可获取又能设置

image-20240820173600330

1
2
3
4
5
6
// Category属性目录
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyIntValue")
int32 Value1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyIntValue|MySubIntValue")
int32 Value2;
// 为变量创建目录

image-20240820174959387

image-20240820175106272

元数据说明符

1
2
3
4
5
// meta元数据说明符
// DisPlayName别名:将变量用另一个别名替换
// MyValue3被MyValue3DisplayName替换
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisPlayName = "MyValue3DisplayName"))
int32 MyValue3;

image-20240820175451727

1
2
3
4
5
6
// EditCondition条件控制编辑
// 只有当Controller为真时,Value3才可编辑
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisPlayName = "Controller"))
bool isController;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "isController"))
float Value3;

image-20240820180119115

image-20240820180123296

1
2
3
// Tooltip解释说明我们的变量
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ToolTip = "isControllerTrue"))
bool isTrue;

image-20240820180451588

UFUNCTION宏、属性说明符和元数据说明符

UFUNCTION宏:生成函数的反射数据,将函数暴露给蓝图,实现函数和蓝图之间的通信

属性说明符

BlueprintCallable暴露我们的函数在蓝图中可调用

BlueprintPure纯虚函数的定义

1
2
3
4
5
6
// 暴露我们的函数在蓝图中可调用
UFUNCTION(BlueprintCallable, Category = "My Function")
void PrintF1();
// 纯虚函数的定义BlueprintPure
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "My Function")
bool PrintF2();

有返回值vs无返回值

  • 有返回值的是一个函数,无返回值的是一个事件

image-20240820205201302image-20240820205206787

BlueprintImplementableEvent在C++中声明,不能定义,蓝图可重载

1
2
3
4
5
6
7
8
9
// BlueprintImplementableEvent:在C++中声明,不能定义,蓝图可重载
UFUNCTION(BlueprintImplementableEvent)
void Test1();
UFUNCTION(BlueprintImplementableEvent)
int Test2();
UFUNCTION(BlueprintImplementableEvent)
void Test3(const FString &MyString);
UFUNCTION(BlueprintImplementableEvent)
int Test4(const FString& MyString);

BlueprintNativeEvent在C++中声明和实现,蓝图可重载或者不重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BlueprintNativeEvent:在C++中声明和实现,蓝图可重载或者不重载
UFUNCTION(BlueprintNativeEvent)
void TestA();
UFUNCTION(BlueprintNativeEvent)
int TestB();
UFUNCTION(BlueprintNativeEvent)
void TestC(const FString &mystring);
UFUNCTION(BlueprintNativeEvent)
int TestD(const FString& mystring);
// 实现时函数名=原函数名+_Implementation
void AMyPawn::TestA_Implementation()
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("TestA"));
}

示例:在 BeginPlay 中调用 TestA, 在蓝图中重载 TestA

image-20240821124658647

运行结果:

image-20240821124837582

元数据说明符

DisplayName:为函数起别名

1
2
3
// 元数据说明符meta
UFUNCTION(BlueprintCallable, Category = "My Function", meta = (DisplayName = "MyPrintTest"))
void PrintTest();

image-20240821125656504

容器TArray

TArray 是虚幻C++中的动态数组

特点:

  1. 速度快、内存消耗小、安全性高
  2. TArray 所有元素均为完全相同类型,不能进行不同元素类型的混合
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
// 数组操作
// 1.增
MyArray.Add(10); // 将元素添加到数组中
MyArray.Add(20);
MyArray.Add(30);
MyArray.AddUnique(20); // 数组中存在相同元素就不添加
MyArray.AddUnique(40); // 数组中不存在相同元素就添加

// 2.删
MyArray.Remove(20); // 移除中数组中所有的等值元素,移除所有20
MyArray.RemoveAt(0); // 移除数组中索引值为0的元素
MyArray.RemoveSingle(10); // 移除首个匹配的等值元素
MyArray.Empty(); // 清空数组,size—>0
MyArray.Reset(); // 重置数组,size不变,所有元素变为0

// 3.改
MyArray.Insert(60, 0); // 在0号索引的位置插入元素60
int32& temp = MyArray[0];
temp = 50; // 通过变量引用修改数组元素

// 4.查
MyArray.Contains(10); // 查找数组中是否包含某个元素,返回值类型:bool
MyArray.Find(10); // 正向查找数组中匹配的第一个元素,返回值类型:bool
MyArray.FindLast(10); //反向查找数组中匹配的第一个元素,返回值类型:bool

// 5.遍历
void AMyGameMode::PrintArray()
{
// 用迭代器遍历数组并打印
for (auto It = MyArray.CreateConstIterator(); It; It++) {
UE_LOG(LogTemp, Warning, TEXT("%d"), *It);
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, FString::Printf(TEXT("%d"), *It));
}
}

容器TMap

TMap 是虚幻C++中的一种键值对容器

特点:

  1. 数据成对出现(Key,Value)
  2. Value通过Key获取,且Key值不能重复
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
// Map操作
// 1.增
MyMap.Emplace(0, 1); // 用法和add一样,都是往容器中添加数值
MyMap.Emplace(1, 2);
MyMap.Emplace(2, 3);

// 2.删
MyMap.Remove(0); // 移除Key为0的元素
MyMap.Empty(); // 清空容器

// 3.查找
MyMap.Contains(1); // 查找匹配的Key值,进行两次查找,返回值:bool
int32* isFind1 = MyMap.Find(2); // 通过查找匹配的Key值返回Value,只进行一次查找,返回值:指针
const int32* isFind2 = MyMap.FindKey(3); //反向查找,通过查找匹配的Value返回Key,返回值:指针
// 分别获取所有的Keys和Values
TArray<int32> TestKeys;
TArray<int32> TestValues;
MyMap.GenerateKeyArray(TestKeys);
MyMap.GenerateValueArray(TestValues);

// 4.遍历
for(auto& TestMap:MyMap){
// 获取Key:TestMap.Key,获取Value:TestMap.Value
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, FString::Printf(TEXT("Key:%d""Value:%d"), TestMap.Key, TestMap.Value));
UE_LOG(LogTemp, Warning, TEXT("Key:%d""Value:%d"), TestMap.Key, TestMap.Value);
}

容器TSet

Tset是一种快速容器类,通常用于排序不重要的情况下存储唯一元素

TSet类似于TMapTMultiMap,但有一个重要区别:TSet是通过对元素求值的可覆盖函数,使用数据值本身作为键

TSet可以非常快速地添加、查找和删除元素(恒定时间)

TSet也是值类型,支持常规复制、赋值和析构函数操作,以及其元素具有较强的所有权

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
// Set操作
// 1.添加元素
// add和emplace都是添加元素到容器中,但是emplace可以避免在插入集合时产生的临时文件
MySet.Add(TEXT("Banana"));
MySet.Add(TEXT("Apple"));
MySet.Emplace(TEXT("Orange"));
PrintSet();

// 2.合并元素
TSet<FString>MySet2;
MySet2.Add(TEXT("wangsen"));
MySet2.Add(TEXT("lindanqi"));
MySet.Append(MySet2); // Append将MySet2的值合并到Myset中
GEngine->AddOnScreenDebugMessage(-1,5.0f,FColor::Cyan,TEXT("=============="));
PrintSet();

// 3.移除元素
MySet.Remove(TEXT("Banana")); // remove函数会返回已删除元素的数量,如果删除的Key不在集合中则返回0
MySet.Empty(); // 清空容器,释放内存
MySet.Reset(); // 清空集合元素,但是保留内存

// 4.查找元素
int32 size = MySet.Num(); // 查询集合中保存的元素数量
bool isFind1=MySet.Contains(TEXT("Banana")); // 查询是否包含特定元素,返回值:bool
FString* isFind2 = MySet.Find(TEXT("Banana")); // Find返回的是指向元素数值的指针,如果不包含该Key则返回Null

// 5.Array函数会返回TArray,存储TSet中每个元素的副本
TArray<FString> FruitArray = MySet.Array();

// 6.排序
TSet<FString> TestSet = { TEXT("a"),TEXT("aa"),TEXT("aaa"),TEXT("aaaa") };
TestSet.Sort([](FString A, FString B) { // 参数:Lambda表达式
return A.Len() > B.Len();
});

// 7.运算符
TSet<FString>NewSet;
NewSet = MySet; // 将MySet里面的值赋值给NewSet
NewSet.Add(TEXT("One"));

// 8.[]
// 根据FSetElementId访问集合对应元素的引用并修改内容
FSetElementId Index = NewSet.Add(TEXT("Two"));
TestSet[Index] = TEXT("NewTwo");

// 9.ReServe
TSet<FString>NewSet1;
NewSet1.Reserve(10); // 预先分配内存,如果输入的Num大于存储的元素个数,则会产生闲置内存Slack

// 10.Shrink
for (int32 i = 0; i < 10; i++) {
NewSet1.Add(FString::Printf("NewSet % d", i)); // 添加元素
}
for (int32 i = 0; i < 10; i += 2) {
NewSet1.Remove(FSetElementId::FromInteger(i)); // 删除元素产生闲置内存
}
NewSet.Shrink(); // 删除末端的空元素

// 11.Compact将容器中的空白元素集合到末尾,+Shrink删除
NewSet1.Compact(); // 注意:Compact可能会改变元素之间的顺序,如果不想改变顺序,可以使用CompactStable
NewSet1.Shrink();

基础变量类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 布尔类型变量
bool varBool;
// 32位整型变量
int32 varInt32;
// 64位整型变量
int64 varInt64;
// 字节类型变量
BYTE varByte;
// 字符串类型变量FString
FString varString;
// 名称类型变量FName
FName varName;
// 文本类型变量FText
FText varText;
// 向量类型变量FVector
FVector varVector; //xyz的坐标
// 旋转向量类型变量FRotator
FRotator varRotator; // x轴旋转Roll,y轴旋转Pitch,z轴旋转Var
// FTransform
FTransform varTransform; // FVector,FRotator,缩放Scale三者的集合类型

FString、FName、FText三者的区别和相互转化

区别:

FString:接近C++中的 std::string,更着重于字符串的操作,提供字符串的操作接口,与其他两个相比唯一一个可以修改的变量,所以消耗更多,性能更低

FName:着重于表示名称,不区分大小写,不可以更改,引擎中的字面名称都是FName类型,FName在创建的时候会根据内容创建一个哈希值,同样的内容只会存储一次,通过哈希值FName的查找和访问比较快,通过比较哈希值来区分不同的FName

FText:着重在于显示与本地化,显示玩家能直接看到的信息,本地化即多语言的处理,不可更改。相较于另外两种类型,FText会更加的臃肿,但提供了优秀的本地化功能

本地化:一个字符串,在不同的地区显示为不同的字符。如 ”菜单“ 在英语地区显示为 “Menu“ ,这种支持不同语言地区显示不同字符的工作,就叫做本地化

转化

  1. FString—>FName/FText

    1
    2
    3
    4
    5
    6
    // 创建一个FString
    FString MyString = TEXT("I am string");
    // FString转化为FName
    FName MyName = FName(*MyString);
    // FString转化为FText
    FText MyText = FText::FromString(MyString);
  2. FName—>FString/FText

    1
    2
    3
    4
    // FName转化为FString
    MyString = MyName.ToString();
    // FName转化为FText
    FText MyText = FText::FromName(MyName);
  3. FText—>FString/FName

    1
    2
    // FText转化为FString
    FString strFromText = MyText.ToString();

    注意:FText不能直接转换成FName,需要FText->FString->FName

基础数据类型输出打印

1
2
3
4
5
6
7
8
9
10
11
12
13
// 基础数据类型打印
int32 MyInt = 10;
float MyFloat = 5.0;
bool MyBool = true;
char MyChar = 'A';
FString MyFString = TEXT("MyString");
FVector MyVector = FVector(0, 0, 0);
UE_LOG(LogTemp, Warning, TEXT("%d"), MyInt);
UE_LOG(LogTemp, Warning, TEXT("%f"), MyFloat);
UE_LOG(LogTemp, Warning, TEXT("%d"), MyBool);
UE_LOG(LogTemp, Warning, TEXT("%c"), MyChar);
UE_LOG(LogTemp, Warning, TEXT("%s"), *MyFString);
UE_LOG(LogTemp, Warning, TEXT("%s"), *MyVector.ToString());

image-20240819192108004

UE C++头文件含义+宏作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once // 预处理指令,头文件只被编译一次

#include "CoreMinimal.h" // 核心编程环境普遍存在类型
#include "GameFramework/GameModeBase.h" // 基于Game类生成的
#include "MyProject2GameModeBase.generated.h" // 存储反射数据

/**
*
*/
UCLASS() // 告知虚幻引擎生成类的反射数据
// UPROPERTY:属性声明宏,虚幻C++在标准基础上实现了一套反射系统(负责垃圾回收、引用更新、编辑器集成等作用),作用:声明该属性在反射系统的行为
// UFUNCTION/USTRUCT/UENUM:函数/结构体/枚举声明宏,反射系统可识别的C++函数/结构体/枚举
class MYPROJECT2_API AMyProject2GameModeBase : public AGameModeBase
{
GENERATED_BODY() // 自动生成的宏
// 表示不直接使用父类的构造函数,需要在头文件中声明构造函数并在cpp中实现,之后的成员是private
// GENERATED_UCLASS_BODY():使用父类的构造函数,之后的成员是public

};

创建默认类

Gamemode 的头文件里包含其他头文件、声明构造函数,在cpp中实现构造函数,创建默认类

1
2
3
4
5
6
7
8
AMyGameMode::AMyGameMode() {
// 获取我们创建的类
DefaultPawnClass = AMyPawn::StaticClass();
PlayerControllerClass = AMyPlayerController::StaticClass();
GameStateClass = AMyGameState::StaticClass();
PlayerStateClass = AMyPlayerState::StaticClass();
HUDClass = AMyHUD::StaticClass();
}

重写BeginPlay\Tick\EndPlay

GameMode.h中的类中添加

1
2
3
4
5
6
7
public:
// 游戏运行的时候一开始就会执行BeginPlay函数
virtual void BeginPlay() override;
// 游戏运行的时候每一帧都在执行Tick函数
virtual void Tick(float DeltaTime) override;
// 退出游戏的时候或者切换/卸载关卡的时候执行EndPlay函数
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

GameMode.cpp中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void AMyGameMode::BeginPlay()
{
Super::BeginPlay();
}

void AMyGameMode::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}

void AMyGameMode::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
}

打印日志/屏幕上打印

1
2
3
4
5
6
7
8
9
10
// 打印日志
// LogTemp临时日志记录类别名称,Warning日志记录的级别,TEXT是打印的文本
// 日志的记录级别(三种):Error>Warning>Display,打印颜色也不一样红/黄/白
UE_LOG(LogTemp, Error, TEXT("My name is ok"));
UE_LOG(LogTemp, Warning, TEXT("My name is ok"));
UE_LOG(LogTemp, Display, TEXT("My name is ok"));

// 打印到屏幕上
// -1 默认值,0.5 打印显示的时间,FColor::Red 显示的颜色,TEXT 文本内容
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("I love ws"));

启动运行后,窗口—>输出日志

image-20240819195008973

场景中

image-20240819194257883

准备工作

更换缓存路径

用记事本打开 引擎版本\Engine\Config\BaseEngine.ini

查找 %ENGINEVERSIONAGNOSTICUSERDIR%DerivedDataCache

替换为 %GAMEDIR%DerivedDataCache

基础操作

按住鼠标右键:视角旋转

右键+wasdqe:摄像机移动(速度可调整/同时鼠标滚轮移动)

内存浏览器:保存所有

内容—>添加—>添加功能或内容包—>蓝图—>添加其他游戏模式到项目—>世界场景设置—>游戏模式重载修改运行方式

坐标轴:红x轴正方向为前方,y为右方,z为上方

创建和移动物体

放置物体:获取/内容浏览器

移动:沿轴/沿平面/自由移动(沿世界轴/本地轴)

End键:模型对齐地面

旋转

缩放:某个维度缩放/平面缩放/整体缩放

世界轴:跟世界一致,不变

本地轴:跟随物体

表面对齐:

栅格吸附/网格对齐:长度/角度

image-20240813211946036

复制模型:ctrl+D / alt+拖拽

同时选中多个模型:选中第一个模型,ctrl加选

复制:平移对象模式,alt+拖拽、旋转对象模式,alt+拖拽(按角度旋转复制)