敲代码很快,写博客很慢,如果写详细一点,就有点太长了,跟写策划文档没区别了,╮(╯_╰)╭。之前虽然在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
#include
#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
#include
#include
#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了,不再设置为私有库了。
PS:我已经将龙神录的所有资源都拷过来了
源码下载地址
github地址