第三方库已经完成,接下来我们要考虑引擎架构的事情了,架构一款从无到有的引擎,从哪里下手呢?面对架构一款比较庞大的引擎,建议先从引擎的内存管理入手,因为内存是跟硬件相关的,它也是引擎的心脏,处理不好,引擎会经常崩溃的。还有因为引擎的代码量比较大,这里只把核心的代码给读者展示,后面会把完整的代码给读者展示,再编写引擎的内存管理之前,我们可以研究一下网上的一些经典引擎的内存管理。
先看看Ogre引擎的内存管理,内存管理主要分为两类:
内存分配器的管理
垃圾回收
Ogre采用了智能指针,使用的是引用计数的实现方式,这种实现方式大部分引擎使用的,其核心还是内存的的分配和垃圾回收。在使用多线程时也要注意计数的引用。
还有引擎采用的是:采用内存池的方式,每个内存池由多个chunk组成,每个chunk默认大小25600,由多个Block组成。每个Block的大小就是1-255,每个Chunk有多少个Block,是默认大小除以Block大小,最多255个最少1个,分配内存时,如果一个按大小从内存池中取出FixAlloctor,然后取空闲Chunk的空闲Block。每次分配时检测当前Chunk是否空,如果有则分配。如果Chunk满了则检测EmptyChunk,如果有EmptyChunk则分配。如果没有空Chunk,则遍历所有的Chunk直到找到一个有空间的Chunk,如果全都满了则新建一个Chunk插入Chunk列表中。这也是一种内存分配思想。
我们将要实现的引擎使用的是LIFO后进先出的方式进行分配,先定义内存的基本方法。
virtual void* allocate(size_t size) = 0;
virtual void deallocate(void* ptr) = 0;
virtual void* reallocate(void* ptr, size_t size) = 0;
virtual void* allocate_aligned(size_t size, size_t align) = 0;
virtual void deallocate_aligned(void* ptr) = 0;
virtual void* reallocate_aligned(void* ptr, size_t size, size_t align) = 0;
template <class T> void deleteObject(T* ptr)
{
if (ptr)
{
ptr->~T();
deallocate_aligned(ptr);
}
}
该代码段所属的类是内存管理的父类,所以声明的函数都是虚函数,它的具体实现是在其子类中实现的,子类的代码如下所示:
void* DefaultAllocator::allocate(size_t n)
{
return malloc(n);
}
void DefaultAllocator::deallocate(void* p)
{
free(p);
}
void* DefaultAllocator::reallocate(void* ptr, size_t size)
{
return realloc(ptr, size);
}
void* DefaultAllocator::allocate_aligned(size_t size, size_t align)
{
return aligned_alloc(align, size);
}
void DefaultAllocator::deallocate_aligned(void* ptr)
{
free(ptr);
}
void* DefaultAllocator::reallocate_aligned(void* ptr, size_t size, size_t align)
{
if (size == 0) {
free(ptr);
return nullptr;
}
void* newptr = aligned_alloc(align, size);
if (newptr == nullptr) {
return nullptr;
}
memcpy(newptr, ptr, malloc_usable_size(ptr));
free(ptr);
return newptr;
}
上面给读者显示的代码是正常的一种内存分配方式,我们采用了LIFO,后进先出的方式。
代码具体实现如下所示:
class LIFOAllocator LUMIX_FINAL : public IAllocator
{
public:
LIFOAllocator(IAllocator& source, size_t bucket_size)
: m_source(source)
, m_bucket_size(bucket_size)
{
m_bucket = source.allocate(bucket_size);
m_current = m_bucket;
}
~LIFOAllocator()
{
m_source.deallocate(m_bucket);
}
void* allocate(size_t size) override
{
u8* new_address = (u8*)m_current;
ASSERT(new_address + size <= (u8*)m_bucket + m_bucket_size);
m_current = new_address + size + sizeof(size_t);
*(size_t*)(new_address + size) = size;
return new_address;
}
void deallocate(void* ptr) override
{
if (!ptr) return;
size_t last_size = *(size_t*)((u8*)m_current - sizeof(size_t));
u8* last_mem = (u8*)m_current - last_size - sizeof(size_t);
ASSERT(last_mem == ptr);
m_current = ptr;
}
void* reallocate(void* ptr, size_t size) override
{
if (!ptr) return allocate(size);
size_t last_size = *(size_t*)((u8*)m_current - sizeof(size_t));
u8* last_mem = (u8*)m_current - last_size - sizeof(size_t);
ASSERT(last_mem == ptr);
m_current = last_mem + size + sizeof(size_t);
*(size_t*)(last_mem + size) = size;
return ptr;
}
void* allocate_aligned(size_t size, size_t align) override
{
size_t padding = (align - ((uintptr)m_current % align)) % align;
u8* new_address = (u8*)m_current + padding;
ASSERT(new_address + size <= (u8*)m_bucket + m_bucket_size);
m_current = new_address + size + sizeof(size_t);
*(size_t*)(new_address + size) = size + padding;
ASSERT((uintptr)new_address % align == 0);
return new_address;
}
void deallocate_aligned(void* ptr) override
{
if (!ptr) return;
m_current = ptr;
}
void* reallocate_aligned(void* ptr, size_t size, size_t align) override
{
if (!ptr) return allocate_aligned(size, align);
size_t last_size_with_padding = *(size_t*)((u8*)m_current - sizeof(size_t));
u8* last_mem = (u8*)m_current - last_size_with_padding - sizeof(size_t);
size_t padding = (align - ((uintptr)last_mem % align)) % align;
u8* new_address = (u8*)last_mem + padding;
ASSERT(new_address + size <= (u8*)m_bucket + m_bucket_size);
m_current = new_address + size + sizeof(size_t);
*(size_t*)(new_address + size) = size + padding;
ASSERT((uintptr)new_address % align == 0);
return new_address;
}
private:
IAllocator& m_source;
size_t m_bucket_size;
void* m_bucket;
void* m_current;
};
不论采用内存的什么方案,最终目的是防止内存分配出现溢出。还有采用多线程分配的,考虑的问题会更多,内存分配这块处理好了,在引擎其他模块使用时就能得心用手了。除了内存分配,还有资源的分配,这个也是非常重要的,比如Unity对资源的分配采用了托管堆的方式,我们的资源管理底层采用的是组合的方式是怎样的呢?
资源管理是引擎的另一个心脏模块,游戏中大场景地形的加载,模型的加载,骨骼动画的加载,图片的加载等等。场景资源加载不好也会影响内存管理的。Unity采用的是AssetBundle的方式,如下图所示:
上图是Unity关于内存的加载卸载处理操作,相信使用过Unity的人都清楚,我们在编写自己引擎的资源管理时,也会相应的研究一下其他引擎是如何做资源管理的,借鉴一下。回过头来再看看我们自己的设计,我们使用了三个模块进行资源的管理:Resource,ResourceBase,ResourceManager,其中ResourceManager是对外提供的接口模块,我们资源管理没有采用复杂的架构设计,先看看Resource的代码设计,如下代码所示:
Resource(const Path& path, ResourceManagerBase& resource_manager, IAllocator& allocator);
virtual ~Resource();
virtual void onBeforeReady() {}
virtual void onBeforeEmpty() {}
virtual void unload() = 0;
virtual bool load(FS::IFile& file) = 0;
void onCreated(State state);
void doUnload();
void addDependency(Resource& dependent_resource);
void removeDependency(Resource& dependent_resource);
为了资源的统一管理,我们在ResourceBase类中定义了资源列表:
typedef HashMap<u32, Resource*> ResourceTable;
我们的资源会通过资源列表统一管理的,再看看我们ResourceBase类中的代码:
void create(ResourceType type, ResourceManager& owner);
void destroy();
void setLoadHook(LoadHook& load_hook);
void enableUnload(bool enable);
Resource* load(const Path& path);
void load(Resource& resource);
void removeUnreferenced();
void unload(const Path& path);
void unload(Resource& resource);
void reload(const Path& path);
void reload(Resource& resource);
ResourceTable& getResourceTable() { return m_resources; }
explicit ResourceManagerBase(IAllocator& allocator);
virtual ~ResourceManagerBase();
ResourceManager& getOwner() const { return *m_owner; }
根据我们的思路,我们再看看ResourceManager类,同样定义了一个哈希表diamagnetic:
typedef HashMap<u32, ResourceManagerBase*> ResourceManagerTable;
是管理ResourceBase的,我们的ResourceManager管理类同样简单:
explicit ResourceManager(IAllocator& allocator);
~ResourceManager();
void create(FS::FileSystem& fs);
void destroy();
IAllocator& getAllocator() { return m_allocator; }
ResourceManagerBase* get(ResourceType type);
const ResourceManagerTable& getAll() const { return m_resource_managers; }
void add(ResourceType type, ResourceManagerBase* rm);
void remove(ResourceType type);
void reload(const Path& path);
void removeUnreferenced();
void enableUnload(bool enable);
大家知道Unity引擎对资源做了实例化以及AssetBundle的处理,我们的引擎在资源打包这块也会做实例化操作,如下所示:
Resource* createResource(const Path& path) override
{
return LUMIX_NEW(m_allocator, PrefabResource)(path, *this, m_allocator);
}
void destroyResource(Resource& resource) override
{
return LUMIX_DELETE(m_allocator, &static_cast(resource));
}
#define LUMIX_NEW(allocator, ...) new (Lumix::NewPlaceholder(), (allocator).allocate_aligned(sizeof(__VA_ARGS__), ALIGN_OF(__VA_ARGS__))) __VA_ARGS__
#define LUMIX_DELETE(allocator, var) (allocator).deleteObject(var);
以上三个类就实现了我们的资源管理,资源管理设计的好还是坏,需要我们的Demo验证,前期我们先基于 底层的封装,封装代码还是比较枯燥的,写代码也要耐得住寂寞。作为想从事编程以及引擎的开发者更需要这方面的功力历练才能成为编程高手。