[实战]C++加Lua加SDL来重写龙神录弹幕游戏(2):Lua创建SDL窗口

       完成了准备工作之后,就可以开始撸代码了。因为项目也不是很大,就打算大部分都用lua来开发。上一篇已经写了一部分测试代码,但都是塞到一个Main.cpp之中,主要是为了测试配置是否成功。这次的工作就要把测试代码给提取出来,用lua来实现SDL窗口的创建。
Lua的搭建
       上一篇是用dostring来执行lua的,项目中不会用这玩意开发的(那要多累啊,排版也不爽)。先右键Ryuujinn工程创建一个Include文件夹来存放.h文件,再创建一个Script文件夹来存放lua文件。文件分类工作做好后,在Script文件夹下创建一个Main.lua,代码也很简单,就是把上一篇写在字符串中的lua代码拷过来,就OK。
Main.lua

print("Hello, lua")

       lua的工作,先到此,接下来,在Include文件夹创建LuaClient.h文件,在Source文件夹创建LuaClient.cpp文件。把Main.cpp中跟lua相关的代码(引入头文件和链接Lua库)全部拷贝到LuaClient.h文件中,再新建个类LuaClient。
       设计LuaClient为饿汉单例类,因此内部添加了个Garbage类来做LuaClient单例类的内存释放工作。单例的具体实现被我用#pragma region指令给wrap了,如果单例模式都不清楚,请去看设计模式。


       接下来添加私有的构造函数和析构函数和一个公有的Start函数,和一个私有的lua虚拟栈变量m_pLuaState。具体的看代码
LuaClient.h

#pragma once

// 引入lua需要的头文件
extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}

// 链接Lua工程生成的静态库
#pragma comment(lib, "Lua.lib")

class LuaClient
{
#pragma region Singleton 单例,利用Garbage类来释放内存
public:
	static LuaClient* GetInstance() { return m_instance; }

private:
	class Garbage
	{
	public:
		~Garbage()
		{
			if (m_instance)
				delete m_instance;
			m_instance = nullptr;
		}
	};

private:
	static LuaClient* m_instance;
	static Garbage m_garbage;
#pragma endregion

private:
	LuaClient();
	virtual ~LuaClient();

public:
	/**
	 * Lua脚本入口
	 * In ->  const char* strStartLuaFile		- 入口Lua脚本路径
	 */
	void Start(const char* strStartLuaFile);

private:
	lua_State* m_pLuaState;
};

       接下来就是实现了,也很简单,就是将上一篇的代码提取下分开放到构造函数和析构函数中,唯一的变化就是执行lua代码这一处地方。将dostring改成dofile而已。
LuaClient.cpp

#include "Test.h"
#include <iostream>

LuaClient* LuaClient::m_instance = new LuaClient();
LuaClient::Garbage LuaClient::m_garbage;

LuaClient::LuaClient()
{
	std::cout << "LuaClient::LuaClient()" << std::endl;
	// 创建虚拟栈
	m_pLuaState = luaL_newstate();	
	// 引入lua的标准库,不然调用print会报错,可以注释掉代码,查看报错
	luaL_openlibs(m_pLuaState);
}

LuaClient::~LuaClient()
{
	std::cout << "LuaClient::~LuaClient()" << std::endl;
	// 关闭虚拟栈
	lua_close(m_pLuaState);
}

void LuaClient::Start(const char* strStartLuaFile)
{
	if (!m_pLuaState) return;

	int result = luaL_dofile(m_pLuaState, strStartLuaFile);
	if (LUA_OK != result)
	{
		std::cout << lua_tostring(m_pLuaState, -1) << std::endl;
		return;
	}
}

       构造函数和析构函数添加输出代码,只是为了测试是否正常执行而已。接下来就是修改Main.cpp文件。因为lua相关的代码已经去掉了,比较简洁了,但看着还是不舒服,把SDL的代码也剪切掉,随便放在一个txt文件中也可以,等会会用到的。

