[實戰]C++加Lua加SDL來重寫龍神錄彈幕遊戲(5):添加背景

       敲代碼很快,寫博客很慢,如果寫詳細一點,就有點太長了,跟寫策劃文檔沒區別了,╮(╯_╰)╭。之前雖然在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了,不再設置爲私有庫了。

PS:我已經將龍神錄的所有資源都拷過來了
源碼下載地址
github地址

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