[實戰]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腳本路徑。

源碼下載地址

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