修改后的Main.cpp:

#include "LuaClient.h"
#include <iostream>

int main()
{
	LuaClient::GetInstance()->Start("Script/Main.lua");

	system("pause");
	return 0;
}

       恩,干净多了,距离这一篇要完成后Main.cpp就差一点了。如果运行得到下面的结果,就说明目前的进制很顺利。

       接下来就是Lua调用C++的工作了,但在这之前,先来完善LuaClient这个类。来看一下完善后的LuaClent文件, 这里得说下,需要在Source文件夹下添加一个LuaClinetBind.cpp文件。
LuaClient.h

#pragma once

// 引入lua需要的头文件
extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}

// 链接Lua工程生成的静态库
#pragma comment(lib, "Lua.lib")

class LuaClient
{
#pragma region Singleton 单例,利用Garbage类来释放内存
public:
	static LuaClient* GetInstance() { return m_instance; }

private:
	class Garbage
	{
	public:
		~Garbage()
		{
			if (m_instance)
				delete m_instance;
			m_instance = nullptr;
		}
	};

private:
	static LuaClient* m_instance;
	static Garbage m_garbage;
#pragma endregion

private:
	LuaClient();
	virtual ~LuaClient();

public:
	/**
	 * Lua脚本入口
	 * In ->  const char* strStartLuaFile		- 入口Lua脚本路径
	 *		  const char* strEntryFunction		- 入口函数
	 */
	void Start(const char* strStartLuaFile, const char* strEntryFunction);

	/**
	 * 导出Cpp到lua
	 */
	void BindCppToLua();

	/**
	 * 输出lua虚拟栈的内容
	 * In ->  bool bPrintTable					- 是否输出table表中的内容
	 */
	void DumpStack(bool bPrintTable = false);

private:
	/**
	 * 添加lua目录
	 */
	void AddSearchPath();
	/**
	 * 获取Lua Script目录
	 */
	bool GetScriptPath(char* strOutPath, size_t length);

	/**
	 * 输出lua虚拟栈中table的内容
	 * In ->  int index							- lua虚拟栈中的索引
	 */
	void PrintTable(int index);

private:
	lua_State* m_pLuaState;
};

LuaClient.cpp

#include "LuaClient.h"
#include <iostream>
#include <Windows.h>

LuaClient* LuaClient::m_instance = new LuaClient();
LuaClient::Garbage LuaClient::m_garbage;

LuaClient::LuaClient()
{
	std::cout << "LuaClient::LuaClient()" << std::endl;
	// 创建虚拟栈
	m_pLuaState = luaL_newstate();	
	// 引入lua的标准库,不然调用print会报错,可以注释掉代码,查看报错
	luaL_openlibs(m_pLuaState);

	AddSearchPath();
}

LuaClient::~LuaClient()
{
	std::cout << "LuaClient::~LuaClient()" << std::endl;
	// 关闭虚拟栈
	lua_close(m_pLuaState);
}

void LuaClient::Start(const char* strStartLuaFile, const char* strEntryFunction)
{
	if (!m_pLuaState) return;

	BindCppToLua();

	// 将全局表压入栈中
	//lua_getglobal(m_pLuaState, "_G");
	//DumpStack(true);
	int result = luaL_dofile(m_pLuaState, strStartLuaFile);
	if (LUA_OK != result)
	{
		std::cout << lua_tostring(m_pLuaState, -1) << std::endl;
		return;
	}
	//DumpStack(true);
	//lua_pop(m_pLuaState, 1);

	lua_getglobal(m_pLuaState, strEntryFunction);

	// 调用指定的入口函数
	int argsNum = 0;		// 参数数量
	int resultsNum = 0;		// 返回值数量
	int errorFunc = 0;		// 错误处理函数在栈中的索引
	result = lua_pcall(m_pLuaState, argsNum, resultsNum, errorFunc);
	if (LUA_OK != result)
	{
		std::cout << lua_tostring(m_pLuaState, -1) << std::endl;
		return;
	}
}

