Home

Awesome

Game-Tricks

仓库大小 Unity Language love :smile:

<!-- TOC --> <!-- /TOC -->

客户端

场景

NO.1 在圆内随机生成点

圆内的笛卡尔方程:

math

math

极坐标:

math

math

随机点(x, y):

math

math

<!-- x^2 + y^2 <= r^2 x^2 + y^2 = random * r^2 \rho = \sqrt{random} * r \theta = 2 * \pi * random x = x = x\_center %2b \rho * cos(\theta) y = y\_center %2b \rho * sin(\theta) -->

NO.2 虚拟摇杆

虚拟摇杆

NO.3 小地图

小地图

Light 光照

光照

Lambert(兰伯特):漫反射

math

Half-Lambert(半-兰伯特):漫反射优化

math

Phong(冯氏):高光反射

math

math

Blinn-Phong(布林-冯氏):高光反射优化

math

math

<!-- I_{diff} = K_d \ast I_l \ast (N \cdot L) I_{diff} = K_d \ast I_l \ast (\alpha (N \cdot L) + \beta I_{spec} = K_s \ast I_l \ast (V \cdot R)^{n_s} R = 2 \ast (N \cdot L) \ast N - L I_{spec} = K_s \ast I_l \ast (N \cdot H)^{n_s} H = \frac{L %2b V}{\vert L %2b V \vert} -->

Xray 透视

透视

第一遍透视绘制:ZWrite Off、Greater。(关闭深度缓存)

第二遍正常绘制:ZWrite On、LEqual。

算法

AStar 寻路算法

AStar

A*寻路算法在Unity中的简单应用

while(OPEN!=NULL)
{
    从OPEN表中取f(n)最小的节点n;
    if(n节点==目标节点)
        break;
    for(当前节点n的每个子节点X)
    {
        计算f(X);
        if(XinOPEN)
            if(新的f(X)<OPEN中的f(X))
            {
                把n设置为X的父亲;
                更新OPEN表中的f(n);
            }
        if(XinCLOSE)
            continue;
        if(Xnotinboth)
        {
            把n设置为X的父亲;
            求f(X);
            并将X插入OPEN表中;//还没有排序
        }
    }//endfor
    将n节点插入CLOSE表中;
    按照f(n)将OPEN表中的节点排序;//实际上是比较OPEN表内节点f的大小,从最小路径的节点向下进行。
}//endwhile(OPEN!=NULL)

FSM 状态机

NullTransition = 0
FindPlayer = 1
LosePlayer = 2
NullStateId = 0
Patrol = 1
Chase = 2
private Dictionary<Transition, StateId> transDict; // Transition字典
public StateId stateId; // State Id
public FSMSystem fsm; // 状态机
...
public void AddTransition(Transition trans, StateId id); // 添加Transition
public void RemoveTransition(Transition trans); // 移除Transition
public StateId GetState(Transition trans); // 获取State
public virtual void DoBeforeEnter() { } // 进入State之前
public virtual void DoBeforeExit() { } // 退出State之前
public abstract void DoUpdate(); // 在State中
private Dictionary<StateId, FSMState> stateDict; // State字典
public FSMState currentState; // 当前State
...
public void AddState(FSMState state); // 添加State
public void RemoveState(FSMState state); // 移除State
public void DoTransition(Transition trans); // 执行Transition
public void StartState(StateId id); // 开始State

Manager 管理类(Audio、Pool、Scene...)

public abstract class Singleton<T>
    where T : new()
{
    private static T _instance;
    private static object _lock = new object();
    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                // 上锁,防止重复实例化
                lock (_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new T();
                    }
                }
            }
            return _instance;
        }
    }
}
public class UnitySingleton<T> : MonoBehaviour
    where T : Component
{
    private static T _instance;
    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                // 利用反射创建 Unity 物体
                _instance = FindObjectOfType(typeof(T)) as T;
                if (_instance == null)
                {
                    GameObject obj = new GameObject();
                    // 利用反射创建 Unity 组件
                    _instance = obj.AddComponent(typeof(T)) as T;
                }
            }
            return _instance;
        }
    }

    public virtual void Awake()
    {
        DontDestroyOnLoad(this.gameObject);
        if (_instance == null)
        {
            _instance = this as T;
        }
        else
        {
            Destroy(this.gameObject);
        }
    }
}

