最近沉迷lua脚本热更,想说这个可以提高多少菜鸡的调试效率,找了网上好多文章,但是都不行,尝试了很久,并且自己测试和学习,写了一遍,勉强能热更了。下面记录一下热更Lua的过程。

一、用来卸载表格的加载

最简单粗暴的热更新就是将package.loaded[modelname]的值置为nil,强制重新加载:

function reload_module_obsolete(module_name)    package.loaded[module_name] = nil    require(module_name)end

这样做虽然能完成热更,但问题是已经引用了该模块的地方不会得到更新, 因此我们需要将引用该模块的地方的值也做对应的更新。

function ReloadUtil.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] = vend

    package.loaded[module_name] = old_moduleend

二、我认为逻辑最清晰的,但是可能是我不会用吧!

源链接:链接

--region 利用_ENV环境,在加载的时候把数据加载到_ENV下,然后再通过对比的方式修改_G底下的值,从而实现热更新,函数 -- 失败

function ReloadUtil.hotfix(chunk, check_name)    check_name = check_name or 'hotfix'local env = {}    setmetatable(env, { __index = _G })local f, err = load(chunk, check_name,  't', env)    assert(f,err)local ok, err = pcall(f)    assert(ok,err)

local protection = {        setmetatable = true,        pairs = true,        ipairs = true,        next = true,        require = true,        _ENV = true,    }--防止重复的table替换,造成死循环local visited_sig = {}

function ReloadUtil.update_table(newTable, oldTable, name, deep)--对某些关键函数不进行比对if protection[newTable] or protection[oldTable] then return end--如果原值与当前值内存一致,值一样不进行对比if newTable == oldTable then return endlocal signature = tostring(oldTable)..tostring(newTable)if visited_sig[signature] then return end        visited_sig[signature] = true--遍历对比值,如进行遍历env类似的步骤for name, newValue in pairs(newTable) dolocal old_value = oldTable[name]if type(newValue) == type(old_value) thenif type(newValue) == 'function' then                    ReloadUtil.update_func(newValue, old_value, name, deep..'  '..name..'  ')                    oldTable[name] = newValueelseif type(newValue) == 'table' then                    ReloadUtil.update_table(newValue, old_value, name, deep..'  '..name..'  ')endelse                oldTable[name] = newValueendend--遍历table的元表,进行对比local old_meta = debug.getmetatable(oldTable)local new_meta = debug.getmetatable(newTable)if type(old_meta) == 'table' and type(new_meta) == 'table' then            ReloadUtil.update_table(new_meta, old_meta, name..'s Meta', deep..'  '..name..'s Meta'..'  ' )endend

function ReloadUtil.update_func(newFunc, oldFunc, name, deep)--取得原值所有的upvalue,保存起来local old_upvalue_map = {}for i = 1, math.huge dolocal name, value = debug.getupvalue(oldFunc, i)if not name then break end            old_upvalue_map[name] = valueend--遍历所有新的upvalue,根据名字和原值对比,如果原值不存在则进行跳过,如果为其它值则进行遍历env类似的步骤for i = 1, math.huge dolocal name, value = debug.getupvalue(newFunc, i)if not name then break endlocal old_value = old_upvalue_map[name]if old_value thenif type(old_value) ~= type(value) then                    debug.setupvalue(newFunc, i, old_value)elseif type(old_value) == 'function' then                    ReloadUtil.update_func(value, old_value, name, deep..'  '..name..'  ')elseif type(old_value) == 'table' then                    ReloadUtil.update_table(value, old_value, name, deep..'  '..name..'  ')                    debug.setupvalue(newFunc, i, old_value)else                    debug.setupvalue(newFunc, i, old_value)endendendend

--原理--利用_ENV环境,在加载的时候把数据加载到_ENV下,然后再通过对比的方式修改_G底下的值,从而实现热更新,函数for name,value in pairs(env) dolocal g_value = _G[name]if type(g_value) ~= type(value) then            _G[name] = valueelseif type(value) == 'function' then            ReloadUtil.update_func(value, g_value, name, 'G'..'  ')            _G[name] = valueelseif type(value) == 'table' then            ReloadUtil.update_table(value, g_value, name, 'G'..'  ')endendreturn 0end