void LuaClient::DumpStack(bool bPrintTable)
{
	if (!m_pLuaState) return;

	int top = lua_gettop(m_pLuaState);
	std::cout << "----------------------Stack Top----------------------" << std::endl;
	for (int i = top; i >= 1; --i)
	{
		int luaType = lua_type(m_pLuaState, i);
		switch (luaType)
		{
		case LUA_TNIL:
		{
			std::cout << "[" << i << "]: nil" << std::endl;
			break;
		}// For case LUA_TNIL

		case LUA_TBOOLEAN:
		{
			std::cout << "[" << i << "]: " << (lua_toboolean(m_pLuaState, i) ? "true" : "false") << std::endl;
			break;
		}// For case LUA_TBOOLEAN

		case LUA_TNUMBER:
		{
			std::cout << "[" << i << "]: " << lua_tonumber(m_pLuaState, i) << std::endl;
			break;
		}// For case LUA_TNUMBER

		case LUA_TSTRING:
		{
			std::cout << "[" << i << "]: " << lua_tostring(m_pLuaState, i) << std::endl;
			break;
		}// For case LUA_TSTRING

		case LUA_TTABLE:
		{
			if (bPrintTable)
			{
				std::cout << "[" << i << "]: " << std::endl;
				PrintTable(i);
			}
			else
				std::cout << "[" << i << "]: " << lua_typename(m_pLuaState, luaType)
				<< ": " << lua_topointer(m_pLuaState, i) << std::endl;
			break;
		}// For case LUA_TTABLE

		default:
			std::cout << "[" << i << "]: " << lua_typename(m_pLuaState, luaType)
				<< ": " << lua_topointer(m_pLuaState, i ) << std::endl;
			break;
		}// For switch (luaType)
	}// For end
	std::cout << "----------------------Stack Bottom----------------------" << std::endl;
}

void LuaClient::AddSearchPath()
{
	char luaScriptPath[1000] = { 0 };
	bool bResult = GetScriptPath(luaScriptPath, 1000);
	if (!bResult) return;

	// 获取lua搜索路径
	lua_getglobal(m_pLuaState, "package");
	lua_pushstring(m_pLuaState, "path");
	lua_gettable(m_pLuaState, -2);
	const char* path = lua_tostring(m_pLuaState, -1);

	// 拼接新的lua搜索路径
	static char newPath[1000] = { 0 };
	strcpy_s(newPath, 1000, path);
	strcat_s(newPath, 1000, ";");
	strcat_s(newPath, 1000, luaScriptPath);

	// 重新设置lua搜索路径
	lua_pop(m_pLuaState, 1);
	lua_pushstring(m_pLuaState, "path");
	lua_pushstring(m_pLuaState, newPath);
	lua_settable(m_pLuaState, -3);

	// 恢复lua栈
	lua_pop(m_pLuaState, 1);
}

bool LuaClient::GetScriptPath(char* strOutPath, size_t length)
{
	if (nullptr == strOutPath) return false;

	// 获取exe的路径
	char exePath[1000] = { 0 };
	GetModuleFileName(NULL, exePath, 1000);

	// 获取当前解决方案的路径
	char* findChar = strstr(exePath, "bin");
	size_t copyLength = strlen(exePath) - strlen(findChar);
	if (length <= copyLength) return false;
	memcpy(strOutPath, exePath, copyLength);

	// 拼接lua脚本所在的相对路径
	strcpy_s(strOutPath + strlen(strOutPath), length - strlen(strOutPath), "Ryuujinn\\Script\\?.lua");

	return true;
}

