[實戰]C++加Lua加SDL來重寫龍神錄彈幕遊戲(6):只讀表

       這次就不急着往下講解遊戲功能了,先來說下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)

github地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章