function ReloadUtil.hotfix_file(debugName)local newCodelocal fp = io.open(debugName)if fp then        newCode = fp:read('*all')        io.close(fp)endif not newCode thenreturn -1endreturn ReloadUtil.hotfix(newCode, debugName)end

--endregion

图文无关

三、有点复杂,递归了debug.getregistry(),然后去替换旧的值

源链接:asqbtcupid.github.io/lu 有介绍原理,讲得还蛮细的,学习了蛮多,但是这个递归真的是复杂,我注释掉了一些递归,能满足基本的需求。

local ReloadUtil = {}

local tableInsert = table.insertlocal tableRemove = table.removelocal tableConcat = table.concatlocal ioPopen = io.popenlocal ioInput = io.inputlocal ioRead = io.readlocal stringMatch = string.matchlocal stringFind = string.findlocal stringSub = string.sublocal stringGsub = string.gsublocal packageLoaded = package.loadedlocal type = typelocal getfenv = getfenvlocal setfenv = setfenvlocal loadstring = loadstringlocal mathHuge = math.hugelocal debugGetupvalue = debug.getupvaluelocal debugSetupvalue = debug.setupvaluelocal debugGetmetatable = debug.getmetatablelocal debugSetfenv = debug.setfenv

function ReloadUtil.FailNotify(...)    printAError(...)end

function ReloadUtil.DebugNofity(...)    print(...)end

function ReloadUtil.ErrorHandle(e)    ReloadUtil.FailNotify("HotUpdate Error\n"..tostring(e))    ReloadUtil.ErrorHappen = trueend

function ReloadUtil.InitProtection()    ReloadUtil.Protection = {}local Protection = ReloadUtil.Protection    Protection[setmetatable] = true    Protection[pairs] = true    Protection[ipairs] = true    Protection[next] = true    Protection[require] = true    Protection[ReloadUtil] = true    Protection[ReloadUtil.Meta] = true    Protection[math] = true    Protection[string] = true    Protection[table] = trueend

local function Normalize(path)    path = path:gsub("/","\\")

local pathLen = #pathif path:sub(pathLen, pathLen) == "\\" then        path = path:sub(1, pathLen - 1)end

local parts = { }for w in path:gmatch("[^\\]+") doif     w == ".." and #parts ~=0 then tableRemove(parts)elseif w ~= "."  then tableInsert(parts, w)endendreturn tableConcat(parts, "\\")end

-- 根据给的路径,找到路径下所有文件,HU.FileMap[FileName] = {SysPath = line, LuaPath = luapath}function ReloadUtil.InitFileMap(RootPath)local systemPathList = {}local HotUpdateDic = ReloadUtil.HotUpdateDiclocal OldCode = ReloadUtil.OldCode    RootPath = Normalize(RootPath)--获取一个File对象其下的所有文件和目录的绝对路径: 的所有文件(/S/B),不包括文件夹(/A:A),ioPopen返回文件句柄file handle--todo 这里有的问题,多次启动后会报bad file decorator ,检查是否是打开文件没有关闭导致local file = ioPopen("dir /S/B /A:A \""..RootPath.."\"")

for SysPath in ioInput(file):lines() dolocal FileName = stringMatch(SysPath,".*\\(.*)%.lua")--todo meta 优化一下regexlocal metaFile = stringMatch(SysPath,".*\\(.*)%.lua.meta")if FileName ~= nil and metaFile == nil then--todo !!! luaPath在保存的时候是按文件夹路径保存的比如:game.modules.XXX.lua,所以可能要自己搞对这个路径