void LuaClient::PrintTable(int index)
{
	std::cout << "{" << std::endl;

	if (m_pLuaState)
	{
		// 缓存之前的虚拟栈,因为后面要对虚拟栈进行操作
		int oldTop = lua_gettop(m_pLuaState);

		// 将需要输出的table压入栈顶
		lua_pushvalue(m_pLuaState, index);
		// 利用当前key(nil)值在进行遍历查找下一个key和value
		lua_pushnil(m_pLuaState);
		while (0 != lua_next(m_pLuaState, -2))
		{
			std::cout << "\t";
			// Stack     
			//				from bottom to top		from top to bottom
			// |  value |			3						-1
			// +--------+
			// |   key  |			2						-2
			// +--------+
			// |  table |			1						-3
			// +========+		
			int luaType = lua_type(m_pLuaState, -2);
			// 对key值进行处理
			{
				switch (luaType)
				{
				case LUA_TNIL:
				{
					std::cout << "nil";
					break;
				}// For case LUA_TNIL

				case LUA_TBOOLEAN:
				{
					std::cout << (lua_toboolean(m_pLuaState, -2) ? "true" : "false");
					break;
				}// For case LUA_TBOOLEAN

				case LUA_TNUMBER:
				{
					std::cout << lua_tonumber(m_pLuaState, -2);
					break;
				}// For case LUA_TNUMBER

				case LUA_TSTRING:
				{
					std::cout << lua_tostring(m_pLuaState, -2);
					break;
				}// For case LUA_TSTRING

				default:
					std::cout << lua_typename(m_pLuaState, luaType)
						<< ": " << lua_topointer(m_pLuaState, -2);
					break;
				}// For switch (luaType)
			}// Process key
			
			std::cout << "\t\t";
			// 对Value值进行处理
			{
				luaType = lua_type(m_pLuaState, -1);
				switch (luaType)
				{
				case LUA_TNIL:
				{
					std::cout << "nil" << std::endl;
					break;
				}// For case LUA_TNIL

				case LUA_TBOOLEAN:
				{
					std::cout << (lua_toboolean(m_pLuaState, -1) ? "true" : "false") << std::endl;
					break;
				}// For case LUA_TBOOLEAN

				case LUA_TNUMBER:
				{
					std::cout << lua_tonumber(m_pLuaState, -1) << std::endl;
					break;
				}// For case LUA_TNUMBER

				case LUA_TSTRING:
				{
					std::cout << lua_tostring(m_pLuaState, -1) << std::endl;
					break;
				}// For case LUA_TSTRING

				default:
					std::cout << lua_typename(m_pLuaState, luaType)
						<< ": " << lua_topointer(m_pLuaState, -1) << std::endl;
					break;
				}// For switch (luaType)
			}// Process value

			// 将Value从虚拟栈继续弹出,利用当前key值在进行遍历查找下一个key和value
			lua_pop(m_pLuaState, 1);
		}

		// 恢复成之前缓存的虚拟栈
		lua_settop(m_pLuaState, oldTop);
	}

	std::cout << "}" << std::endl;
}

LuaClinetBind.cpp

#include "LuaClient.h"

void LuaClient::BindCppToLua()
{

}

       新加的几个函数中DumpStack和PrintTable函数我就不细说了,因为没啥好说的。如果对lua虚拟栈比较清楚的话,自己也可以写,网上搜也有一堆,如果不清楚的话,就需要从lua和C++通信开始说起,有点长了。从栈顶往栈底显示只是出于个人习惯。如果这篇篇幅不长的话,我可以试着在最下面来介绍一下。
       AddSearchPath和GetScriptPath暂时先不讲,因为现在还用不上。BindCppToLua函数注释上也写明白了功能,但现在没有导出的Cpp,所以是个空函数。那就讲修改过后的Start函数。在讲之前,先修改Main.lua脚本。
修改后Main.lua

function Main()
	print("Main")
end

       用lua编译器等工具执行(不能用Ryuujinn工程来执行,会报错,如果一步一步来写改变的话,篇幅太长),没有看到控制台输出Main这个字符串的,如果想看到输出怎么办?最简单的方法是在Main.lua下添加调用就好,比如这样:
