這次就不急着往下講解遊戲功能了,先來說下lua的功能,因爲Unity熱更新的問題,導致很多手遊都會使用c#加lua來開發,因此有很多新手,或者用lua開發了一兩年的程序員,還不是很瞭解lua,在使用中會出現很多問題。這裏推薦一個比較好的文章,有英文基礎的可以看下。
PS:爲什麼要說這個,因爲最近在自己上線半年多的手遊項目中發現一個bug,之前有看到過,但沒時間仔細看,就掃了一眼。
先來看下我們項目中錯誤的代碼:
do
local base = { __index = __default_values, __newindex = function() error( "Attempt to modify read-only table" ) end }
for k, v in pairs( achievement ) do
setmetatable( v, base )
end
end
寫的人想實現的功能其實是禁止其他人修改策劃配置的表格,但這根本就是錯的。假設策劃配的表示A,這裏的功能就是將A的元表設置爲base,猜一下會不會觸發__nexindex元方法,會在什麼情況下觸發。
結論是會觸發,但是是在A中沒有該key的時候,比如A表中只有一個鍵值對,key爲title,value位"XX活動"。調用A["title"] = 100, 則是不會觸發__nexindex元方法,而調用A["time"] = 100,則會觸發__nexindex元方法。實現了禁止其他人修改策劃配置的表格的需求嗎,沒有,這代碼就是廢的,還浪費內存浪費時間去執行。寫了個測試代碼,可以自行測試。
測試代碼:
local A =
{
title = "XX活動"
}
local base =
{
__newindex = function()
error( "Attempt to modify read-only table" )
end
}
setmetatable(A, base )
A["title"] = 100
Logger.Log("title: "..tostring(A["title"]))
A["time"] = 100
那真正的只讀表怎麼寫呢,其實在之前的文章([實戰]C++加Lua加SDL來重寫龍神錄彈幕遊戲(2):Lua創建SDL窗口和[實戰]C++加Lua加SDL來重寫龍神錄彈幕遊戲(4):完善Game類)中,已經說過了__nexindex的特性,並且也在遊戲中多次實現禁止重載函數的功能。
爲了方便記憶,我將讀取Window.ini的功能修改了,添加了一個TB_Window.lua的配置表。
TB_Window.lua
local Window =
{
["1"] =
{
title = "Ryuujinn",
x = SDL_POSITION_TYPE.SDL_WINDOWPOS_CENTERED,
y = SDL_POSITION_TYPE.SDL_WINDOWPOS_CENTERED,
width = 640,
height = 480,
fullscreen = 0,
},
}
do
for k, v in pairs(Window) do
local proxy = {}
local mt =
{
__index = v,
__newindex = function(t, k, v)
error("attempt to update a read-only talbe", 2)
end
}
Window[k] = setmetatable(proxy, mt)
end
end
return Window
相當於代理模式,就是新建一個表,並將策劃的表設置新表的元表,我們讀取數據不是從策劃配的表上獲取,而是從新表上獲取,新表是個空表,因此會觸發__index元方法,從元表(也就是策劃配的表上)獲取數據,在修改的時候就會觸發__nexindex元方法,因此就完美的實現了禁止其他人修改策劃配置的表格的需求。
回到GameBase.lua,註釋到讀取Window.ini配置的代碼,添加讀取TB_Window配置的代碼,來實現SDL窗口的初始化工作,之前忘記修改SDL窗口的位置,這次也一併修改了,使得SDL窗口居中顯示。
--初始化
function GameBase:Init()
self.m_bIsRunning = false
--SDL初始化
local flags = SDL_INIT_TYPE.SDL_INIT_VIDEO
local bResult = Renderer.SDLInit(flags)
if not bResult then
Logger.LogError("Renderer.SDLInit(%d) failed, %s", flags, Renderer.GetError())
return false
end
--1.讀取Window.ini配置
--local filePath = "Config/Window.ini"
--local windowConfig = io.open(filePath, "r")
--if not windowConfig then
-- Logger.LogError("Error: Can't find %s", filePath)
-- return false
--end
--self.m_title = windowConfig:read()
--self.m_width = tonumber(windowConfig:read())
--self.m_height = tonumber(windowConfig:read())
--self.m_bFullscreen = tonumber(windowConfig:read()) ~= 0
--windowConfig:close()
--local pos_x = 100
--local pos_y = 100
--2.讀取TB_Window配置
local tb_window = require "Config.TB_Window"
if not tb_window then
Logger.LogError("Error: Don't find the table Config.TB_Window")
return false
end
self.m_title = tb_window["1"].title
local pos_x = tb_window["1"].x
local pos_y = tb_window["1"].y
self.m_width = tb_window["1"].width
self.m_height = tb_window["1"].height
self.m_bFullscreen = tb_window["1"].fullscreen ~= 0
--根據Window.ini配置或者TB_Window配置創建SDL窗口
flags = 0
if self.m_bFullscreen then
flags = flags | SDL_WINDOW_TYPE.SDL_WINDOW_FULLSCREEN
end
self.m_pSDLWindow = Renderer.CreateWindow(self.m_title, pos_x, pos_y, self.m_width, self.m_height, flags)
--創建SDLRenderer
flags = SDL_RENDERER_TYPE.SDL_RENDERER_ACCELERATED | SDL_RENDERER_TYPE.SDL_RENDERER_PRESENTVSYNC
self.m_pSDLRenderer = Renderer.CreateRenderer(self.m_pSDLWindow, -1, flags)
--SDL_Image初始化
flags = SDL_IMAGE_INIT_TYPE.IMG_INIT_PNG
bResult = Renderer.SDLImageInit(flags)
if not bResult then
Logger.LogError("Renderer.SDLImageInit(%d) failed, %s", flags, Renderer.GetError())
return false
end
if self.OnInit then
bResult = self:OnInit()
if not bResult then
return false
end
end
self.m_bIsRunning = true
return true
end
這裏還有對TB_Window測試的代碼,可以自行測試是否真的完成只讀表的功能。
local tb_window = require "Config.TB_Window"
Logger.Log("title: "..tostring(tb_window["1"].title))
Logger.Log("x: "..tostring(tb_window["1"].x))
Logger.Log("y: "..tostring(tb_window["1"].y))
Logger.Log("width: "..tostring(tb_window["1"].width))
Logger.Log("height: "..tostring(tb_window["1"].height))
Logger.Log("fullscreen: "..tostring(tb_window["1"].fullscreen))
tb_window["1"].title = nil
tb_window["1"].x = nil
tb_window["1"].y = nil
tb_window["1"].width = nil
tb_window["1"].height = nil
tb_window["1"].fullscreen = nil
因爲現在手遊開發,國內很多都是用Unity,而且爲了熱更新,因此很多都會用lua開發,在面試的時候也會經常碰到lua的問題,小廠問的比較簡單,比如ipair和pair的區別啥的問題,大廠的話則是很細,你必須瞭解元表和元方法。曾在網易面試的時候被問到過元表和元方法,很輕鬆通過,但是被問到個問題,新手程序員在寫代碼的時候會經常將局部變量寫成全局變量,問你怎麼解決這個問題。剛被問到的時候有點懵,一時沒想到,給的思考時間也不是很多。回去後想了下,不就是隻讀表的問題嗎,然後仔細想了一下,還有一個方案,就是多次打印全局表的內容做對比。其實這2個解決方案都有缺陷,可以自己想下。
也寫了一個將全局表改成只讀表的測試代碼:
Logger.Log("_ENV: "..tostring(_ENV))
Logger.Log("__G: "..tostring(_G))
v1 = 100
Logger.Log("v1: "..tostring(v1))
local proxy = {}
local mt =
{
__index = _G,
__newindex = function(t, k, v)
error("attempt to update a global talbe", 2)
end
}
_G = setmetatable(proxy, mt)
--setfenv(1, _G)
_ENV = _G
Logger.Log("v1: "..tostring(v1))
v2 = 100
Logger.Log("v2: "..tostring(v2))
lua版本比較高,是5.3,因此沒有用setfenv,會報錯。查了下,lua5.2版本是用_ENV,測試可以。
這裏有2篇文章不錯,一篇代碼較長,估計很多新手不太易於理解,另一篇則是寫了實現只讀和只寫表的功能,可以詳細閱讀下。
lua只讀表(1)
lua只讀表(2)