这次就不急着往下讲解游戏功能了,先来说下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 stringformat_C++

       lua版本比较高,是5.3,因此没有用setfenv,会报错。查了下,lua5.2版本是用_ENV,测试可以。
这里有2篇文章不错,一篇代码较长,估计很多新手不太易于理解,另一篇则是写了实现只读和只写表的功能,可以详细阅读下。
lua只读表(1)lua只读表(2)

github地址