再次修改后Main.lua

function Main()
	print("Main")
end

Main()

       但是我偏不想这样调用,想通过Cpp来控制调用呢?这就是修改后Start函数的功能,先来看注释,const char* strEntryFunction指的是入口函数,就是说想调用lua的那一个函数(根据函数名来调用)。这里先恢复成刚才只有一个Main函数的Main.lua脚本。
       现在来看修改后的Start函数。dofile之后新增加的代码就只有lua_getglobal和lua_pcall这2个API,这里简单介绍下这2个API,lua_getglobal就是根据第2个参数从全局表查找对应的值,如果找到就将其压入栈中,没有找到就将nil压入栈中。lua_pcall就更简单明了了,就是调用一个函数,3个参数都有注释,就不再解释了。现在应该明白dofile之后的代码功能了吧:根据函数名从全局表中查找函数的指针,然后调用查找到的函数。
       我这里还添加了总共4行辅助代码,都被我注释了,可以取消注释,从控制台查看变化。这里可以解释下具体的实现:前2句注释代码实现的功能是将lua中的全局表压入栈中,输入lua全局表的内容。后2句注释代码实现的功能是输出dofile后lua全局表的内容,再利用lua_pop这个API实现堆栈平衡。你会发现在全局表中多了一个键值:key是Main,Value是一个函数。

       好了,上面已经完美实现调用Lua脚本这个功能了(看起来),接下来就需要实现Cpp导出lua的功能了。虽然可以直接将SDL相关的方法导出到lua,但查找API就不太方便了,因此添加了一个帮助类Renderer。在Include文件夹中添加Renderer.h方法,在Source文件夹中添加Renderer.cpp方法,并且在Source文件夹中添加一个新的文件夹:Wrap文件夹(里面存放导出实现Cpp导出lua功能的文件),并且添加第一个Cpp导出Lua的文件RendererWrap.cpp。
Renderer.h

#pragma once

#define SDL_MAIN_HANDLED

// 引入SDL需要的头文件
#include <SDL.h>
#include <SDL_image.h>

// 链接SDL静态库
#pragma comment(lib, "SDL2.lib")
#pragma comment(lib, "SDL2main.lib")
#pragma comment(lib, "SDL2_image.lib")

class Renderer
{
public:
	static bool Init(Uint32 flag);
	static SDL_Window* CreateWindow(const char* title, int posX, int posY, int width, int hegiht, Uint32 flags);
	static SDL_Renderer* CreateRenderer(SDL_Window* pWindow, int index, Uint32 flags);

	static void DestroyWindow(SDL_Window* pWindow);
	static void DestroyRenderer(SDL_Renderer* pRenderer);
	static void Quit();
};

struct lua_State;
namespace LuaWrap
{
	void RegisterRenderer(lua_State* L);
}

Renderer.cpp

#include "Renderer.h"

bool Renderer::Init(Uint32 flag)
{
	return 0 == SDL_Init(flag);
}

SDL_Window* Renderer::CreateWindow(const char* title, int posX, int posY, int width, int hegiht, Uint32 flags)
{
	return SDL_CreateWindow(title, posX, posY, width, hegiht, flags);
}

SDL_Renderer* Renderer::CreateRenderer(SDL_Window* pWindow, int index, Uint32 flags)
{
	return SDL_CreateRenderer(pWindow, index, flags);
}

void Renderer::DestroyWindow(SDL_Window* pWindow)
{
	if (pWindow)
		SDL_DestroyWindow(pWindow);
}

void Renderer::DestroyRenderer(SDL_Renderer* pRenderer)
{
	if (pRenderer)
		SDL_DestroyRenderer(pRenderer);
}

