敲代碼很快,寫博客很慢,如果寫詳細一點,就有點太長了,跟寫策劃文檔沒區別了,╮(╯_╰)╭。之前雖然在SDL窗口中顯示出了一張人物圖片,但也只是爲了測試SDL_Image而已。現在就正式來完成這次的工作,顯示背景。
PS:老實說,我真沒有仔細看那個彈幕射擊遊戲的教程,因爲代碼寫的實在是太亂了。因此我就按照教程實現的效果來,不用他的亂七八糟的代碼。這次採用的框架也是我剛看的一本書上的,原本是C++寫的,我將其改成Lua來寫。書我也介紹下,《Game Programming in C++》,作者是Sanjay MADHAV, 前4章是講SDL的,這就是我會SDL的原因(..•˘_˘•..),後面是講OpenGL的。我看的是英文版的,在CSDN上搜了下,有資源的,中文版的在某東上竟然搜到,剩下的就看你們自己了。
刪除之前的Renderer類中的2個測試函數,GetTexture和Test,新添加Texture.h,Texture.cpp和TextureWrap.cpp文件。
Texture.h
#pragma once
class Texture
{
public:
Texture();
Texture(struct SDL_Renderer* pRenderer, const char* fileName);
virtual ~Texture();
/**
* 渲染貼圖
* In -> struct SDL_Renderer* pRenderer
* int x, int y - 顯示位置
*/
void Render(struct SDL_Renderer* pRenderer, int x, int y);
/**
* 渲染貼圖
* In -> struct SDL_Renderer* pRenderer
* int x, int y - 顯示位置
* int width, int height - 顯示的寬高(可以通過這2個值進行縮放)
*/
void Render(struct SDL_Renderer* pRenderer, int x, int y, int width, int height);
private:
/**
* 獲取貼圖寬高數據
*/
void QueryTexture();
private:
const char* m_fileName;
struct SDL_Texture* m_pSDLTexture = nullptr;
int m_textureWidth = 0;
int m_textureHeight = 0;
};
struct lua_State;
namespace LuaWrap
{
void RegisterTexture(lua_State* L);
}
Texture.cpp
#include "Texture.h"
#include <SDL.h>
#include <SDL_image.h>
#include "Logger.h"
Texture::Texture()
{
m_fileName = "";
m_pSDLTexture = nullptr;
m_textureWidth = 0;
m_textureHeight = 0;
}
Texture::Texture(SDL_Renderer* pRenderer, const char* fileName)
{
m_fileName = fileName;
SDL_Surface* pSurface = IMG_Load(fileName);
if (!pSurface)
{
Logger::LogError("IMG_Load() failed in Renderer::GetTexture(): %s", fileName);
return;
}
m_pSDLTexture = SDL_CreateTextureFromSurface(pRenderer, pSurface);
SDL_FreeSurface(pSurface);
if (!m_pSDLTexture)
{
Logger::LogError("SDL_CreateTextureFromSurface() failed in GetTexture(): %s", SDL_GetError());
return;
}
QueryTexture();
}
Texture::~Texture()
{
if (m_pSDLTexture)
{
SDL_DestroyTexture(m_pSDLTexture);
m_pSDLTexture = nullptr;
}
Logger::Log("Call Texture' destructor, m_fileName: %s", m_fileName);
}
void Texture::Render(SDL_Renderer* pRenderer, int x, int y)
{
if (nullptr == pRenderer || nullptr == m_pSDLTexture) return;
SDL_Rect destination;
destination.w = m_textureWidth;
destination.h = m_textureHeight;
destination.x = x;
destination.y = y;
SDL_RenderCopy(pRenderer, m_pSDLTexture, nullptr, &destination);
}
void Texture::Render(SDL_Renderer* pRenderer, int x, int y, int width, int height)
{
if (nullptr == pRenderer || nullptr == m_pSDLTexture) return;
if (nullptr == pRenderer || nullptr == m_pSDLTexture) return;
SDL_Rect destination;
destination.w = width;
destination.h = height;
destination.x = x;
destination.y = y;
SDL_RenderCopy(pRenderer, m_pSDLTexture, nullptr, &destination);
}
void Texture::QueryTexture()
{
if (m_pSDLTexture)
SDL_QueryTexture(m_pSDLTexture, nullptr, nullptr, &m_textureWidth, &m_textureHeight);
}
TextureWrap.cpp
#include "Texture.h"
#include <SDL.h>
#include <LuaClient.h>
#include <new>
#include "Logger.h"
int CreateTexture(lua_State* L)
{
int count = lua_gettop(L);
if (0 == count)
{
void* pointerToTexture = lua_newuserdata(L, sizeof(Texture));
new (pointerToTexture)Texture();
}
else if(2 == count)
{
SDL_Renderer* pRenderer = (SDL_Renderer*)lua_touserdata(L, 1);
const char* fileName = lua_tostring(L, 2);
void* pointerToTexture = lua_newuserdata(L, sizeof(Texture));
new (pointerToTexture)Texture(pRenderer, fileName);
}
else
Logger::LogError("Error: Texture don't have the constructor have %d arguments.", count);
luaL_getmetatable(L, "TextureMetaTable");
lua_setmetatable(L, -2);
return 1;
}
int DestroyTexture(lua_State* L)
{
Texture* pTexture = (Texture*)lua_touserdata(L, 1);
if (pTexture)
pTexture->~Texture();
return 0;
}
int RenderTexture(lua_State* L)
{
int count = lua_gettop(L);
Texture* pTexture = (Texture*)lua_touserdata(L, 1);
SDL_Renderer* pRenderer = (SDL_Renderer*)lua_touserdata(L, 2);
int x = (int)lua_tonumber(L, 3);
int y = (int)lua_tonumber(L, 4);
if (4 == count)
pTexture->Render(pRenderer, x, y);
else if(6 == count)
{
int width = (int)lua_tonumber(L, 5);
int height = (int)lua_tonumber(L, 6);
pTexture->Render(pRenderer, x, y, width, height);
}
return 0;
}
int TextureGet(lua_State* L)
{
luaL_getmetatable(L, "TextureMetaTable");
lua_pushvalue(L, 2);
lua_rawget(L, -2);
return 1;
}
void LuaWrap::RegisterTexture(lua_State* L)
{
if (!L) return;
lua_newtable(L);
luaL_newmetatable(L, "TextureMetaTable");
lua_pushstring(L, "New");
lua_pushcfunction(L, CreateTexture);
lua_rawset(L, -3);
lua_pushstring(L, "__index");
lua_pushcfunction(L, TextureGet);
lua_rawset(L, -3);
lua_pushstring(L, "__gc");
lua_pushcfunction(L, DestroyTexture);
lua_rawset(L, -3);
lua_pushstring(L, "Render");
lua_pushcfunction(L, RenderTexture);
lua_rawset(L, -3);
lua_setmetatable(L, -2);
lua_setglobal(L, "Texture");
}
頭文件沒啥好說的,註釋也比較多,先來看Texture的實現。雖然有2個構造函數,但其中一個只是爲了介紹lua怎麼調用同名且不同參數個數的函數例子而已。因此那個構造函數是沒啥用的,雖然可以再添個接口來處理加載SDL_Texture,但核心不是這個,因此就不關注了。
這邊需要關注下析構函數,加了一個輸出信息,這個是爲了測試Lua中是否有調用GC功能完成C++這邊的內存釋放功能。
再來看Texture的Render函數,也有2個,只是爲了可以控制縮放顯示圖片大小而已。後面要顯示動畫的話,估計還要添加個接口,或者寫個子類來完成改功能,這些暫且不說。先來看下SDL_RenderCopy的參數,前2個沒啥好說的,第3個參數srcrect,這個我猜應該是跟IDXSprite的功能一樣,讀取貼圖中某一個區域,第4個參數dstrect則是顯示區域,dstrect的x,y代表的是位置,w,h則是顯示的寬高,因此能明白3個參數的Render函數是顯示默認貼圖大小的,而5個參數的Render函數是可以控制縮放顯示圖片的大小。
接着來看TextureWrap.cpp,先來看Wrap的構造函數,通過lua_gettop獲取棧中參數數量,再根據參數數量選擇合適的構造函數來,使用lua_newuserdata,是因爲要讓lua來管理內存。在接着看Wrap的析構函數,從棧中取出userdata的地址,強轉成Texture對象,再接着釋放內存。RenderTexture就不介紹了,跟構造函數類似。RegisterTexture函數中關注下__gc元方法的設置就OK了。
這次也就只增加了一個C++類,很多代碼其實都是在lua上的。爲了調試方便,我順便將LuaClient上的打印棧信息的接口Wrap到lua上去了。
這次添加的lua代碼很多,我就不一一介紹了,太長,先從最簡單的開始。
TextureManager.lua
TextureManager =
{
m_textures = {}, --所有C++Texture對象
}
--獲取Texture,沒有就加載到內存中
function TextureManager:GetTexture(pRenderer, textureName)
--Logger.Log("pRenderer: "..tostring(pRenderer))
--Logger.Log("textureName: "..tostring(textureName))
--沒有就加載到內存中,並標記引用次數爲1
if nil == self.m_textures[textureName] then
self.m_textures[textureName] = {}
self.m_textures[textureName].pTexture = Texture.New(pRenderer, textureName)
self.m_textures[textureName].count = 1
else
--標記引用次數自增1
self.m_textures[textureName].count = self.m_textures[textureName].count + 1
end
return self.m_textures[textureName].pTexture
end
function TextureManager:RemoveTexture(pTexture)
if nil == pTexture then return end
--查找,並減少引用次數1次,如果引用次數小於0,就釋放內存
for k, v in pairs(self.m_textures) do
if v.pTexture == pTexture then
self.m_textures[k].count = self.m_textures[k].count - 1
if self.m_textures[k].count <= 0 then
self.m_textures[k] = nil
break
end
end
end
end
TextureManager是全局表,類似於單例,功能就是管理加載貼圖,避免重複加載和釋放貼圖內存的功能。代碼有註釋,而且代碼也不長,只要關注是否將C++的Texture對象有沒有在lua中賦值爲nil就行,如果沒釋放,lua在GC中是不會釋放內存的,會導致內存泄漏的。因爲C++的Texture對象是在表中的,而且這張表也只有在TextureManager表中引用,爲了方便,直接將表賦值爲nil也完成內存釋放的功能了。
上圖就能表示lua釋放C++內存功能沒有問題。在請按任意鍵繼續前是動態釋放的,也就是按下F鍵,強制釋放玩家內存導致貼圖也被釋放了。而請按任意鍵繼續後則是lua運行結束後調用的。代碼會在後面的RyuujinnGame.lua上看到。
在說Actor之前,先來說下Component和SpriteComponet。
Component.lua
local Component =
{
m_pActor = nil, --擁有者
m_updateOrder = 100, --更新順序
}
Component.__index = Component
--構造函數
function Component:New(pActor, updateOrder, ...)
local newTable = {}
setmetatable(newTable, self)
self.__index = self
newTable.m_pActor = pActor
newTable.m_updateOrder = updateOrder
--將該組件對象傳入到Actor中的m_components列表中
--由Actor來管理該組件對象的更新釋放等
--因此Actor可以根據更新順序(m_updateOrder)來控制那個組件先更新
if newTable:GetActor() then
newTable:GetActor():AddComponent(newTable)
end
if newTable.OnInit then
newTable:OnInit(...)
end
return newTable
end
function Component:GetActor()
return self.m_pActor
end
function Component:GetUpdateOrder()
return self.m_updateOrder
end
--子類通過實現OnUpdate來完成多態
function Component:Update(deltaTime)
if self.OnUpdate then
self:OnUpdate(deltaTime)
end
end
--相當於析構函數,將該組件對象從Acotr中的m_components列表中移除
--子類通過實現OnRelease來完成多態,
function Component:Release()
if self:GetActor() then
self:GetActor():RemoveComponent(newTable)
end
if self.OnRelease then
self:OnRelease()
end
end
return Component
SpriteComponet.lua
local SpriteComponent =
{
m_pTexture = nil, --C++ Texture類
m_renderOrder = 100, --渲染順序
}
SpriteComponent.__index = SpriteComponent
setmetatable(SpriteComponent, require "Module.Component.Component")
function SpriteComponent:OnInit(renderOrder)
self.m_renderOrder = renderOrder
--將該組件對象傳入到RyuujinnGame中的m_pSpriteComponents列表中
--由RyuujinnGame來管理該組件的渲染等
--因此RyuujinnGame可以根據渲染順序(m_renderOrder)來控制那個先渲染
if self:GetActor() and self:GetActor():GetGame() then
self:GetActor():GetGame():AddSpriteComponent(self)
end
end
function SpriteComponent:GetRenderOrder()
return self.m_renderOrder
end
function SpriteComponent:Render(pSDLRenderer)
if nil == pSDLRenderer or nil == self.m_pTexture then return end
local pActor = self:GetActor()
if nil == pActor then return end
--獲取Actor的位置和縮放來渲染圖片
local x, y = pActor:GetPosition()
local width, height = pActor:GetScale()
if (1 == width and 1 == height) or (nil == width and nil == height) then
self.m_pTexture:Render(pSDLRenderer, x, y)
else
self.m_pTexture:Render(pSDLRenderer, x, y, width, height)
end
end
function SpriteComponent:SetTexture(pTexture)
self.m_pTexture = pTexture
end
function SpriteComponent:OnRelease()
if self:GetActor() and self:GetActor():GetGame() then
self:GetActor():GetGame():RemoveSpriteComponent(self)
end
TextureManager:RemoveTexture(self.m_pTexture)
self.m_pTexture = nil
end
return SpriteComponent
爲了實現子類構造函數參數和父類不同的問題,使用了lua的可變參數功能,可以在Component:New中看到會先檢查子類中是否有OnInit函數,如果有就將可變參數傳入OnInit函數中。再接着看下Component的子類SpriteComponent的OnInit函數,只有一個參數renderOrder參數。因爲後面寫的Player和BG都是採用默認值,所以只傳入一個參數,有興趣的可以傳入3個參數來測試。SpriteComponent稍微有點特殊,因爲它不僅僅受到Actor管理,也受到了RyuujinnGame的管理,這個會在後面講到。
接着來看下Actor,後面繼承Actor的Player和BG類就不會細講了,感覺太簡單了,這邊在說明下,Player還是測試代碼。
Actor.lua
local Actor =
{
m_game = nil, --遊戲類
m_actorState = ActorState.Active, --狀態
--m_position = { x = 0.0, y = 0.0 }, --位置
--m_scale = { width = 1.0, height = 1.0 }, --縮放
--m_components = {}, --所有組件
}
Actor.__index = Actor
--構造函數
function Actor:New(game, ...)
local newTable =
{
m_position = { x = 0.0, y = 0.0 },
m_scale = { width = 1.0, height = 1.0 },
m_components = {}
}
setmetatable(newTable, self)
self.__index = self
newTable.m_game = game
--將該Actor對象傳入到RyuujinnGame中的m_pActors或者m_pPendingActors列表中
--由RyuujinnGame來管理該Actor對象的更新釋放等
if newTable:GetGame() then
newTable:GetGame():AddActor(newTable)
end
if newTable.OnInit then
newTable:OnInit(...)
end
return newTable
end
function Actor:IsDead()
return ActorState.Dead == self.m_actorState
end
function Actor:GetGame()
return self.m_game
end
function Actor:GetActorState()
return self.m_actorState
end
function Actor:GetPosition()
return self.m_position.x, self.m_position.y
end
function Actor:GetScale()
return self.m_scale.width, self.m_scale.height
end
function Actor:SetActorState(actorState)
self.m_actorState = actorState
end
function Actor:SetPosition(x, y)
self.m_position.x, self.m_position.y = x, y
end
function Actor:SetScale(width, height)
self.m_scale.width, self.m_scale.height = width, height
end
--對組件進行排序,根據更新順序
local function SortComponent(a, b)
if a:GetUpdateOrder() ~= b:GetUpdateOrder() then
return a:GetUpdateOrder() < b:GetUpdateOrder()
end
return false
end
function Actor:AddComponent(pComponent)
if nil == pComponent then return end
table.insert(self.m_components, pComponent)
table.sort(self.m_components, SortComponent)
end
function Actor:RemoveComponent(pComponent)
if nil == pComponent then return end
for i = #self.m_components, 1, -1 do
if self.m_components[i] == pComponent then
table.remove(self.m_components, i)
return
end
end
end
function Actor:UpdateComponent(deltaTime)
for i = 1, #self.m_components do
if self.m_components[i] ~= nil then
self.m_components[i]:Update(deltaTime)
end
end
end
--子類通過實現OnUpdate來完成多態
function Actor:Update(deltaTime)
if ActorState.Active == self.m_actorState then
self:UpdateComponent()
if self.OnUpdate then
self:OnUpdate(deltaTime)
end
end
end
function Actor:Release()
if self:GetGame() then
self:GetGame():RemoveActor(self)
end
for i = #self.m_components, 1, -1 do
self.m_components[i]:Release()
self.m_components[i] = nil
end
if self.OnRelease then
self:OnRelease()
end
end
return Actor
Actor的構造函數類似Component,因此跳過,接着的是一堆Getter和Setter函數,也跳過。接着看到SortComponent和AddComponent,AddComponent這個函數之前就在Component中有看到,之前說Component受到Actor管理,就因爲這個功能,這裏解釋到底做了什麼:Actor將Component組件添加m_components表中,再調用SortComponent進行排序(根據Component中的更新順序,也就是m_updateOrder)。因此在後面的Actor:UpdateComponent函數中就可以根據更新順序來對所有組件進行一次有序的更新,舉個例子:這一幀本來是收到了玩家的輸入,受到控制的角色的MoveComponent應該使角色向前移動一步,在接着SpriteComponent按更新後的位置渲染,但因爲沒有更新順序的話,會導致先渲染再更新角色,舉得例子有可能不恰當,但差不多是這個意思。
最後的腳本就是RyuujinnGame.lua了,改動還是稍微有一點的,︿( ̄︶ ̄)︿。但是在說RyuujinnGame.lua之前,我們需要先修改Window.ini的配置,將窗口大小改成640x480就好了,我在調試的時候發現背景圖片大小就這麼大,也懶得去測試適應。而且這個640x480的大小還是我猜的,因爲日本那個教程用的是DxLib,就是我之前說不喜歡它的封裝,就沒有用DxLib來開發。這次調試的時候,就沒有找到設置窗口大小的地方,這有毒吧,太不友好了。
RyuujinnGame.lua
require "Module.Enum.ActorState"
require "Manager.TextureManager"
RyuujinnGame =
{
m_bUpdatingActors = true, --正在更新Actors
m_pActors = {}, --正在活動中的Actors
m_pPendingActors = {}, --新創建的Actors
m_pSpriteComponents = {}, --渲染Sprite
m_pPlayer = nil, --主角
}
setmetatable(RyuujinnGame,
{
__index = require "GameBase",
__newindex = function(t, key, newValue)--禁止重載GameBase函數
local oldValue = t[key]
if oldValue == nil or type(oldValue) ~= "function" then
rawset(t, key, newValue)
else
Logger.LogError("This action overrides GameBase's function is not allowed\n"..debug.traceback())
end
end
})
function RyuujinnGame:OnInit()
--添加背景
require("Entity.BG"):New(self, "Resource/img/board/10.png", 0, 0)
require("Entity.BG"):New(self, "Resource/img/board/11.png", 0, 16)
require("Entity.BG"):New(self, "Resource/img/board/12.png", 0, 464)
require("Entity.BG"):New(self, "Resource/img/board/20.png", 416, 0)
--添加玩家
self.m_pPlayer = require("Entity.Player"):New(self)
return true
end
function RyuujinnGame:OnRelease()
for i = #self.m_pActors, 1, -1 do
self.m_pActors[i]:Release()
end
for i = #self.m_pPendingActors, 1, -1 do
self.m_pPendingActors[i]:Release()
end
end
function RyuujinnGame:OnHandleInput()
if self.m_pPlayer and self.m_pPlayer.HandleInput then
self.m_pPlayer:HandleInput()
end
--測試是否有調用GC,釋放C++ Texture資源
if Renderer.GetKeyboardState(SDL_KEYCODE.SDL_SCANCODE_F) then
if self.m_pPlayer then
self.m_pPlayer:Release()
self.m_pPlayer = nil
end
end
end
function RyuujinnGame:OnUpdate(deltaTime)
collectgarbage("collect")
self:UpdateActor(deltaTime)
end
function RyuujinnGame:OnRender()
self:RenderSpriteComponent()
end
function RyuujinnGame:AddActor(pActor)
if self.m_bUpdatingActors then
table.insert(self.m_pPendingActors, pActor)
else
table.insert(self.m_pActors, pActor)
end
end
function RyuujinnGame:RemoveActor(pActor)
for i = #self.m_pPendingActors, 1, -1 do
if self.m_pPendingActors[i] == pActor then
table.remove(self.m_pPendingActors, i)
end
end
for i = #self.m_pActors, 1, -1 do
if self.m_pActors[i] == pActor then
table.remove(self.m_pActors, i)
end
end
end
function RyuujinnGame:UpdateActor(deltaTime)
self.m_bUpdatingActors = true
for k, v in pairs(self.m_pActors) do
v:Update(deltaTime)
end
self.m_bUpdatingActors = false
for i = #self.m_pPendingActors, 1, -1 do
table.insert(self.m_pActors, self.m_pPendingActors[i])
table.remove(self.m_pPendingActors, i)
end
local deadActors = {}
for i = #self.m_pActors, 1, -1 do
if self.m_pActors[i]:IsDead() then
table.insert(deadActors, self.m_pActors[i])
end
end
for i = #deadActors, 1, -1 do
deadActors[i]:Release()
end
end
--對組件進行排序,根據渲染順序
local function SortSpriteComponent(a, b)
if a:GetRenderOrder() ~= b:GetRenderOrder() then
return a:GetRenderOrder() < b:GetRenderOrder()
end
return false
end
function RyuujinnGame:AddSpriteComponent(pSpriteComponent)
table.insert(self.m_pSpriteComponents, pSpriteComponent)
table.sort(self.m_pSpriteComponents, SortSpriteComponent)
end
function RyuujinnGame:RemoveSpriteComponent(pSpriteComponent)
for i = #self.m_pSpriteComponents, 1, -1 do
if self.m_pSpriteComponents[i] == pSpriteComponent then
table.remove(self.m_pSpriteComponents, i)
end
end
end
function RyuujinnGame:RenderSpriteComponent()
for i = 1, #self.m_pSpriteComponents do
self.m_pSpriteComponents[i]:Render(self:GetRenderer())
end
end
之前說SpriteComponent稍微有點特殊,就是因爲該組件受到了RyuujinnGame的管理,因爲只有在RyuujinnGame中才能獲取所有SpriteComponent,然後根據渲染順序來進行渲染。RyuujinnGame中操作SpriteComponent的函數和Actor中操作AddComponent的函數非常類似,就不再重複了。
RyuujinnGame:OnInit初始化代碼就是添加4個BG對象,和一個Player對象,這裏的Player對象主要是爲了測試lua的CG用的,而這次的主要功能就是背景而已,不要忘記。因此,OnInit中這麼添加BG,就能被渲染出來了,而且也不要我們後面在特殊處理,因爲RyuujinnGame中的m_pActors表會自己管理BG的更新釋放,m_pSpriteComponents表會對BG進行渲染。
這裏在總結下這個框架:RyuujinnGame中有2個Actor表,一個是正在活動中的,一個是新生成的,而Actor中有1個Component表,因此每次在RyuujinnGame更新時,正在活動中的Actors都會調用Update方法來進行對Actor中的組件進行更新,這樣就做到了一個所有物體的更新,並且在其中有可能有新生成的Actor,而這些就不會填加到正在活動中的Actor表中,而是加入新生成的Actor表中,等正在活動中的Actors全部更新完畢後,就將新生成的Actor表中的對象全部放入到正在活動中的Actor表中,等待下一次的更新。
這次代碼真的比較多,就沒有一一介紹了,代碼我也會上傳,但發現現在CSDN的積分有時候不能控制了,有時候能控制,因此我就把該庫設置成public了,不再設置爲私有庫了。