架构

UnityECS学习日记 EntityComponentSystemSamples

服务端

C/C++

Lua下载

lua-5.4.0_Win64_bin.zip
lua54.dll
lua54.exe -- 重命名为lua.exe
luac54.exe
wlua54.exe

lua-5.4.0_Win64_dllw6_lib.zip
include/*
liblua54.a -- 静态链接库
lua54.dll -- 动态链接库
  1. lua.h: 基础函数,均为lua_前缀。
  2. lauxlib.h: 辅助库,均为luaL_前缀。
  3. lualib.h: 标准库,大部分为luaopen_前缀,以及luaL_openlibs。

【Lua和C之间通信的主要组件是无处不在的虚拟栈(stack),几乎所有的API调用都是在操作这个栈中的值,Lua与C之间所有的数据交换都是通过这个栈完成的。此外,还可以利用栈保存中间结果。】

g++参数:
-l 链接库
-L 链接库目录
-I include

C++调用Lua

add.lua:

function add(x, y)
    return x + y
end
add.cpp:

int luaadd(int x, int y)
{
    int sum;

    /* the function name */
    lua_getglobal(L, "add");

    /* the first argument */
    lua_pushnumber(L, x);

    /* the second argument */
    lua_pushnumber(L, y);

    /* call the function with 2 arguments, return 1 result */
    lua_call(L, 2, 1);

    /* get the result */
    sum = (int)lua_tointeger(L, -1);
    lua_pop(L, 1);

    return sum;
}
g++ add.cpp -o add -llua54 -L ./lib -I ./include
=> add.exe

Lua调用C++

avg.cpp:

static int average(lua_State *L)
{
    /* get number of arguments */
    int n = lua_gettop(L);
    double sum = 0;
    int i;

    /* loop through each argument */
    for (i = 1; i <= n; i++)
    {
        /* total the arguments */
        sum += lua_tonumber(L, i);
    }

    /* push the average */
    lua_pushnumber(L, sum / n);

    /* push the sum */
    lua_pushnumber(L, sum);

    /* return the number of results */
    return 2;
}
average.lua:

avg, sum = average(10, 20, 30, 40, 50)

print("The average is ", avg)
print("The sum is ", sum)
g++ avg.cpp -o avg -llua54 -L ./lib -I ./include
=> avg.exe

MessagePack

It's like JSON. but fast and small.

MessagePack is an efficient binary serialization format. It lets you exchange data among multiple languages like JSON. But it's faster and smaller. Small integers are encoded into a single byte, and typical short strings require only one extra byte in addition to the strings themselves.

$ git clone https://github.com/msgpack/msgpack-c.git
$ cd msgpack-c
$ git checkout c_master

$ mkdir build
$ cd build
$ cmake -G "MinGW Makefiles" ..
$ mingw32-make
生成 libmsgpackc.dll

ENet

ENet's purpose is to provide a relatively thin, simple and robust network communication layer on top of UDP (User Datagram Protocol).The primary feature it provides is optional reliable, in-order delivery of packets.

ENet omits certain higher level networking features such as authentication, lobbying, server discovery, encryption, or other similar tasks that are particularly application specific so that the library remains flexible, portable, and easily embeddable.

$ git clone https://github.com/lsalzman/enet

$ mkdir build
$ cd build
$ cmake -G "MinGW Makefiles" ..
$ mingw32-make
生成 libenet.a

Kcp

KCP是一个快速可靠协议,能以比 TCP 浪费 10%-20% 的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据包的发送方式,以 callback的方式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何一次系统调用。

整个协议只有 ikcp.h, ikcp.c两个源文件,可以方便的集成到用户自己的协议栈中。也许你实现了一个P2P,或者某个基于 UDP的协议,而缺乏一套完善的ARQ可靠协议实现,那么简单的拷贝这两个文件到现有项目中,稍微编写两行代码,即可使用。

整合

# Windows需要参数-lwinmm -lws2_32