local startIndex = stringFind(SysPath,"game")local luaPath = stringSub(SysPath, startIndex, #SysPath -4)            luaPath = stringGsub(luaPath, "\\", ".")            HotUpdateDic[luaPath] = SysPath            tableInsert(systemPathList,SysPath)-- 初始化旧代码            ioInput(SysPath)            OldCode[SysPath] = ioRead("*all")            ioInput():close()endend

    file:close()

return systemPathListend

function ReloadUtil.InitFakeTable()local meta = {}    ReloadUtil.Meta = metalocal function FakeT() return setmetatable({}, meta) endlocal function EmptyFunc() endlocal function pairs() return EmptyFunc endlocal function setmetatable(t, metaT)        ReloadUtil.MetaMap[t] = metaTreturn tendlocal function getmetatable(t, metaT)return setmetatable({}, t)end

local function require(LuaPath)if not ReloadUtil.RequireMap[LuaPath] thenlocal FakeTable = FakeT()            ReloadUtil.RequireMap[LuaPath] = FakeTableendreturn ReloadUtil.RequireMap[LuaPath]end

function meta.__index(table, key)if key == "setmetatable" thenreturn setmetatableelseif key == "pairs" or key == "ipairs" thenreturn pairselseif key == "next" thenreturn EmptyFuncelseif key == "require" thenreturn requireelselocal FakeTable = FakeT()            rawset(table, key, FakeTable)return FakeTableendendfunction meta.__newindex(table, key, value) rawset(table, key, value) endfunction meta.__call() return FakeT(), FakeT(), FakeT() endfunction meta.__add() return meta.__call() endfunction meta.__sub() return meta.__call() endfunction meta.__mul() return meta.__call() endfunction meta.__div() return meta.__call() endfunction meta.__mod() return meta.__call() endfunction meta.__pow() return meta.__call() endfunction meta.__unm() return meta.__call() endfunction meta.__concat() return meta.__call() endfunction meta.__eq() return meta.__call() endfunction meta.__lt() return meta.__call() endfunction meta.__le() return meta.__call() endfunction meta.__len() return meta.__call() endreturn FakeTend

function ReloadUtil.IsNewCode(SysPath)    ioInput(SysPath)local newCode = ioRead("*all")

local oldCode = ReloadUtil.OldCode[SysPath]if oldCode == newCode then        ioInput():close()return falseend

    ReloadUtil.DebugNofity(SysPath)return true, newCodeend

function ReloadUtil.GetNewObject(newCode, LuaPath, SysPath)--loadstring 一段lua代码以后,会经过语法解析返回一个函数,执行返回的函数时,字符串中的代码就被执行了。local NewFunction = loadstring(newCode)if not NewFunction then        ReloadUtil.FailNotify(SysPath.." has syntax error.")        collectgarbage("collect")else-- 把加载的字符串放置在空环境了,防止报错        setfenv(NewFunction, ReloadUtil.FakeENV)local NewObject        ReloadUtil.ErrorHappen = false--类似其它语言里的 try-catch, xpcall 类似 pcall xpcall接受两个参数:调用函数、错误处理函数-- todo 父类没拿到        xpcall(function () NewObject = NewFunction() end, ReloadUtil.ErrorHandle)

if not ReloadUtil.ErrorHappen then            ReloadUtil.OldCode[SysPath] = newCodereturn NewObjectelse            collectgarbage("collect")endendend

function ReloadUtil.ResetENV(object, name, From, Deepth)local visited = {}local function f(object, name)if not object or visited[object] then return end        visited[object] = trueif type(object) == "function" then            ReloadUtil.DebugNofity(Deepth.."HU.ResetENV", name, "  from:"..From)            xpcall(function () setfenv(object, ReloadUtil.ENV) end, ReloadUtil.FailNotify)elseif type(object) == "table" then            ReloadUtil.DebugNofity(Deepth.."HU.ResetENV", name, "  from:"..From)for k, v in pairs(object) do                f(k, tostring(k).."__key", " HU.ResetENV ", Deepth.."    " )                f(v, tostring(k), " HU.ResetENV ", Deepth.."    ")endendend    f(object, name)end

-- 遍历_G这张全局表,替换HU.ChangedFuncList 有改动列表 的函数function ReloadUtil.Travel_G()local visited = {}local ChangedFuncList = ReloadUtil.ChangedFuncList    visited[ReloadUtil] = true

local function f(table)if (type(table) ~= "function" and type(table) ~= "table") or visited[table] or ReloadUtil.Protection[table] then return end

        visited[table] = true

if type(table) == "function" thenfor i = 1, mathHuge dolocal name, value = debugGetupvalue(table, i)if not name then break end

if type(value) == "function" thenfor _, funcs in ipairs(ChangedFuncList) doif value == funcs.OldObject then                            debugSetupvalue(table, i, funcs.NewObject)endendend-- todo--f(value)endelseif type(table) == "table" then-- 不要漏掉元表和upvalue的表,元表的获取用debug.getmetatable,-- todo 这样对于有metatable这个key的元表,也能正确获取。--f(debugGetmetatable(table))

local changeIndexList = {}for key, value in pairs(table) do-- todo 还有注意table的key也可以是函数。--f(key)                f(value)

if type(value) == "function" thenfor _, funcs in ipairs(ChangedFuncList) doif value == funcs.OldObject then                            table[key] = funcs.NewObjectendendend

-- 找出改动的indexif type(key) == "function" thenfor index, funcs in ipairs(ChangedFuncList) doif key == funcs.OldObject then                            changeIndexList[#changeIndexList + 1] = indexendendendend

-- 修改改动的值for _, index in ipairs(changeIndexList) dolocal funcs = ChangedFuncList[index]                table[funcs.NewObject] = table[funcs.OldObject]                table[funcs.OldObject] = nilendendend

--遍历_G这张全局表,_G在registryTable里--f(_G)--如果有宿主语言,那么还要遍历一下注册表,用debug.getregistry()获得。local registryTable = debug.getregistry()    f(registryTable)end

function ReloadUtil.UpdateTable(OldTable, NewTable, Name, From, Deepth)if ReloadUtil.Protection[OldTable] or ReloadUtil.Protection[NewTable] then return end

if OldTable == NewTable then return end

local signature = tostring(OldTable)..tostring(NewTable)

if ReloadUtil.VisitedSig[signature] then return end

    ReloadUtil.VisitedSig[signature] = true    ReloadUtil.DebugNofity(Deepth.."HU.UpdateTable "..Name.."  from:"..From)

for ElementName, newValue in pairs(NewTable) dolocal OldElement = OldTable[ElementName]if type(newValue) == type(OldElement) thenif type(newValue) == "function" then                ReloadUtil.UpdateOneFunction(OldElement, newValue, ElementName, OldTable, "HU.UpdateTable", Deepth.."    ")elseif type(newValue) == "table" then                ReloadUtil.UpdateTable(OldElement, newValue, ElementName, "HU.UpdateTable", Deepth.."    ")endelseif OldElement == nil and type(newValue) == "function" then-- 新增的函数,添加到旧环境里if pcall(setfenv, newValue, ReloadUtil.ENV) then                OldTable[ElementName] = newValueendendend

-- todo 更新metatable--local OldMeta = debug.getmetatable(OldTable)--local NewMeta = ReloadUtil.MetaMap[NewTable]--if type(OldMeta) == "table" and type(NewMeta) == "table" then--    ReloadUtil.UpdateTable(OldMeta, NewMeta, Name.."'s Meta", "HU.UpdateTable", Deepth.."    ")--endend

-- Upvalue 是指那些函数外被引用到的local变量function ReloadUtil.UpdateUpvalue(OldFunction, NewFunction, Name, From, Deepth)    ReloadUtil.DebugNofity(Deepth.."HU.UpdateUpvalue", Name, "  from:"..From)local OldUpvalueMap = {}local OldExistName = {}-- 记录旧的upvalue表for i = 1, mathHuge dolocal name, value = debugGetupvalue(OldFunction, i)if not name then break end        OldUpvalueMap[name] = value        OldExistName[name] = trueend

-- 新的upvalue表进行替换for i = 1, mathHuge dolocal name, value = debugGetupvalue(NewFunction, i)if not name then break endif OldExistName[name] thenlocal OldValue = OldUpvalueMap[name]if type(OldValue) ~= type(value) then-- 新的upvalue类型不一致时,用旧的upvalue                debugSetupvalue(NewFunction, i, OldValue)elseif type(OldValue) == "function" then-- 替换单个函数                ReloadUtil.UpdateOneFunction(OldValue, value, name, nil, "HU.UpdateUpvalue", Deepth.."    ")elseif type(OldValue) == "table" then-- 对table里面的函数继续递归替换                ReloadUtil.UpdateTable(OldValue, value, name, "HU.UpdateUpvalue", Deepth.."    ")                debugSetupvalue(NewFunction, i, OldValue)else-- 其他类型数据有改变,也要用旧的                debugSetupvalue(NewFunction, i, OldValue)endelse-- 对新添加的upvalue设置正确的环境表            ReloadUtil.ResetENV(value, name, "HU.UpdateUpvalue", Deepth.."    ")endendend

function ReloadUtil.UpdateOneFunction(OldObject, NewObject, FuncName, OldTable, From, Deepth)if ReloadUtil.Protection[OldObject] or ReloadUtil.Protection[NewObject] then return end

if OldObject == NewObject then return end

local signature = tostring(OldObject)..tostring(NewObject)

if ReloadUtil.VisitedSig[signature] then return end    ReloadUtil.DebugNofity(Deepth.."HU.UpdateOneFunction "..FuncName.."  from:"..From)    ReloadUtil.VisitedSig[signature] = true--最后注意把热更新的函数的环境表再改回旧函数的环境表即可,方法是setfenv(newfunction, getfenv(oldfunction))。if pcall(debugSetfenv, NewObject, getfenv(OldObject)) then        ReloadUtil.UpdateUpvalue(OldObject, NewObject, FuncName, "HU.UpdateOneFunction", Deepth.."    ")        ReloadUtil.ChangedFuncList[#ReloadUtil.ChangedFuncList + 1] = {OldObject = OldObject,NewObject = NewObject,FuncName = FuncName,OldTable = OldTable}endend

function ReloadUtil.ReplaceOld(OldObject, NewObject, LuaPath, From)if type(OldObject) == type(NewObject) thenif type(NewObject) == "table" then            ReloadUtil.UpdateTable(OldObject, NewObject, LuaPath, From, "")elseif type(NewObject) == "function" then            ReloadUtil.UpdateOneFunction(OldObject, NewObject, LuaPath, nil, From, "")endendend

function ReloadUtil.HotUpdateCode(LuaPath, SysPath)local OldObject = packageLoaded[LuaPath]if not OldObject then-- 没加载的就不热更??returnend

local isNew,newCode = ReloadUtil.IsNewCode(SysPath)if not isNew thenreturnend

local newObject = ReloadUtil.GetNewObject(newCode,SysPath,LuaPath)-- 更新旧代码    ReloadUtil.ReplaceOld(OldObject, newObject, LuaPath, "Main")

--原理--利用_ENV环境,在加载的时候把数据加载到_ENV下,然后再通过对比的方式修改_G底下的值,从而实现热更新,函数--setmetatable(ReloadUtil.FakeENV, nil)--todo ??--ReloadUtil.UpdateTable(ReloadUtil.ENV, ReloadUtil.FakeENV, " ENV ", "Main", "")

-- 替换完,上一次的代码就是旧代码    ReloadUtil.OldCode[SysPath] = newCodeend

-- 外部调用(先Init需要的路径,然后Update是热更时候调用的)

---@param RootPath 需要被更新的文件夹路径---@param UpdateListFile 需要被更新的文件列表,不传为整个文件夹,会卡function ReloadUtil.Init(RootPath, ENV)    ReloadUtil.HotUpdateDic = {}    ReloadUtil.FileMap = {}    ReloadUtil.OldCode = {}    ReloadUtil.ChangedFuncList = {}    ReloadUtil.VisitedSig = {}    ReloadUtil.FakeENV = ReloadUtil.InitFakeTable()()--当我们加载lua模块的时候,这时候这个模块信息并不像初始化全局代码一样,就算提前设置了package.loaded["AA"] = nil,--也不会出现在env中同时也不会调用_G的__newindex函数,也就是说env["AA"]为空,故这种写法无法进行热更新,所以通常模块的写法改成如下--定义模块AAlocal AA = {}--相当于package.seeall    setmetatable(AA, {__index = _G})--环境隔离local _ENV = AA    ReloadUtil.NewENV = _ENV    ReloadUtil.ENV = ENV or _G    ReloadUtil.InitProtection()    ReloadUtil.ALL = falsereturn ReloadUtil.InitFileMap(RootPath)end

function ReloadUtil.Update()    ReloadUtil.VisitedSig = {}    ReloadUtil.ChangedFuncList = {}for LuaPath, SysPath in pairs(ReloadUtil.HotUpdateDic) do        ReloadUtil.HotUpdateCode(LuaPath, SysPath)end

if #ReloadUtil.ChangedFuncList > 0 then        ReloadUtil.Travel_G()end    collectgarbage("collect")end

我跟老大炫耀的时候,老大说,那你懂其中的原理吗,一下问懵我了,老大说,你要学习到其中的原理才能进步啊,不然就只是个会用工具的人。好有道理,搞得我羞愧难当,赶紧好好学习其中原理。

Upvalue

Upvalue 是指那些函数外被引用到的local变量,比如:

local a = 1function foo()    print(a)end

那么a就是这个foo的upvalue。

getupvalue (f, up)

此函数返回函数 f 的第 up 个上值的名字和值。如果该函数没有那个上值,返回 nil 。
以 '(' (开括号)打头的变量名表示没有名字的变量 (去除了调试信息的代码块)。

setupvalue (f, up, value):

这个函数将 value 设为函数 f 的第 up 个上值。如果函数没有那个上值,返回 nil 否则,返回该上值的名字。

_G和debug.getregistry这2张表

学习的时候,我一直以为只有把_G这张全局表的旧值替换掉就好了,然后真正实施的时候,还是会有种种问题,实在是很糟糕,看这段代码的时候一直不是很理解,看了debug.getregistry的定义:debug.getregistry():返回注册表表,这是一个预定义出来的表, 可以用来保存任何 C 代码想保存的 Lua 值。
还是半桶水,但是我有注意到_G这种表其实在debug.getregistry返回的这张注册表里有,所以最后就递归这张表其替换里面的旧值就好了。

getfenv(object):返回对象的环境变量。
setfenv(function,_ENV):设置一段代码的运行环境

io.popen("dir /S/B /A:A \""..RootPath.."\""):获取一个File对象其下的所有文件和目录的绝对路径: 的所有文件(/S/B),不包括文件夹(/A:A),io.popen返回文件句柄file handle

对了,InitFileMap这个函数有个要注意的,luaPath在保存的时候是按文件夹路径保存的比如:game.modules.XXX.lua,所以可能要自己搞对这个路径;

最后总结一下流程:

1. 初始化需要热更的文件路径,用一张哈希表存下来;
2. 然后遍历这些路径,读取所有的代码,判断是否是新代码,新的话记录到ChangedFuncList列表里面;3. 新代码的话,就把加载的字符串放置在空环境了,防止报错setfenv(NewFunction, ReloadUtil.FakeENV);4. 代替旧代码,拿着ChangedFuncList列表到注册表(debug.getregistry())里面去找旧值,递归注册表的旧值替换成新的值。

来源知乎专栏:Unity手游开发叨叨

和lua的效率对比测试_Unity游戏开发Lua更新运行时代码!相关推荐

  1. [Unity3D]Unity3D游戏开发Lua随着游戏的债券(于)

    ---------------------------------------------------------------------------------------------------- ...

  2. 使用eclipse开发web项目运行时出现中文乱码问题

    使用eclipse开发web项目运行时出现中文乱码问题 检查Tomcat sever.xml中的编码设置是否为 utf-8 ,不是的话改一下: <?xml version="1.0&q ...

  3. [Unity3D]Unity3D游戏开发Lua随着游戏的债券(在)

    ---------------------------------------------------------------------------------------------------- ...

  4. unity 通过resouce加载图片_Unity游戏开发笔记-资源管理之资源加载

    资源加载是游戏中非常重要也非常繁琐的的一部分,不合理的资源管理,必定回给游戏的内存带来非常大的压力,尤其是一些重度游戏,不但资源特别多,引用关系特别复杂.维护一个不会内存泄漏而且加载效率高的资源加载框 ...

  5. unity开发入门_Unity游戏开发终极入门指南

    unity开发入门 Unity is a great tool for prototyping everything from games, to interactive visualisations ...

  6. unity 继承了 获取_Unity游戏开发——设计模式概述

    0.前言 这一系列的文章其实应该算作几本书和一些资料总结的笔记,是有关设计模式与游戏开发之间的应用.笔者将阅读学习过程中的思考和总结记录下来,也希望能提供给同样在这方面有疑问的朋友一些帮助. 1.设计 ...

  7. arcore之路-unity开发从入门到实践_Unity游戏开发——单例模式的最佳实践

    0.前言 StarryFun:Unity游戏开发--关于单例模式的理解​zhuanlan.zhihu.com 之前一篇文章讲了单例模式的简单理解,自知其中有很多不严谨的地方,由于本萌新也是在学习阶段, ...

  8. unity 发光字体_Unity 游戏开发技巧集锦之创建自发光材质

    Unity 游戏开发技巧集锦之创建自发光材质 Unity游戏开发技巧集锦教程大学霸内部资料 创建自发光材质 自发光材质(self-illuminated material)是指自己会发光的材质.生活中 ...

  9. 游戏设计与开发_Unity游戏开发——设计模式概述

    0.前言 这一系列的文章其实应该算作几本书和一些资料总结的笔记,是有关设计模式与游戏开发之间的应用.笔者将阅读学习过程中的思考和总结记录下来,也希望能提供给同样在这方面有疑问的朋友一些帮助. 1.设计 ...

最新文章

  1. List集合的去除重复性练习
  2. 面向对象的五大原则与IOC
  3. 点云数据向图像数据转换(附源码)
  4. CF232C Doe Graphs
  5. poj1018 Communication System (有道翻译完全拯救不了)
  6. 当凡尔赛文学遇上产品经理
  7. 杭电oj 1002 c++版本
  8. php mysql子查询,mysql子查询命令
  9. 这件装备让你排位上分有如神助,vivo X20王者荣耀限量版图评
  10. ASP.NET中调用Excel的问题
  11. ssh之雇员管理系统(7)-spring可以启用注解的方式来配置属性+解决懒加载问题...
  12. VMware ESXi 安装 IPMITOOL 工具
  13. 海思 Hi3559A Sample_vdec视频解码及编译
  14. R TALK | 旷视研究院范浩强周舒畅: AI计算机摄影的原理、应用与硬件设计
  15. 不翻墙 轻松打开github
  16. 关于更佳学术搜索及Android SDK更新问题
  17. 线程死锁、锁死、饥饿、活锁讲解
  18. 发表论文介绍(计算机类)
  19. mysql mybatis分表查询_mybatis 自动分表
  20. 和 Cee 聊聊如何拿 Google Offer

热门文章

  1. css p 文本不换行,超出文字显示省略号
  2. superset 时区问题Timestamp subtraction must have the same timezones or no timezones
  3. outerDocument访问外部属性方法
  4. git bash上传大文件到github
  5. 用numpy,matplotlib库画笛卡尔爱心曲线
  6. 计算机成绩表及格率怎么算,卫生资格人机对话如何考试如何评分?成绩如何核算?...
  7. 【C++深度剖析教程39】实现C++数组类模板
  8. 【C++深度剖析教程19】前置操作符与后置操作符
  9. POJ 1276 完全背包
  10. 07.用户故事与敏捷方法——优秀用户故事准则笔记