void Renderer::Quit()
{
	SDL_Quit();
}

       Renderer类的API其实很简单,就是包装了一下SDL的API,完全没有啥内容。这里解释下,我不会仔细介绍SDL相关的API(网上一搜就有很多),因为我没有这个资格介绍(学习SDL也没多久,撑死1个月的时间),而且这都是官方定义好的,就像Window窗口的初始化,RegisterWindow,CreateWindow及ShowWindow等这些API,只要会用就好。
RendererWrap.cpp

#include "Renderer.h"
#include <LuaClient.h>
#include <iostream>

int Init(lua_State* L)
{
	Uint32 flag = (Uint32)lua_tointeger(L, 1);
	lua_pushboolean(L, Renderer::Init(flag));

	return 1;
}

int CreateWindow(lua_State* L)
{
	const char* title = lua_tostring(L, 1);
	int posX = (int)lua_tointeger(L, 2);
	int posY = (int)lua_tointeger(L, 3);
	int width = (int)lua_tointeger(L, 4);
	int hegiht = (int)lua_tointeger(L, 5);
	Uint32 flag = (Uint32)lua_tointeger(L, 6);
	lua_pushlightuserdata(L, Renderer::CreateWindow(title, posX, posY, width, hegiht, flag));

	return 1;
}

int CreateRenderer(lua_State* L)
{
	SDL_Window* pWindow = (SDL_Window*)lua_touserdata(L, 1);
	int index = (int)lua_tointeger(L, 2);
	Uint32 flag = (Uint32)lua_tointeger(L, 3);
	lua_pushlightuserdata(L, Renderer::CreateRenderer(pWindow, index, flag));

	return 1;
}

int DestroyWindow(lua_State* L)
{
	SDL_Window* pWindow = (SDL_Window*)lua_touserdata(L, 1);
	Renderer::DestroyWindow(pWindow);

	return 0;
}

int DestroyRenderer(lua_State* L)
{
	SDL_Renderer* pRenderer = (SDL_Renderer*)lua_touserdata(L, 1);
	Renderer::DestroyRenderer(pRenderer);

	return 0;
}

int Quit(lua_State* L)
{
	Renderer::Quit();

	return 0;
}

int RendererGet(lua_State* L)
{
	const char* strKey = lua_tostring(L, 2);

	luaL_getmetatable(L, "RendererMetaTable");
	lua_pushvalue(L, 2);
	lua_rawget(L, -2);
	if (lua_isnil(L, -1))
		std::cout << "Renderer don't have the field: " << strKey << std::endl;

	return 1;
}

int RendererSet(lua_State* L)
{
	luaL_getmetatable(L, "RendererMetaTable");
	lua_pushvalue(L, 2);
	lua_rawget(L, -2);
	if (lua_isnil(L, -1))
	{
		lua_pop(L, 1);
		lua_pushvalue(L, 2);
		lua_pushvalue(L, 3);
		lua_rawset(L, -3);
	}
	else
        {
		if(LUA_TFUNCTION == lua_type(L, -1))
			std::cout << "The action is not allowed." << std::endl;
		else
		{
			lua_pop(L, 1);
			lua_pushvalue(L, 2);
			lua_pushvalue(L, 3);
			lua_rawset(L, -3);
		}
	}

	return 0;
}

void LuaWrap::RegisterRenderer(lua_State* L)
{
	lua_newtable(L);

	luaL_newmetatable(L, "RendererMetaTable");
	lua_pushstring(L, "__index");
	lua_pushcfunction(L, RendererGet);
	lua_rawset(L, -3);

	lua_pushstring(L, "__newindex");
	lua_pushcfunction(L, RendererSet);
	lua_rawset(L, -3);

	lua_pushstring(L, "Init");
	lua_pushcfunction(L, Init);
	lua_rawset(L, -3);

	lua_pushstring(L, "CreateWindow");
	lua_pushcfunction(L, CreateWindow);
	lua_rawset(L, -3);

	lua_pushstring(L, "CreateRenderer");
	lua_pushcfunction(L, CreateRenderer);
	lua_rawset(L, -3);

	lua_pushstring(L, "DestroyWindow");
	lua_pushcfunction(L, DestroyWindow);
	lua_rawset(L, -3);

	lua_pushstring(L, "DestroyRenderer");
	lua_pushcfunction(L, DestroyRenderer);
	lua_rawset(L, -3);

	lua_pushstring(L, "Quit");
	lua_pushcfunction(L, Quit);
	lua_rawset(L, -3);

	lua_setmetatable(L, -2);
	lua_setglobal(L, "Renderer");
}

       之前LuaClinetBind.cpp也要修改下:

