0%

Lecture 02: Review of Linear Algebra

向量

向量的模:

  • 作用:一般可以用来将一个向量变成一个单位向量

单位向量:

  • 作用:一般用于判断一个向量的方向

向量点乘:

作用:

  • 计算两个向量之间的夹角(如果两个向量是单位向量的话很方便)
  • 出一个向量在另一个向量的投影(投影有什么用?可以用于分解原来的向量)
  • 点乘可以告诉我们两个向量间的大体方向
    • 点乘结果 > 0,则说明是 forward
    • 点乘结果 < 0,则说明是 backward
    • 点乘结果 = 0,则说明是 垂直
  • 点乘还可以告诉我们两个向量有多接近:点乘结果(从进到远):1 -> 0 -> -1

向量叉乘:

作用:

  • 用于定义三维空间中的坐标系;
  • 判断 左 右
    • 规定向外z为正,如果 axb 得到结果为正,则说明b在a左边;
  • 判断 内 外
    • 依次看 ab x ap,bc x ap,ca x ap 的结果;
    • 如果 都是>0(p都在三条边的左边)或者 都是<0(p都在三条边的右边),则说明 p 在三角形内部;

矩阵

作用:就是用来做一些 空间变换

矩阵相乘

前提是内标相同;

没有交换律,但是有分配律和结合律;

矩阵 乘 向量

用于变换:比如对于一个向量,我们要让他关于y轴对称,那么我们要让这个向量乘什么矩阵呢?

转置

转置:每个元素的行列交换

单位矩阵

对角线都是1,其他都是0;

作用:

  • 用于求 矩阵的逆

向量相乘 和 矩阵相乘 的关系

Lecture 01: Overview of Computer Graphics

计算机图形学下关于“实时”的定义:>30fps,<30fps称为离线

model:真实模型
image:图像

计算机图形学(渲染):

  • (model) -> (image)
  • (model) -> (model)

计算机视觉:

  • (image)->(model)
  • (image)->(image)

Lecture 10:Geometry 1 (Introduction)

这节课主要讲:

  • 纹理的应用(放在上一篇文章了)
  • 几何的表示方法

几何的表示方法

  • 隐式的几何表示
  • 显示的几何表示

隐式的几何表示(Implicit)

概述

简单的讲就是,在空间中找到满足特定关系的点,用坐标公式表达一下;

优点:

  • 很容易判断点是否在面上。(将点代入f(x,y,z),正数表示点在图形外,负数表示点在图形内,0表示点在图形上。)
  • 表述起来很容易(对存储很有利)。
  • 容易做面与光线求交。
  • 容易描述和处理拓扑结构。

缺点:

  • 不好描述复杂的形状;
  • 不好知道这个面上有哪些点;
  • 很难从式子中看出表示的是什么形状。

分类:

  • 构造立体几何 CSG(Constructive Solid Geometry)(Implict)
  • 距离函数 Distance Function(Implict)
  • 水平集 Level Set Methods(Implict)
  • 分形 Fractals(Implict)

构造立体几何 CSG(Constructive Solid Geometry)

通过一系列基本几何的基本布尔运算来定义新的几何。

距离函数 Distance Function

给出任何一个位置到物体的最短距离。对一个点不描述表面,而是描述点到表面的最近距离。

把两个物体的距离函数都算出来然后把两个距离函数做融合(blending)

这个挺妙的;

比如:混合一个移动的边界

通过blend两个SDF可以得到移动后的边界。

分别求出两个物体对应的距离函数, 然后把他们blend起来,然后恢复成原本的面(找出SDF 的值为0的位置)

水平集 Level Set Methods

封闭形式的方程很难描述复杂的形状,所以选择一种替代方案:

存储一个值近似函数的网格(可以是二维的也可以是三维的)

通过找到插值为0的位置来找到表面,它具有一个对形状更加显示的控制。

eg:如果有一个三维的纹理表示人体不同位置的骨密度,可以让密度函数=某个具体的值,然后找到所有满足这个等式的位置,就可以得到一个表面。

分形 Fractals

局部和整体相似,一直递归;

eg:雪花,西兰花等;

这些类型的几何在渲染时会造成强烈的走样。

显示的几何表示(Explicit)

概述

所有的点直接给出,或通过参数映射定义表面。

优点:对于表示形状很容易。

缺点:不好判断点是否在表面上(或内、外)。

分类:

  • 点云 (Point Cloud)
  • 多边形网格(Polygon Mesh)

点云(Point Cloud)

  • 简单的表示为点的列表(x,y,z)
  • 可以简单的表示任何一种几何图形
  • 对于表示大数据集是很有用的(>>1 point/pixel)
  • 经常被转换成多边形网格
  • 难以在采样不足的区域画出来

多边形网格(Polygon Mesh)

  • 存储点和多边形(通常是三角形或四边形)
  • 容易去处理/模拟,进行自适应采样
  • 数据结构比较复杂
  • 图形学中最常用的表示

表示方式:The Wavefront Object File(.obj)格式

它是一个文本文件,由指定的顶点、法线、纹理坐标和它们的连接组成。

如下图表示了一个立方体:

v是顶点坐标

vn是法线(多了两条是因为建模误差)

vt是纹理坐标

f(face)表示面,比如f 5/1/1 1/2/1 4/3/1 表示这个三角形面是由第5、1、4个顶点组成的,三个点的纹理坐标是第1、2、3对应的纹理坐标,这个面的法线是第一条法线。

这篇文章记录一下 Blog 的一些使用指南:

source/_posts:里面放的就是文章,要写文章就加一篇上去;

文章的分类和标签在上面加上去就行;


操作流程

全部改完之后,在 Blog 文件夹打开 git bash,执行下面命令:
hexo clean
hexo g
hexo d

hexo cl && hexo g && hexo d


插入图片

首先在 _config.yml 配置文件里面的这个:
post_asset_folder: false
这个一定要为 false,之前不小心设置为 true 了,然后就一直插入失败…

语法

  1. md 语法

    1
    2
    3
    ![](/images/使用指南/1.png)

    ![](images/使用指南/1.png)
  2. html 语法

    1
    <img src="/images/使用指南/1.png">

images 文件夹是在 source 下面自己创建的!

路径名好像不能有空格。

这篇文章记录一下阅读 Programming in Lua 时的一些注意点,以及一些重要的模块。

ps:页数指的是书中的页数,不是 PDF 目录的页数;

GitHub - 0kk470/pil4: Lua程序设计第四版习题答案 (Programming in Lua 4th Edition Exercise Solutions)

第 3 章 表达式

P14

注意一下:

and 和 or 的运算结果不是 true 和 false,而是和它的两个操作数相关。

P16