g++ server.cpp -o server -lenet -lmsgpackc -L ./lib -I ./include -lwinmm -lws2_32
g++ client.cpp -o client -lenet -lmsgpackc -L ./lib -I ./include -lwinmm -lws2_32

Lua

字节码

> luac -o test.luac test.lua

a = 18
print("hello world")
> python test.py

1b 4c 75 61 53 00 19 93 0d 0a 1a 0a 04 08 04 08
08 78 56 00 00 00 00 00 00 00 00 00 00 00 28 77
40 01 0a 40 74 65 73 74 2e 6c 75 61 00 00 00 00
00 00 00 00 00 01 02 05 00 00 00 08 40 40 80 06
80 40 00 41 c0 00 00 24 40 00 01 26 00 80 00 04
00 00 00 04 02 61 13 12 00 00 00 00 00 00 00 04
06 70 72 69 6e 74 04 0c 68 65 6c 6c 6f 20 77 6f
72 6c 64 01 00 00 00 01 00 00 00 00 00 05 00 00
00 02 00 00 00 03 00 00 00 03 00 00 00 03 00 00
00 03 00 00 00 00 00 00 00 01 00 00 00 05 5f 45
4e 56

.LuaS...........
.xV...........(w
@..@test.lua....
............@@..
.@.A...$@..&....
.....a..........
.print..hello wo
rld.............
................
.............._E
NV

signature: b'\x1bLua'
version: 53
format: 0
data: b'\x19\x93\r\n\x1a\n'
size_int: 4
size_size_t: 8
size_Instruction: 4
size_lua_Integer: 8
size_lua_Number: 8
Int: 5678
Num: 4077280000000000
sizeupvalues: 1
String值: b'@test.lua'
...
详细分析可见:Lua5.3.5的字节码
https://github.com/lua/lua/blob/063d4e4543088e7a21965bda8ee5a0f952a9029e/ldump.c
https://cloud.tencent.com/developer/article/1648925

面向对象

local _class = {}

function class(super)
    local class_type = {}
    class_type.ctor = false
    class_type.super = super
    class_type.new = function(...)
        local obj = {}
        do
            local create
            create = function(c, ...)
                if c.super then create(c.super, ...) end
                if c.ctor then c.ctor(obj, ...) end
            end

            create(class_type, ...)
        end
        setmetatable(obj, {__index = _class[class_type]})
        return obj
    end
    local vtbl = {}
    _class[class_type] = vtbl

    setmetatable(class_type, {__newindex = function(t, k, v) vtbl[k] = v end})

    if super then
        setmetatable(vtbl, {
            __index = function(t, k)
                local ret = _class[super][k]
                vtbl[k] = ret
                return ret
            end
        })
    end

    return class_type
end
base_type = class() -- 定义一个基类 base_type

function base_type:ctor(x) -- 定义 base_type 的构造函数
    print("base_type ctor")
    self.x = x
end

function base_type:print_x() -- 定义一个成员函数 base_type:print_x
    print(self.x)
end

function base_type:hello() -- 定义另一个成员函数 base_type:hello
    print("hello base_type")
end
test = class(base_type) -- 定义一个类 test 继承于 base_type

function test:ctor() -- 定义 test 的构造函数
    print("test ctor")
end

function test:hello() -- 重载 base_type:hello 为 test:hello
    print("hello test")
end
a = test.new(1) -- 输出两行,base_type ctor 和 test ctor 。这个对象被正确的构造了。
a:print_x() -- 输出 1 ,这个是基类 base_type 中的成员函数。
a:hello() -- 输出 hello test ,这个函数被重载了。

垃圾回收

function A()
    collectgarbage("collect") -- 进行垃圾回收,减少干扰
    PrintCount()
    local a = {}
    for i = 1, 5000 do
        table.insert(a, {})
    end
    PrintCount()
    collectgarbage("collect")
    PrintCount()
end

A()
PrintCount()
collectgarbage("collect")
PrintCount()
output:
21
423 -- 分配内存为423-21=402kb。
423 -- 局部变量,生命周期,没有办法进行回收。
423 -- GC每隔一段时间才会自动回收。
21 -- 手动回收。
function A()
    collectgarbage("collect") -- 进行垃圾回收,减少干扰
    PrintCount()
    a = {} -- 修改1
    for i = 1, 5000 do
        table.insert(a, {})
    end
    PrintCount()
    collectgarbage("collect")
    PrintCount()
end

A()
PrintCount()
collectgarbage("collect")
PrintCount()

-- 修改2
a = nil
collectgarbage("collect")
PrintCount()
output:
21
423
423
423
423 -- 全局变量,生命周期,没有办法进行回收。
21 -- 将a置空,手动回收。

高性能Lua

  1. 前言:不要优化,还是不要优化。优化前后要做性能测试。
  2. 基本事实:使用局部变量,避免动态编译。
  3. 关于表:数组or哈希表,开放定址法。哈希的大小必须为2的幂。 老版Lua扩容会预分配空位,新版Lua扩容不会预分配空位,避免浪费内存空间,例子:点Point{x, y}。可以通过构造器避免重新哈希。 {true, true, true} => 3个空位 {x = 1, y = 2, z = 3} => 4个空位(浪费内存,浪费时间) 删除表元素,表不会缩小,更好的做法是删除表本身。
  4. 关于字符串:Lua的字符串只有一份拷贝,变量只是引用类型。
  5. 3R原则:减少reduce,重用reuse,回收recycle。
  6. Tips:(1)LuaJIT;(2)Lua+C/C++。

基础

function obj:fun()
  print(self.x)
end

等价于

function obj.fun(self)
  print(self.x)
end
-- pairs: 迭代表。
t = { a = "apple", b = "baby", c = "cool" }
for k, v in pairs(t) do print(k, v) end
b   baby
a   apple
c   cool

-- ipairs: 迭代数组。遇到nil退出。
t = {"one", "two", "three"}
for i, v in ipairs(t) do print(i, v) end
1   one
2   two
3   three
-- 获取长度
t1 = {1, 2, 3}
t2 = {1, nil, 2}
t3 = {1, 2, nil, 3}
print(#t1) -- 3 ok
print(#t2) -- ? undefined
print(#t3) -- ? undefined

-- 非空判断
function isTableEmpty(t)
    return t == nil or next(t) == nil
end

-- string转table
s = "{1, 2, 3}"
t = load("return " .. s)()
print(t) -- table: 0x...
-- pcall
> =pcall(function(i) print(i) error('error..') end, 33)
33
false        stdin:1: error..

-- xpcall
> =xpcall(function(i) print(i) error('error..') end, function() print(debug.traceback()) end, 33)
33
stack traceback:
stdin:1: in function <stdin:1>
[C]: in function 'error'
stdin:1: in function <stdin:1>
[C]: in function 'xpcall'
stdin:1: in main chunk
[C]: in ?
false        nil

游戏库

Lume: A collection of functions for Lua, geared towards game development.

热更新

-- 暴力热更
function reload_module(module_name)
  package.loaded[module_name] = nil
  require(module_name)
end

-- 优化热更
function reload_module(module_name)
  local old_module = _G[module_name]

  package.loaded[module_name] = nil
  require(module_name)

  local new_module = _G[module_name]
  for k, v in pairs(new_module) do
    old_module[k] = v
  end

  package.loaded[module_name] = old_module
end
-- Lume实现
lume.hotswap("lume") -- Reloads the lume module
assert(lume.hotswap("inexistant_module")) -- Raises an error

序列化

-- 官方实现 http://lua-users.org/wiki/TableUtils
function table.val_to_str ( v )
  if "string" == type( v ) then
    v = string.gsub( v, "\n", "\\n" )
    if string.match( string.gsub(v,"[^'\"]",""), '^"+$' ) then
      return "'" .. v .. "'"
    end
    return '"' .. string.gsub(v,'"', '\\"' ) .. '"'
  else
    return "table" == type( v ) and table.tostring( v ) or
      tostring( v )
  end
end

function table.key_to_str ( k )
  if "string" == type( k ) and string.match( k, "^[_%a][_%a%d]*$" ) then
    return k
  else
    return "[" .. table.val_to_str( k ) .. "]"
  end
end

function table.tostring( tbl )
  local result, done = {}, {}
  for k, v in ipairs( tbl ) do
    table.insert( result, table.val_to_str( v ) )
    done[ k ] = true
  end
  for k, v in pairs( tbl ) do
    if not done[ k ] then
      table.insert( result,
        table.key_to_str( k ) .. "=" .. table.val_to_str( v ) )
    end
  end
  return "{" .. table.concat( result, "," ) .. "}"
end

t = {['foo']='bar',11,22,33,{'a','b'}}
print( table.tostring( t ) )
-- {11,22,33,{"a","b"},foo="bar"}
-- Lume实现
lume.serialize({a = "test", b = {1, 2, 3}, false})
-- Returns "{[1]=false,["a"]="test",["b"]={[1]=1,[2]=2,[3]=3,},}"

其他算法

lume.random([a [, b]])
lume.randomchoice(t)
lume.weightedchoice(t)
lume.shuffle(t)
lume.sort(t [, comp])
...

性能优化

火焰图

火焰图 火焰图是基于 perf 结果产生的 SVG 图片,用来展示 CPU 的调用栈。

y 轴表示调用栈,每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。

x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长。注意,x 轴不代表时间,而是所有的调用栈合并后,按字母顺序排列的。

火焰图就是看顶层的哪个函数占据的宽度最大。只要有“平顶”(plateaus),就表示该函数可能存在性能问题。

颜色没有特殊含义,因为火焰图表示的是 CPU 的繁忙程度,所以一般选择暖色调。

# 安装perf
yum install perf

# 下载火焰图
git clone https://github.com/brendangregg/FlameGraph.git

# 捕获堆栈
perf record -F 99 -p 181 -g -- sleep 60

-F 99:每秒99次。
-p 181:进程号。
-g:记录调用栈。
sleep 60:持续60秒。

# 解析堆栈
perf script > out.stacks

# 折叠堆栈
./stackcollapse-perf.pl out.stacks > out.folded

# 生成火焰图
./flamegraph.pl out.folded > perf.svg

# 使用管道简化命令
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > perf.svg

# 红蓝差分火焰图(红色上升,蓝色下降。)
./difffolded.pl out.folded1 out.folded2 | ./flamegraph.pl > diff.svg

不足之处:http://www.brendangregg.com/blog/2014-11-09/differential-flame-graphs.html
如果一个代码执行路径完全消失了,那么在火焰图中就找不到地方来标注蓝色。
你只能看到当前的 CPU 使用情况,而不知道为什么会变成这样。
一个办法是,将对比顺序颠倒,画一个相反的差分火焰图。
./difffolded.pl out.folded1 out.folded2 | ./flamegraph.pl > diff2.svg
./difffolded.pl out.folded2 out.folded1 | ./flamegraph.pl --negate > diff1.svg
diff1.svg:宽度是以修改前profile文件为基准,颜色表明将要发生的情况。
diff2.svg:宽度是以修改后profile文件为基准,颜色表明已经发生的情况。
int i() { for(int i = 0; i < 20000; i++) {}; }

int e() { for(int i = 0; i < 20000; i++) {}; }

int g() { for(int i = 0; i < 50000; i++) {}; }

int h() { i(); }

int f() { g(); }

int d() { e(); f(); }

int c() { d(); }

int b() { c(); }

int a() { b(); h(); }

> g++ main.cpp -o main
> ./main

火焰图测试

Postman

调试工具,不要我多说了8。

Jmeter

测压工具,不要我多说了8。

VSCode

"editor.fontSize": 16,
"editor.fontFamily": "Consolas",
"window.zoomLevel": 0,
"workbench.colorTheme": "Default Light+",
"workbench.iconTheme": "vscode-icons",
"workbench.editor.enablePreview": false,
"files.eol": "\n",
...
"code-runner.runInTerminal": true,
"code-runner.fileDirectoryAsCwd": true,
"python.linting.pylintUseMinimalCheckers": false,
"python.linting.pylintArgs": [
    "--disable=missing-docstring", "--disable=invalid-name"
]
  1. Untiy默认编辑器:Edit –> Preferences -> External Tools -> External Script Editor => Visual Studio Code
  2. Untiy默认模板:C:\Program Files\Unity\Editor\Data\Resources\ScriptTemplates => 81-C# Script-NewBehaviourScript.cs.txt