#include "LuaClient.h"
#include "Renderer.h"

void LuaClient::BindCppToLua()
{
	LuaWrap::RegisterRenderer(m_pLuaState);
}

       估计有不少人发现Renderer.h文件中命名空间LuaWrap下的RegisterRenderer函数的实现没有在Renderer.cpp中,没错,实现在RendererWrap.cpp中。因为Cpp导出lua的核心就在这里,为了逻辑清晰,特意分离出来。先来说下Cpp函数导入到lua的结构必须是int FunctionName(lua_State*)结构,返回值int表示有多少个返回值,这个也许好理解,但参数呢?需要从虚拟栈中取。
       就拿CreateWindow来举例,该函数有6个参数,先从lua调用开始说起:
1.lua调用lua的CreateWindow函数,按从左到右的顺序将参数依次压入栈中,所以最左边的参数在栈底,最右边的参数在栈顶。
2.lua的CreateWindow函数根据Cpp函数的地址调用到了Cpp的CreateWindow函数
3.根据虚拟栈取出需要的参数。lua的索引关系是这样的,从栈底到栈顶,索引是正的,按照顺序排列,比如1,2,3,4...n这样,但从栈顶到栈底,索引是负的,按照逆序排列,比如-1,-2,-3,-4...-n这样。所以可以理解为栈顶即等于-1,也等于n,栈底即等于1,也等于-n。
4.因为有一个返回值,所以需要将Renderer::CreateWindow(也就是SDL_CreateWindow返回的SDL_Window*)压入栈中
5.lua从栈中从栈顶按返回值数量取值
       以上5步就完成了一次lua调用Cpp的功能,是不是很简单,如果搞清楚lua的栈后,都是小case了。
       但在lua调用Cpp函数之前,我们需要把Cpp函数注册到lua内存中,这就是RegisterRenderer函数实现的功能了。逻辑大致是这样的:
1.创建一个table,后面命名为Renderer(跟C++类名相同,方便调用)。
2.创建一个名称为RendererMetaTable的元表。
3.设置元表的__index和__newindex元方法
4.注入C++中Renderer的函数到RendererMetaTable元表中。
5.设置RendererMetaTable元表为Renderer表的元表。
       其中调用lua_rawset是不想触发__newindex元方法,稍微加快了速度。不想把C++的函数注册到Renderer中,而注册到元表中的原因是我想不让人在lua中重载了注册到lua中的C++函数。
举个例子:

1.先把RendererSet这个函数中的else代码段改成if中的一样

int RendererSet(lua_State* L)
{
	luaL_getmetatable(L, "RendererMetaTable");
	lua_pushvalue(L, 2);
	lua_rawget(L, -2);
	if (lua_isnil(L, -1))
	{
		lua_pop(L, 1);
		lua_pushvalue(L, 2);
		lua_pushvalue(L, 3);
		lua_rawset(L, -3);
	}
	else
	{
		lua_pop(L, 1);
		lua_pushvalue(L, 2);
		lua_pushvalue(L, 3);
		lua_rawset(L, -3);
	}
		//std::cout << "The action is not allowed." << std::endl;

	return 0;
}

2.在lua中重载Renderer.Init函数,将其改成lua中随便写的一个Test函数。

local function Test(flag)
	print("Lua flag: "..tostring(flag))