首先我们看一下下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
polyline = 
{
color="blue", thickness=2, npoints=4,
{x=0, y=0},
{x=-10, y=0},
{x=-10, y=1},
{x=0, y=1},
5,
6,
"a",
"b",
}
print(#polyline) -- 输出:8

可以发现:

table 中的域(color,thickness,npoints)不会被算入table大小中,所以如果我们直接for i=1,#polyline 去遍历table,无法输出所有的内容。

但是我们可以通过泛型for去遍历,for name in pair(polyline ) do … end

Lua 打印table表内容_lua打印表-CSDN博客

注意语法:

1
2
3
4
5
6
7
8
s = "-"

polyline =
{
"+" = "-", -- 语法错误
["+"] = "-", -- 这才是对的,直接使用字符串要用[]包一下
s = "+", -- 间接使用则不用包
}

第 4 章 基本语法

P21

p22

(这里面讲到了,尽量不要改变控制变量的值…)

第 5 章 函数

p27

unpack()

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
polyline = 
{
color="blue", thickness=2, npoints=4,
{x=0, y=0},
{x=-10, y=0},
{x=-10, y=1},
{x=0, y=1},
5,
6,
"a",
"b",
}

print(table.unpack(polyline))
--[[
输出:
table: 00479BF0
table: 00479BA0
table: 00479CE0
table: 00479D08
5
6
a
b
可以看到域还是没输出。
]]--

可变参数

第 6 章 再论函数

p32

闭包(这个很重要呀)

  • 可以用于持久化数据,保存一些状态
  • 可以用于重写函数,保证安全性

p37

尾调用:

因为尾调用不需要额外的栈空间,所以可以用来优化;

注意分辨哪些是真正的尾调用

第 7 章 迭代器与泛型 for

p40

图片中的程序怎么理解

对于上面的解释,应该是第一次调用的时候,返回的是函数(还未调用),此时i=0(闭包 iter),然后第二次调用闭包 iter() 的时候i才会自增

同理,在这里的for in ipairs 中,ipairs()表达式是先计算好的,返回了三个值(闭包);然后for语句通过in不断去调用这个闭包,达到遍历的效果,而且是无状态的(不用先创建闭包出来)

第7章需要细品,其实是闭包的应用之迭代器

第 8 章 编译·运行·调试

pcall,xpcall

第 9 章 协同程序

重要的

第 12 章 数据文件与持久化

Lua string.format用法-CSDN博客

p89

12.1.2 保存带有循环的table,这一块比较复杂一点;

其实就是 记忆化搜索 的过程。

第 13 章 Metatables and Metamethods

1
2
3
4
Metatables 允许我们改变 table 的行为,例如,使用 Metatables 我们可以定义 Lua 如
何计算两个 table 的相加操作 a+b。当 Lua 试图对两个表进行相加时,他会检查两个表是
否有一个表有 Metatable,并且检查 Metatable 是否有__add 域。如果找到则调用这个__add
函数(所谓的 Metamethod)去计算结果。

p92

下面测试一下 算术运算的Metatables

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

Set1 = {}
Set1.mt = {}

function Set1.new(t)
local set = {}
setmetatable(set, Set1.mt)
for _, l in ipairs(t) do set[l] = true end
return set
end

function Set1.union (a,b)
local res = Set1.new{}
for k in pairs(a) do res[k] = true end
for k in pairs(b) do res[k] = true end
return res
end

Set1.mt.__add = Set1.union

---------------------

Set2 = {}
Set2.mt = {}

function Set2.new(t)
local set = {}
setmetatable(set, Set2.mt)
for _, l in ipairs(t) do set[l] = true end
return set
end

function Set2.union (a,b)
local res = Set2.new{}
for k in pairs(a) do
res[k] = b[k]
end
return res
end

Set2.mt.__add = Set2.union

--------------------

function fun_tostring (set)
local s = "{"
local sep = ""
for e in pairs(set) do
s = s .. sep .. e
sep = ", "
end
return s .. "}"
end

function fun_print (s)
print(fun_tostring(s))
end

---------------------

-- 测试两个都是同一种集合
s1 = Set1.new{1,2,3}
s2 = Set1.new{4,5,6}

s3 = s1 + s2
s4 = s2 + s1

fun_print(s3) -- {1, 2, 3, 4, 5, 6}
fun_print(s4) -- {1, 2, 3, 4, 5, 6}

-- 测试两个是不同种集合
s1 = Set1.new{1,2,3}
s2 = Set2.new{4,5,6}

s3 = s1 + s2
s4 = s2 + s1

fun_print(s3) -- {1, 2, 3, 4, 5, 6}
fun_print(s4) -- {}

总结:

首先 Metatable 可以理解为一个专门重载表的运算符的集合;

所有关于两个表的操作,都要定义在这个 Metatable 里面,定义的具体运算函数为 Metamethods;

规则:(针对 同一种 / 非同一种 表都适用)

  • 第一个 table 的 Metatable 有定义对应的 Metamethods,则用第一个表的;
  • 如果第一个表没定义,看第二个表是否有定义,有就用第二个的;
  • 都没定义,则会报错

关于 关系运算的Metamethods

两个 table 的 Metamethods 必须相同才能进行运算;

p97

表相关的 Metamethods

这一部分很妙啊!值得反复阅读;

主要是搞清楚:

  • __index metamethod
    • 可以理解为访问,当访问表中不存在的域时,会去找 __index metamethod
  • __newindex metamethod
    • 可以理解为更新,当对表中不存在的域进行赋值时,会去找 __newindex metamethod

第 14 章 环境

这一章我们将讨论一些如何操纵环境的有用的技术。

第 15 章 Packages

p111

私有成员

  • 将 Packages 中的函数声明为 local 的(私有)

  • 改进:所有的函数都声明为local的,在 Packages 的最后定义一下公有函数就行。

这一块好多技巧…

学了后面忘了前面… 复习一下;

Lua中setfenv()_lua setfenv-CSDN博客

第 16 章 面向对象程序设计

p119

这里讲到了 self 的调用;

p119 - 121(16.1 类)

lua 没有类的概念,更像是一种原型模式;

1
2
3
4
5
在这些语言中,对象没有类。相反,每个对象都有一个 prototype
(原型),当调用不属于对象的某些操作时,会最先会到 prototype 中查找这些操作。在
这类语言中实现类(class)的机制,我们创建一个对象,作为其它对象的原型即可(原
型对象为类,其它对象为类的 instance)。类与 prototype 的工作机制相同,都是定义了特
定对象的行为。

这里讲到了用 metatable 的操作去做一种 类和对象的关系;

但是实际上感觉讲的不准确… 实际上就是没有类… 就都是对象;

比如:setmetatable(a, {__index = b})

我个人的理解是 b是父对象,a是子对象;

当a找不到属性/方法的时候会去找b的;

这一小节后面的例子有点绕… 需要细品;

p121 - 122(16.2 继承)

这里讲一下为啥能够形成 (祖父-父-子) 的继承链关系:

  • 祖父:Account
  • 父:SpecialAccount
  • 子:s

其实我们把冒号写成点的形式就一目了然了(这里只讲new)

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
主要看下面这一段:
Account = {balance = 0}

function Account:new (o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end

这里我们把它用dot的形式写出来(也就是上一小节最后的样子):
function Account.new (Account, o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end

这里我 self 先不变,但是要知道,函数中的 self 就是函数的第一个参数!

-- 下面开始继承关系

首先第一次继承:
SpecialAccount = Account:new()
也就是调用了一次:SpecialAccount = Account.new (Account, nil)
可以看到此时new函数中的self是Account;
于此同时 o={},然后 o 的 metatable 为self(也就是Account),然后o返回出去给SpecialAccount(o <=> SpecialAccount);

然后第二次继承:(关键的来了)
s = SpecialAccount:new{limit=1000.00}
这里也就是调用了一次:s = SpecialAccount.new(SpecialAccount,{limit=1000.00});
但是但是,SpecialAccount是没有new函数的,所以去SpecialAccount的metatable里面找,找到Account的函数并调用:Account.new(SpecialAccount,{limit=1000.00});
可以看到,这里的self是SpecialAccount,而不是Account!!!
此时我们看看new函数里面做了啥:它把 {limit=1000.00} 的 metatable 设置为 self(也就是SpecialAccount)!然后返回给s;
也就是说 s 的 metatable 其实是 SpecialAccount,而 SpecialAccount 的 metatable 是 Account;
进而形成了继承链!

一开始看的时候也很懵...

第 17 章 Weak 表

  • 只有对象才会被回收

  • 可以通过weak表来进行垃圾回

  • 通过 metatable 的 __mode 域来表示 weak 表

  • 优化记忆函数:设置 记忆表 的 metatable 为 weak 表(value 为 weak,这样能节省空间)

  • 最后的两个例子好繁琐…

  • 大致的思想就是通过 weak 表来优化默认值的存储,允许被回收

第 18 章 数学库

  • y = math.rad(x)
    • 将 角度 x 转化为 弧度 y
  • x = math.deg(y)
    • 将 弧度 y 转换为 角度 x
  • math.random
    • 生成随机数(可以带 0/1/2 个参数)
  • math.randomseed(os.time())
    • 设置随机种子(通过系统时间)

第 19 章 table 库

p136(数组大小)

  • table.getn() 在 lua 5.0 以上已经被废弃了
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
table.getn() 等价于 #t

t = {n = 10}
print(#t) -- 输出:0

-- 下面做一个有意思的实验
local t = {nil}
print(#t) -- 输出:0

t = {1, nil}
print(#t) -- 输出:1

t = {nil,1}
print(#t) -- 输出:2

t = {nil,nil,1}
print(#t) -- 输出:3

t = {1,nil,2}
print(#t) -- 输出:3

t = {1,nil,2,nil}
print(#t) -- 输出:1

t = {1,nil,2,nil,3}
print(#t) -- 输出:5

t = {1,nil,2,nil,3,nil}
print(#t) -- 输出:3

是不是懵逼了… 我也没搞懂… 具体看这:

lua 中求 table 长度 | 菜鸟教程 (runoob.com)

  • 另外 table.setn() 好像也没了… 直接没有这个方法了

p137 插入删除

1
2
3
4
5
6
7
8
9
10
11
12
13
t = {1,2,3}

table.insert(t,4); -- 默认在最后插入
print(unpack(t)) -- 输出:1 2 3 4

table.insert(t,2,5); -- 在位置2插入值为5
print(unpack(t)) -- 输出:1 5 2 3 4

local v = table.remove(t) -- 默认在最后删除(并返回)
print(unpack(t)) -- 输出:1 5 2 3

v = table.remove(t,1) -- 删除位置为1的值(并返回)
print(unpack(t)) -- 输出:5 2 3
  • 排序讲的很清楚了,注意到最后一个例子又用到了闭包(迭代器)

第 20 章 String 库

函数

  • string.len(s)
    • 返回字符串长度
  • string.rep(s,n)
    • 返回重复了n次s的字符串
  • string.lower(s)
    • 将 s 中的大写字母转换成小写
  • string.upper(s)
    • 将小写转换成大写
  • string.sub(s,i,j)
    • 截取字符串,可以是负数,表示倒着数的第几个;
    • lua 中的字符串不会改变,所有操作之后是返回一个新的字符串;
  • string.char(97,98,99)
    • 将数字num转化为字符
  • string.byte(s,i)
    • 将字符串s的第i个字符转化为数字,i默认为1,i可以为负数
  • string.format()
    • 格式化字符串,和printf一样…
    • %d 十进制;%x 十六进制;%o 八进制
  • st,en = string.find(s, p, i)
    • 字符串查找(基于模式匹配,不是posix规范的正则表达式)
    • 从 s 的 i 位置开始,在 s 中找 p ,找到返回 开始st和结束下标en,找不到返回 nil;
  • s,count = string.gsub(s, p1, p2,times)
    • 全局字符串替换(基于模式匹配,不是posix规范的正则表达式)
    • 从 s 中 找 p1,并替换成 p2(全部替换)
    • times 是限制要替换几次;
    • count 是一共替换了几次;
  • string.gfind()
    • 全局字符串查找(基于模式匹配,不是posix规范的正则表达式)

模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
下面的表列出了 Lua 支持的所有字符类:

. 任意字符
%a 字母
%c 控制字符
%d 数字
%l 小写字母
%p 标点字符
%s 空白符
%u 大写字母
%w 字母和数字
%x 十六进制数字
%z 代表 0 的字符

上面字符类的大写形式表示小写所代表的集合的补集。例如,'%A'非字母的字符;

print(string.gsub("hello, up-down!", "%A", "."))
--> hello..up.down. 4
4 是替换的次数
1
2
3
4
5
6
Lua 中的特殊字符如下:
( ) . % + - * ? [ ^ $

特殊字符用 % 转义

其他的用 \ 转义
  • 其他的好复杂(主要是我不会正则表达式…我是fw)

  • 捕获、转换的技巧 也看不懂…

第 21 章 IO库

p157 简单IO模式

  • io.input(filename)
    • 打开给定的文件,并将其设置为当前输入文件;
  • io.output()
    • 设置当前的输出文件;
  • io.read()
    • 从当前输入文件读取字符串
    • “*all”:读取整个文件
    • “*line” :读取下一行
    • “*number” :从串中转换出一个数值
    • num :读取 num 个字符到串
  • io.lines()
    • io读取当前文件的迭代器

p160 完全IO模式

  • 通过文件句柄

  • f = io.open()

    • 打开文件,获得一个文件句柄,失败返回nil
  • f:read()

    • 读取文件f
  • 预定义的流句柄:

    • io.stdin
    • io.stdout
    • io.stderr
  • IO优化

    • *local lines, rest = f:read(BUFSIZE, “line”)
    • 读取整个文件时,避免对行进行分割;
    • 代码中的 rest 就保存了任何可能被段划分切断的行。
    • 具体使用看书
  • 二进制文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    local inp = assert(io.open(arg[1], "rb")) 
    local out = assert(io.open(arg[2], "wb"))
    local data = inp:read("*all")
    data = string.gsub(data, "\r\n", "\n")
    out:write(data)
    assert(out:close())

    -- 用 “b” 表示打开二进制文件

    > lua prog.lua file.dos file.unix'

    这样运行时,相当于是吧file.dos file.unix 当作参数传进去了;
    arg[1] = file.dos
    arg[2] = file.unix
    • 然后就是一些具体的例子了
  • 其他文件操作(知道一下,用到时具体查)

    • tmpfile
      • 返回临时文件句柄(rw),用完自动清除;
    • flush
      • 对文件进行修改
    • filehandle:seek(whence,offset)
      • 用来设置和获取一个文件当前的存取位置

第 22 章 操作系统库

  • os.time()

    • 单位:秒
    • 返回当前时钟的数值
    • 可以带参数,表示距离参数设定的日期的时间(有默认值,可以看一下书)
  • os.date( “ ”,seconds)

    • 可以理解为time函数的反函数;

    • 这个好高级啊,通过第一个参数设置不同的格式,输出不同的内容:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      %a		abbreviated weekday name (e.g., Wed) 
      %A full weekday name (e.g., Wednesday)
      %b abbreviated month name (e.g., Sep)
      %B full month name (e.g., September)
      %c date and time (e.g., 09/16/98 23:48:10)
      %d day of the month (16) [01-31]
      %H hour, using a 24-hour clock (23) [00-23]
      %I hour, using a 12-hour clock (11) [01-12]
      %M minute (48) [00-59]
      %m month (09) [01-12]
      %p either "am" or "pm" (pm)
      %S second (10) [00-61]
      %w weekday (3) [0-6 = Sunday-Saturday]
      %x date (e.g., 09/16/98)
      %X time (e.g., 23:48:10)
      %Y full year (1998)
      %y two-digit year (98) [00-99]
      %% the character '%'
  • os.clock()

    • 返回执行该程序 CPU 花去的时钟秒数。

    • 使用:

      1
      2
      3
      4
      5
      local x = os.clock()

      做一些操作...

      print(string.format("elapsed time: %.2f\n", os.clock() - x))
  • os.exit()

    • 终止一个程序
  • os.getnv(“”)

    • 得到“环境变量”的值。以“变量名”作为参数,返回该变量值的字符串:
  • os.execute( “ “ )

    • 执行一个系统命令(和 C 中的 system

      函数等价)

  • os.setlocale()

    • 设定 Lua程序所使用的区域(locale)。
    • 这个比较高级…

第 23 章 Debug库

自省

  • debug.getinfo()

    • 第一个参数:数字或者函数;

      • 函数:返回关于这个函数信息的一个表。
      • 数字:返回在 n 级栈的活动函数的信息数据。
        • 如果 n=1,返回的是正在进行调用的那个函数的信息。(n=0 表示 C 函数 getinfo 本身)
    • 第二个参数:可以用来指定选取哪些信息。

      1
      2
      3
      4
      5
      'n' selects fields name and namewhat 
      'f' selects field func
      'S' selects fields source, short_src, what, and linedefined
      'l' selects field currentline
      'u' selects field nup
    • 具体例子可以看书

  • debug.getlocal(level, index)

    • 访问任何活动状态的局部变量。

    • 参数level:将要查询的函数的栈级别;

    • 参数index:变量的索引。

    • 函数有两个返回值:变量名和变量当前

      值。

    • 书中有例子和解释;

  • debug.getupvalue(func, index)

  • 访问 Lua 函数的 upvalues。和局部变量

    不同的是,即使函数不在活动状态他依然有 upvalues(这也就是闭包的意义所在)。所以,

    getupvalue 的第一个参数不是栈级别而是一个函数(精确的说应该是一个闭包),第二个

    参数是 upvalue 的索引。

  • Lua的upvalue和闭包_lua upvalue-CSDN博客

Hooks

profiler

  • 好高级东西…

第 24 章 C API 纵览

  • 这一部分主要是 lua 和 c 的交互吧

  • C 和 Lua 之间来能个不协调的问题:

    • Lua 自动进行垃圾回收,C需要显示的分配存储单元;
    • Lua 中的动态类型和C中的静态类型不一致;

24.1 简介

  • lua_open():创建一个新环境(lua_state)
    • 所有的标准库以单独的包提供,所以如果你不需要就不会强求你使用它们。
    • 可以用 luaopen_io() 注册io库等等;

24.2 堆栈

  • 栈在 Lua 与 C 之间交换值;

  • 栈由Lua来管理,垃圾回收器知道那个值正在被 C 使用;

  • 压入元素:

    1
    2
    3
    4
    5
    6
    void lua_pushnil (lua_State *L); 
    void lua_pushboolean (lua_State *L, int bool);
    void lua_pushnumber (lua_State *L, double n);
    void lua_pushlstring (lua_State *L, const char *s,
    size_t length);
    void lua_pushstring (lua_State *L, const char *s);
  • int lua_checkstack (lua_State *L, int sz);

    • 它检测栈上是否有足够你需要的空间
  • 查询元素:

    • API 用索引来访问栈中的元素。在栈中的第一个元素(也就是第一个被压入栈的)有索引 1,下一个有索引 2,以此类推。我们也可以用栈顶作为参照来存取元素,利用负索引。

    • 检查一个元素是否是一个指定的类型:

      • int lua_is… (lua_State *L, int index);
      • lua_isnumber,lua_isstring,lua_istable;
    • 获取栈中元素的类型:

      • lua_type
      • 类型常量:LUA_TNIL、LUA_TBOOLEAN 、 LUA_TNUMBER 、 LUA_TSTRING 、 LUA_TTABLE 、LUA_TFUNCTION、LUA_TUSERDATA 以及 LUA_TTHREAD。
    • 获取栈中元素的值:

      • 用 lua_to*函数:

      • int lua_toboolean (lua_State *L, int index); 
        double lua_tonumber (lua_State *L, int index); 
        const char * lua_tostring (lua_State *L, int index); 
        size_t lua_strlen (lua_State *L, int index);
        
        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

        - 当元素类型和函数类型不匹配时:

        - lua_toboolean、lua_tonumber 和 lua_strlen 返回 0,其他函数返回 NULL。

        - 注意:

        - 当一个 C 函数返回后,Lua 会清理他的栈,所以,有一个原则:永远不要将指向 Lua 字

        符串的指针保存到访问他们的外部函数中。

        - 其他堆栈操作

        ```lua
        int lua_gettop (lua_State *L);
        -- 返回堆栈中元素个数,它也是栈顶元素的索引;

        void lua_settop (lua_State *L, int index);
        --[[
        lua_settop 设置栈顶(也就是堆栈中的元素个数)为一
        个指定的值。如果开始的栈顶高于新的栈顶,顶部的值被丢弃。否则,为了得到指定的
        大小这个函数压入相应个数的空值(nil)到栈上。特别的,lua_settop(L,0)清空堆栈。你
        也可以用负数索引作为调用 lua_settop 的参数;那将会设置栈顶到指定的索引。利用这
        种技巧,API 提供了下面这个宏,它从堆栈中弹出 n 个元素:
        #define lua_pop(L,n) lua_settop(L, -(n)-1
        ]]--

        void lua_pushvalue (lua_State *L, int index);
        -- lua_pushvalue 压入堆栈上指定索引的一个抟贝到栈顶;

        void lua_remove (lua_State *L, int index);
        -- lua_remove 移除指定索引位置的元素,并将其上面所有的元素下移来填补这个位置的空白;

        void lua_insert (lua_State *L, int index);
        --lua_insert 移动栈顶元素到指定索引的位置,并将这个索引位置上面的元素全部上移至栈顶被移动留下的空隔;

        void lua_replace (lua_State *L, int index);
        --lua_replace 从栈顶弹出元素值并将其设置到指定索引位置,没有任何移动操
        作。
      • 24.2 有例子

第 25 章 扩展你的程序

p189

用 Lua 做配置文件,书里有个例子,讲的比较清除;

  • luaL_loadfile(L, filename)
    • 加载 lua 文件
  • lua_pcall
    • 调用函数
  • lua_getglobal(L , “globalname”)
    • 每调用一次就把相应的变量值压入lua栈顶

25.1 表操作

书里讲的不是特别清除… 看这里:

lua_gettable 函数详解_lua_getglobal-CSDN博客

这个讲的很清楚;

  • 关键是要理解 lua_gettable:
    • 先把key值压入栈顶
    • 调用 lua_gettable(L, index) 时,会弹出栈顶的key值,然后找到lua栈中对应的index的table,然后再取到 table 的 key 值(table[key]),然后将这个值再次压入栈顶;

lua_gettable和lua_settable - 乐swap火 - 博客园 (cnblogs.com)

  • lua_settable 可以看这里,也是比较好理解的;
    • 修改表中对应key值的value
    • 先把key入栈,再把value入栈,然后调用 lua_settable(L, index);
    • 此时会把 key 和 value 都弹出栈,然后将lua栈中的对应的index 的table 的 key 修改为value

25.2 调用 lua 函数

  • 将被调用的函数入栈(可以用lua_getglobal,将要执行的函数入栈)
  • 以此将所有参数入栈
  • 使用 lua_pcall 调用函数;
    • 这一小节的最后有详细介绍了lua_pcall 函数;
    • 参数:
      • lua 栈
      • 参数个数
      • 返回值个数
      • 错误处理函数(0表示没有,如果有,需要在函数和参数入栈之前入栈)
  • 最后从栈中获取函数执行的返回结果

第 26 章 调用 C 函数

  • 将 C 函数注册到 Lua 中去;
  • 用来交互的栈不是全局变量,每一个函数都有他自己的私有栈。
  • 函数在将返回值入栈之前不需要清理栈,因为C函数会返回一个(返回值个数)
  • C函数返回之后,Lua 自动的清除栈中返回结果下面的所有内容。
  • 呃,注册函数这一块有点小复杂啊;

第 27 章 撰写 C 函数的技巧

在这一章我们将讨论数组操纵、string 处理、在 C 中存储 Lua 值等一些特殊的机制。

27.1 数组操作

1
2
3
4
5
6
7
8
9
-- 获取数组(table)表中的元素(key对应的value)
void lua_rawgeti (lua_State *L, int index, int key);

-- 设置数组(table)表中的元素(key对应的value,value应该放在栈顶,然后才能设置)
void lua_rawseti (lua_State *L, int index, int key);


index 指向 table 在栈中的位置;
key 指向元素在 table 中的位置。

27.2 字符串处理

  • C函数 接受 lua 的字符串作为参数时的原则:

    • 当字符串正在被访问的时候不要将其出栈;
    • 永远不要修改字符串;
  • lua_pushlstring(L, start, len)*

    • 从start开始截取长度为len的字符串,并压入栈顶;
    • start*时一个指针,比如 s+i,s=char*
  • lua_concat(L,n)

    • 将连接(同时会出栈)栈顶的 n 个值,并

      将最终结果(字符串)放到栈顶。

  • 提高连接大量字符串的效率:使用辅助库提供的 buffer 相关函数;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void luaL_buffinit (lua_State *L, luaL_Buffer *B); 

    void luaL_putchar (luaL_Buffer *B, char c);

    void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l);

    void luaL_addstring (luaL_Buffer *B, const char *s);

    void luaL_pushresult (luaL_Buffer *B);


    -- 来将位于栈顶的值放入 buffer
    void luaL_addvalue (luaL_Buffer *B);
    • 具体例子可以看书;

27.3 在 C 函数中保存状态

27.3.1 The Registry

  • Lua 提供了一个独立的被称为 registry 的表,C代码可以自由使用,但 Lua 代码不能访问他。
  • 由于所有的 C 库共享相同的 registry ,你必须注意使用什么样的值作为 key,否则会导致命名冲突。一个防止命名冲突的方法是使用 static 变量的地址作为 key:C 链接器保证在所有的库中这个 key 是唯一的。
  • 书中有使用例子;

27.3.2 References

  • 永远不要使用数字作为 registry 的 key,因为这种类型的 key 是保留给 reference 系统使用。

  • Reference 系统是由辅助库中的一对函数组成,这对函数用来不需要担心名称冲突的将值保存到 registry 中去。

    1
    2
    3
    int r = luaL_ref(L, LUA_REGISTRYINDEX);

    从栈中弹出一个值,以一个新的数字作为 key 将其保存到 registry 中,并返回这个key。我们将这个 key 称之为reference。
  • 作用:

    • 将一个指向 Lua 值的 reference 存储到一个 C 结构体中。
    • 当我们需要这种指针(指向Lua对象的指针,Lua 本身不提供指向其他对象的指针)的时候,我们创建一个 reference 并将其保存在 C 中。
    1
    2
    3
    4
    5
    6
    -- 要想将一个 reference 的对应的值入栈,只需要:
    lua_rawgeti(L, LUA_REGISTRYINDEX, r);

    -- 最后,我们调用下面的函数释放值和 reference:luaL_unref(L, LUA_REGISTRYINDEX, r);

    -- reference 系统也定义了常量 LUA_NOREF,她是一个表示任何非有效的 reference 的整数值,用来标记无效的 reference。任何企图获取 LUA_NOREF 返回 nil,任何释放他的操作都没有效果。

27.3.3 Upvalue

  • C 闭包… 有点高级…

第 28 章 User-Defined Types in C

28.1 Userdata

我们首先关心的是如何在 Lua 中表示数组的值。Lua 为这种情况提供专门提供一个基本的类型:userdata

一个 userdatum 提供了一个在 Lua 中没有预定义操作的 raw 内存区域。

Lua API 提供了下面的函数用来创建一个 userdatum:

**void lua_newuserdata (lua_State L, size_t size);

  • lua_newuserdata 函数按照指定的大小分配一块内存,将对应的 userdatum 放到栈内,并返回内存块的地址。

下面看一下书中的例子:

在 C 中定义数据类型(数组),然后给 lua 去使用:

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

// 表示一个 double 类型的数组
typedef struct NumArray
{
int size; // 数组大小
double values[1]; // 相当于是标记一下值
} NumArray;

// 创建数组(Lua 那边传进来一个数组的大小 n)
static int newarray (lua_State *L)
{
int n = luaL_checkint(L, 1); // 获取数组大小
size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double); // 求一下数组占用的内存大小
NumArray *a = (NumArray *)lua_newuserdata(L, nbytes); // 创建一个 userdatum(就是一块内存空间,放在栈中)
a->size = n; // 设置一下数组的大小
return 1; // 到这里,一个新的 userdatum 就在栈中了
}

// 设置数组的值(传进来一个 数组指针,下标,值)
static int setarray (lua_State *L) {
NumArray *a = (NumArray *)lua_touserdata(L, 1); // 获取数组(userdatum)
int index = luaL_checkint(L, 2); // 获取要修改的下标
double value = luaL_checknumber(L, 3); // 获取值
luaL_argcheck(L, a != NULL, 1, "`array' expected"); // 检查参数
luaL_argcheck(L, 1 <= index && index <= a->size, 2, "index out of range"); // 检查参数
a->values[index-1] = value; // 设置一下值
return 0;
}

// 获取一个数组元素(传进来: 数组指针, 下标)
static int getarray (lua_State *L)
{
NumArray *a = (NumArray *)lua_touserdata(L, 1); // 获取数组
int index = luaL_checkint(L, 2); // 获取下标
luaL_argcheck(L, a != NULL, 1, "'array' expected");
luaL_argcheck(L, 1 <= index && index <= a->size, 2, "index out of range");
lua_pushnumber(L, a->values[index-1]); // 获取数组的值, 并压入栈
return 1;
}

// 获取数组的带下
static int getsize (lua_State *L)
{
NumArray *a = (NumArray *)lua_touserdata(L, 1);
luaL_argcheck(L, a != NULL, 1, "`array' expected");
lua_pushnumber(L, a->size);
return 1;
}

// 注册一下 C 函数给 Lua 调用
static const struct luaL_reg arraylib [] =
{
{"new", newarray},
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};

// 打开这个C库
int luaopen_array (lua_State *L)
{
luaL_openlib(L, "array", arraylib, 0);
return 1;
}
1
2
3
4
5
6
7
8
-- 下面就是使用的例子了
a = array.new(1000) -- 创建一个大小为1000的数组
print(a) -- 输出: userdata 的地址
print(array.size(a)) -- 输出: 1000
for i=1,1000 do
array.set(a, i, 1/i)
end
print(array.get(a, 10)) --> 0.1

28.2 Metatables

细节… 复杂起来了;

为数组创建了一个metatable(记住userdata 也可以拥有 metatables)。下面,我们每次创建一个新的数组的时候,我们将这个单独 metatable 标记为数组的 metatable。每次我们访问数组的时候,我们都要检查他是否有一个正确的 metatable。

因为 Lua 代码不能改变 userdatum 的 metatable,所以他不会伪造我们的代码。

具体例子可以看一下书;

相当于是通过 metatable 给数组打了一个标记,区分了 数组 和 其他的 userdata;

然后在调用的时候检查一下 userdata 是否是数组;

28.3 访问面向对象的数据

复杂起来了…

这一小节讲的是如何通过面向对象的方式去访问数据;

1
2
3
4
5
a = array.new(1000)

array.size(a)
=>
a:size()

这儿的关键在于__index 元方法(metamethod)的使用。对于表来说,不管什么时候只要找不到给定的 key,这个元方法就会被调用。

28.5 Light Userdata

《Lua5.4 源码剖析——基本数据类型 之 UserData》 - 知乎 (zhihu.com)

Light userdata 真正的用处在于可以表示不同类型的对象。当 full userdata 是一个对象的时候,它等于对象自身;另一方面,light userdata 表示的是一个指向对象的指针,同样的,它等于指针指向的任何类型的 userdata。所以,我们在 Lua 中使用 light userdata表示 C 对象。

Lua 垃圾回收

Lua的垃圾回收可以参考下面这几篇文章,讲的比较详细:

Lua的垃圾回收机制详解_lua垃圾回收机制-CSDN博客

【Lua学习笔记】Lua进阶——垃圾回收_lua 垃圾回收-CSDN博客(这个里面的图比较好)

Lua GC机制原理、过程、源码详解-CSDN博客(这个里面文字阐述的比较好,有流程图和源码分析)

Lua 垃圾回收 | 菜鸟教程 (runoob.com)

Lua 实现了一个增量标记-扫描Mark and Sweep)收集器。 它使用这两个数字来控制垃圾收集循环:

  • 垃圾收集器间歇率
    • 控制着收集器需要在开启新的循环前要等待多久。
    • 增大这个值会减少收集器的积极性。
  • 垃圾收集器步进倍率
    • 控制着收集器运作速度相对于内存分配速度的倍率。
    • 增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。

待完成

这篇文章是 GAMES 104 随笔

主要是暂时没那么多时间去详细记录,等以后有时间二刷了再细细总结;

这个是 Games104 的官网,做的是真的好!

GAMES104 - 现代游戏引擎入门必修课 (boomingtech.com)

第二节 引擎架构的分层

这里直接搬了别人的,侵删。

Games104学习笔记(2) - 知乎 (zhihu.com)

GAMES104:现代游戏引擎架构分层 - 知乎 (zhihu.com)

记录一些 UE 资源、小知识点、遇到的问题

资源

UE蓝图基础_@大宁字的博客-CSDN博客

UE蓝图进阶_@大宁字的博客-CSDN博客

新手指南 | Epic Developer Community (epicgames.com)

业界大佬们:

大钊 - 知乎 (zhihu.com)

南京周润发 - 知乎 (zhihu.com)

Jiff - 知乎 (zhihu.com)

大侠刘茗Marin - 知乎 (zhihu.com)

Jerish - 知乎 (zhihu.com)

陶仁贤 - 知乎 (zhihu.com)

安柏霖 - 知乎 (zhihu.com)

顾煜 - 知乎 (zhihu.com)

小知识

输出

1
2
3
4
5
6
7
8
9
10
11
12
// 输出日志到文件中
UE_LOG(LogTemp, Warning, TEXT("Hello"));
UE_LOG(LogTemp, Error, TEXT("Hello"));
UE_LOG(LogTemp, Log, TEXT("Hello"));

// 输出到屏幕上
GEngine->AddOnScreenDebugMessage(-1, 20, FColor::Yellow, "Hello");

// 有时候要输出一下 FSting
FString fs = "asd";
UE_LOG(LogTemp, Error, TEXT("%s"), *fs);

获取

获取,设置 Actor 的位置和旋转

1
2
3
4
5
6
FVector NewLocation = GetActorLocation();
FRotator NewRotation = GetActorRotation();

SetActorLocationAndRotation(NewLocation, NewRotation);

// 这些函数都是 Actor 的成员函数,继承后直接用就行了

获取 GameState

1
2
3
AMyGameStateBase* GS = Cast<AMyGameStateBase>(GetWorld()->GetGameState());

GetWorld() 是 Actor 里面的函数;

获取 GameMode

1
2
3
4
5
6
7
8
9
// 方法一:
#include "Kismet/GameplayStatics.h"
const AMyGameModeBase* GM = Cast<AMyGameModeBase>(UGameplayStatics::GetGameMode(this));

// 方法二:
AMyGameModeBase* GM = Cast<AMyGameModeBase>(GetWorld()->GetAuthGameMode());

// 方法三:(GetDefaultGameMode() 是 AGameStateBase 里面的)
const AMyGameModeBase* GM = Cast<AMyGameModeBase>(GetDefaultGameMode());

通过反射,直接获取蓝图中配置的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FString AIPath = "Blueprint'";
AIPath.Append(M_SoldierInfoList[i].ToString());
AIPath.Append("'");
UClass* AIClass = LoadClass<AAICharacter_Base>(NULL, *AIPath);

if (AIClass)
{
AAICharacter_Base* AI_Base = Cast<AAICharacter_Base>(AIClass->GetDefaultObject());
if (AI_Base)
{
// 通过 GetDefaultObject() 获取到的 AI_Base 就是蓝图本身的这个Object;
// 然后在这里就可以直接获取到蓝图身上的东西了(我们可以往蓝图上面配置东西)
}
}

加载

从 c++ 中加载 UI

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
// 方法1:
FString HUDClassLoadPath = FString(TEXT("Blueprint'/Game/Blueprints/HUD.HUD_C'"));
auto MyUIClass = LoadClass<UUserWidget>(NULL, *HUDClassLoadPath);
if (MyUIClass != nullptr)
{
UUserWidget* MyUI = CreateWidget<UUserWidget>(GetWorld(), MyUIClass);
if (MyUI != nullptr) MyUI->AddToViewport();
}

// 方法2:
FString UiPath = TEXT("/Script/UMGEditor.WidgetBlueprint'/Game/Blueprints/HUD.HUD_C'");//动态加载UI 类
UClass* MyUIClass = LoadClass<UUserWidget>(nullptr, *UiPath);
if (MyUIClass)
{
UUserWidget* MyUI = UWidgetBlueprintLibrary::Create(GWorld, MyUIClass, nullptr);//创建蓝图添加到窗口
MyUI->AddToViewport();
}

/*
注意:

1. 需要包含头文件:
#include "Blueprint/UserWidget.h"
#include "Blueprint/WidgetBlueprintLibrary.h"

2. 两种方法都得在当前项目的 .Build.cs 文件中检查是否有包含 "UMG" 模块,没有的话需要添加上去,这样才能编译通过;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "UMG" });
(看到最后一个没有~)
*/

在 C++ 代码中设置 GameMode

1
2
3
4
5
6
7
8
9
10
// TestGameGameMode 构造函数中设置对应的 GameMode 的参数,当然要先引入下面对应类的头文件
ATestGameGameMode::ATestGameGameMode()
{
GameStateClass = ATestGameState::StaticClass();
PlayerControllerClass = ATestPlayerController::StaticClass();
PlayerStateClass = ATestPlayerState::StaticClass();
HUDClass = ATestHUD::StaticClass();
DefaultPawnClass = ATestPawn::StaticClass();
SpectatorClass = ATest_SpectatorPawn::StaticClass();
}

C++类 加载 蓝图类

1
2
3
4
5
6
7
8
9
10
11
FString CharacterClassLoadPath = FString(TEXT("Blueprint'/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter.ThirdPersonCharacter_C'"));	// 路径
DefaultPawnClass = LoadClass<AMyProject02Character>(NULL, *CharacterClassLoadPath); // 动态加载

// 这里需要注意这个 TEXT 里面的规则... 真的是很烦...
// Blueprint'路径';最后的蓝图类是 blueprintname.blueprintname_C

// 注意字符串的拼接
FString s = "Blueprint'";
s.Append(ClassPath.ToString()); // ClassPath 是 FSoftClassPath
s.Append("'");
UClass* BP_ProjectileClass = LoadClass<AProjectile>(NULL, *s);

创建销毁

创建组件,初始化组件,使用组件

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
/*--------------- .h 文件-----------------*/ 	
UStaticMeshComponent *VisualMesh = nullptr;


/*--------------- .cpp 文件-----------------*/
// 一般在构造函数里面做
VisualMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisualMesh")); // 创建组件

// 把 VisualMesh 作为根组件
RootComponent = Cast<USceneComponent>(VisualMesh);

// 将组件附在另一个组件下面
Box->SetupAttachment(VisualMesh);

// 设置碰撞预设
VisualMesh->SetCollisionProfileName(TEXT("BlockAllDynamic"));

// 设置模型(注意,ConstructorHelpers::FObjectFinder 只能在构造函数中用,否则会崩溃)
static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeVisualAsset(TEXT("/Game/StarterContent/Shapes/Shape_Cube.Shape_Cube")); // 注意路径格式
if (CubeVisualAsset.Succeeded())
{
VisualMesh->SetStaticMesh(CubeVisualAsset.Object);
}

// 设置位置
VisualMesh->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));

创建、销毁Actor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AMyBox *mybox = GetWorld()->SpawnActor<AMyBox>(SpawnLocation, SpawnRotation);	// 创建
mybox->destroy(); // 销毁

// 创建一个特效类对象,附在MyBox类对象上面(这里的this是MyBox的对象)
AMyEffect *MyEffect = GetWorld()->SpawnActor<AMyEffect>();
MyEffect->AttachToActor(this, FAttachmentTransformRules::KeepRelativeTransform);

// 延迟创建 Actor,用于完整的创建,放置还未初始化就被销毁的情况
AProjectile* NewProjectile = Cast<AProjectile>(UGameplayStatics::BeginDeferredActorSpawnFromClass(this, BP_ProjectileClass, SpawnTransform, ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn));
if (NewProjectile)
{
// 在这里就是完成创建之前要做的事情
NewProjectile->SetOwner(this);
NewProjectile->Init(this);
}
UGameplayStatics::FinishSpawningActor(NewProjectile, SpawnTransform);

粒子效果的使用实例

1
2
3
4
5
6
7
8
9
10
11
class UParticleSystem* PickUpFX;
class UParticleSystem* StaticFX;

// Actor 消失,特效不消失
UGameplayStatics::SpawnEmitterAtLocation(this, PickUpFX, GetActorLocation());

// MeshComp 是一个组件,特效附着在组件上面,Actor销毁,特效也销毁
UGameplayStatics::SpawnEmitterAttached(StaticFX, MeshComp);

// 这一篇将了随着Actor销毁,特效销毁/不销毁 的两种做法。
https://blog.csdn.net/weixin_36728104/article/details/82893670

数学计算

计算两个位置的距离

1
FVector::Distance(FVector1, Fvector2);

随机数的使用

1
2
#include "Kismet/KismetMathLibrary.h"
float RandomX = UKismetMathLibrary::RandomFloatInRange(MIN_X, MAX_X);

时间的使用

1
2
3
4
5
6
7
8
9
10
11
12
// 获取当前的游戏时间
double CurrentTime = FDateTime::Now().GetTimeOfDay().GetTotalMilliseconds();

// 输出年月日
FDateTime tt = FDateTime::Now();
int year = tt.GetYear();
int month = tt.GetMonth();
int day = tt.GetDay();
int hour = tt.GetHour();
int minute = tt.GetMinute();
int second = tt.GetSecond();
UE_LOG(LogTemp, Error, TEXT("Time : %d, %d, %d, %d, %d, %d"), year, month, day, hour, minute, second);

定时器的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明一个定时器 Handle,用于特效对象的延迟销毁
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &AMyEffect::OnDestroyed, 2.0f, false);

// 检测定时器是否活跃
GetWorldTimerManager().IsTimerActive(M_TimerHandle) == true;

// 清楚定时器
GetWorldTimerManager().ClearTimer(M_TimerHandle);

// 这个可以看一下官方文档
https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Timers/

// 但其实关于定时,我们可以通过记录时间的方式来实现,这样更加简单。

AI

运行行为树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	UPROPERTY()
class UBehaviorTreeComponent* M_BehaviorTree;

UPROPERTY()
class UBlackboardComponent* M_Blackboard;

UPROPERTY(EditAnywhere, Category = "BaseConfig")
class UBehaviorTree* BTree = nullptr;

UBehaviorTree* AAICharacter_Base::GetBTree()
{
return BTree;
}

void AAIController_Base::RunAIBehaviorTree(APawn* InPawn)
{
AAICharacter_Base* AI = Cast<AAICharacter_Base>(InPawn);
if (AI && AI->GetBTree())
{
M_Blackboard->InitializeBlackboard(*AI->GetBTree()->BlackboardAsset); // 初始化AI中的黑板,黑板是在行为树中的。
M_BehaviorTree->StartTree(*AI->GetBTree()); // 开始运行行为树
}
}

使用感知组件

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
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AIPerceptionComponent.h"

UPROPERTY()
class UAISenseConfig_Sight* SightConfig = nullptr;

UPROPERTY()
class UAIPerceptionComponent* M_AIPerception = nullptr;

// 构造函数
AAIController::AAIController()
{
M_AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>("AIPerception");
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("AISightConfig"));
if (M_AIPerception)
{
M_AIPerception->ConfigureSense(*SightConfig);
M_AIPerception->SetDominantSense(SightConfig->GetSenseImplementation());
}
}

// 注意绑定感知函数不能在构造函数中,否则不生效,所以放在 BeginPlay 里面
void AAIController::BeginPlay()
{
Super::BeginPlay();

if (M_AIPerception)
{
M_AIPerception->OnTargetPerceptionUpdated.AddDynamic(this, &AAIController::OnTargetPerceptionUpdated);
}

UAIPerceptionSystem::RegisterPerceptionStimuliSource(this, UAISense_Sight::StaticClass(), this);
}


// 设置 AI 感知组件的参数
void AAIController_CloseCombat::InitAIPerception(APawn* InPawn)
{
// 写自己的设置参数逻辑
// ......

// 注意设置完需要重新配置一下,否则不生效!!!
M_AIPerception->ConfigureSense(*SightConfig);
}

枚举的使用

1
2
3
4
5
6
7
8
9
10
11
UENUM(BlueprintType)
enum class ECampType : uint8
{
NONE,
RED = 1 UMETA(DisplayName = "Red"),
BLUE = 2 UMETA(DisplayName = "Blue"),
};

// 枚举转换成字符串
UEnum* const CampTypeEnum = StaticEnum<ECampType>();
FString s = CampTypeEnum->GetDisplayNameTextByValue(static_cast<uint8>(ECampType::RED)).ToString();

遍历 TMap

1
TMap<FString, int>::TConstIterator iter = _Map.CreateConstIterator(); iter; ++iter)

问题

导入项目时,vs显示不支持?

1
2
3
答:vs版本不匹配,可以去网上找找 vs 和 ue 版本的匹配程度;我是:
ue 4.24 <=> vs 2017
ue 4.27 <=> vs 2022

换了之后 vs 编译时显示 sdk 有问题?

1
2
3
4
5
6
7
8
9
这个搞了好久...盲猜删2017的时候给删掉了...

报错信息:
/*
Could not find NetFxSDK install dir; this will prevent SwarmInterface from installing. Install a version of .NET Framework SDK at 4.6.0 or higher.
*/

参考这篇文章:
https://zhuanlan.zhihu.com/p/133456753

分享一下查阅到的资料,总结的太好了!

UPROPERTY()

UFUNCTION()

UCLASS()

USTRUCT()

UINTERFACE()

UPPARAM()

UENUM() 、UMETA()

下面是自己总结自用的,感觉有些不常用的我就没列出来了,可以看看原文,写的真的详细,还有图片展示。

UPROPERTY()

主要有一下相关分类:

  • 编辑器
  • 数据修饰的
  • 序列化
  • 蓝图逻辑
  • 网络
  • UMG
  • 数据表
  • 蓝图
  • Console
  • 脚本
  • 资产注册
  • 配置文件
  • 动画
  • 材质
  • C++

实在是太多了…

下面是选项:

VisibleAnywhere

VisibleDefaultsOnly

VisibleInstanceOnly

EditAnywhere

EditInstanceOnly

  • 蓝图 细节面板 可以编辑,蓝图图表中 不行;

EditDefaultsOnly

meta=(HideInDetailPanel)

  • 显然,隐藏

meta=(ShowOnlyInnerProperties)

  • 用于修饰结构体成员变量,意思就是,被修饰的结构体会被展开,但是不能点击(编辑),而且被嵌套在里面的结构体不会被展开!可以用于隐藏结构。

Category = “abc”

  • 分类,把被同一 Category 修饰的属性在细节面板中分类在一起;
  • 可以嵌套分类:
    • Animals
      • Animals|Birds
      • Animals|Dogs
    • Animals 类别下又有两类:Birds,Dogs。

meta=(DisplayName=”abc”)

  • 将被修饰的属性改名字为 abc,配合 EditAnywhere 一起用;

meta=(ToolTip=“abc”)

  • 将鼠标悬停在属性上时,显示包含此文本的工具提示(abc)。

AdvancedDisplay

  • 将属性折叠起来,需要点击箭头才能展开
  • 不兼容:SimpleDisplay

SimpleDisplay

  • 和上面是相反的属性修饰

InlineEditConditionToggle

  • 将一个 bool 类型的成员变量,附在被 InlineEditConditionToggle 修饰的属性的左边,也就是两个属性合在一起;bool 不会单独显示;

DisplayAfter

  • 用于定义属性在编辑器展示的先后顺序,详细看原文;

DisplayPriority

  • 这个更牛,给编号,按编号从小到大排序属性;

然后是一些限制编辑操作的:
数值上的

meta = (AllowedClasses = “Texture”)

  • 限制一下类类型

BlueprintAssignable

  • 定义委托的时候声明这个,表示蓝图可以使用该委托

UFUNCTION

下面是相关的分类:

  • 蓝图逻辑
  • 编辑器
  • 常规
  • c++
  • Console
  • 外观
  • Debug
  • 网络

常用的就是前面几个蓝图逻辑

BlueprintCallable

蓝图可调用

BlueprintNativeEvent 和 BlueprintImplementableEvent

UE4函数标记BlueprintImplementableEvent和BlueprintNativeEvent区别-CSDN博客

后面还有好多 …

这下知道为什么 UE 好资料比较少了…

一拉就是一大块,又多又杂又难…

没有时间和毅力真的很难坚持…