end

function Main()
	print("Lua start...")
	local flag = "00000020"
	flag = tonumber(flag, 16)

	Renderer.Init(flag)
	print(Renderer.Init)
	Renderer.Init = Test
	Renderer.Init(flag)
	print(Renderer.Init)
	print("Lua end...")
end

3.调用Renderer.Init函数。


       你会发现你调用的不是C++中的函数,而是刚才重新赋值后的Test函数(lua中的)。这个结果不是我所期望的,所以必须控制,但__newindex元方法的特性是如果已存在的索引键,则会进行赋值,而不调用元方法__newindex,因此不想被修改的函数都注册到元表中。

       但为了可以扩展,所以将__newindex元方法改成RendererWrap中的RendererSet函数一样,可以添加新的键值对,但不可以修改已有的。比如将Main.lua改成这样:

local function Test(flag)
	print("Lua flag: "..tostring(flag))
end

function Main()
	print("Lua start...")
	local flag = "00000020"
	flag = tonumber(flag, 16)

	Renderer.Init(flag)
	print(Renderer.Init)
	Renderer.Init = Test
	Renderer.Init(flag)
	print(Renderer.Init)

	print("Lua Add Test field in Renderer table...")
	Renderer.Test = Test
	Renderer.Test(flag)

	print("Lua end...")
end

       结果如下图,可以看到在尝试修改Renderer.Init函数的时候,报了个Error: The action is not allowed.但是我们有为Renderer表添加了一个新的键(Test字符串)值(Test函数)对,执行后没有问题。

       介绍C++导出类到Lua花了不少时间,现在开始回归当前的目标,Lua创建SDL窗口。继续在Script添加一个Game.lua,内容暂时先这样:
Game.lua

local GameBase = 
{
	pSDLWindow = nil,
	pSDLRenderer = nil
}

local Game = setmetatable({}, { __index = GameBase })
print("Game")
return Game

Main.lua

function Main()
	print("Main")
	--将16进制字符串转换成10进制数字
	local flag = "00000020"
	flag = tonumber(flag, 16)
	
	Renderer.Init(flag)
	--这里需要设置lua查找路径
	gGame = require("Game")
	gGame.pSDLWindow = Renderer.CreateWindow("Test", 100, 100, 500, 500, 0)
	gGame.pSDLRenderer = Renderer.CreateRenderer(gGame.pSDLWindow, -1, 0)
	
	--while循环中添加print函数是为了避免执行太快,窗口一闪而过
	local count = 1
	while count < 10000 do
		count = count + 1
		print(count)
	end
	
	Renderer.DestroyRenderer(gGame.pSDLRenderer)
	Renderer.DestroyWindow(gGame.pSDLWindow)
	Renderer.Quit()
end

       Game.lua的代码很简单,没啥好说的,Main.lua脚本也差不多,跟C++写代码没多少区别。这里有3处地方说一下。2处比较简单(代码中也已经有详细的注释了),1处比较麻烦,涉及到之前没用讲的LuaClient类中AddSearchPath和GetScriptPath函数。如果之前没有设置好lua文件查找路径的话,代码执行到require("Game")会报错,控制台上会显示处一堆路径。原因其实很简单,Game.lua不在lua查找路径中,找不到该文件,所以require失败。这里有篇文章是讲require路径的,可以看下
       好,回到LuaClient类中AddSearchPath函数中,这个函数在构造函数调用中,是一开始就已经执行了的,调用顺序就不要关心了。注释也比较清楚,只是有一个要点是不要传局部变量(字符串)到lua中,lua其实是没有拷贝内容的,只是拷贝了地址,过了作用域后,就变成野指针了,会报错。GetScriptPath函数功能就是定位lua脚本路径的,因为我之前修改过exe的生成路径,所以代码是这样的,如果exe路径生成路径跟我不一样,需要自己去定位lua脚本路径。

源码下载地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值