I’m building a video game engine using C++ as the coding language and Vulkan for graphics. I am trying to set up a generic renderer using Vulkan that is flexible and will render objects based on a vector that is supplied to it. The renderer will also handle the creation of the window using GLFW and use GLM for all relevant math calls. I am using the ASSIMP library to load 3d models and animations.

The basic class structure is as follows:

1. Core:

- class Engine
- This is the main class that will initialize, run, and shut down other subsystems of the engine (e.g., Window, Renderer, Input, etc.)

2. Window:

- class Window
- This class will handle window creation, resizing, and destruction using GLFW. It should also handle keyboard and mouse input events.

3. Renderer:

- class Renderer
- This class will manage Vulkan objects (e.g., instance, device, swapchain, command buffers) and rendering pipelines.
- class Shader
- To handle shader modules creation and destruction.
- class Texture
- To handle texture loading and destruction.
- class Pipeline
- To encapsulate the description and creation of a Vulkan graphics or compute pipeline.

4. Scene:

- class Scene
- This class will manage the list of objects in the vector, update their transforms, and send them to the Renderer for rendering.
- class GameObject
- To represent individual objects in the scene, store their transformation matrices, and bind the relevant data like vertices and indices.
- class Camera
- To store and manage camera properties such as position, rotation, view, and projection matrices.
- class Mesh
- To handle mesh data (e.g., vertices, indices, etc.) for individual objects.
- class Material
- To store and manage material properties such as colors, shaders, and textures.

5. Math:

- Utilize GLM library to handle vector and matrix math operations.


Here is some of the relevant header and source file code:

Engine.h:

#pragma once

#include "Window.h"
#include "Renderer.h"
#include "Scene.h"

class Engine
{
public:
	Engine();
	~Engine();

	void Run();
	void Shutdown();

private:

	void Initialize();
	void MainLoop();
	void Update(float deltaTime);
	void Render();

	Window window;
	Renderer renderer;
	Scene scene;
};

Engine.cpp:

#include "Engine.h"
#include "Terrain.h"
#include <iostream>

Engine::Engine()
{
	Initialize();
}

Engine::~Engine()
{
	Shutdown();
}

void Engine::Run()
{
	MainLoop();
}

void Engine::Initialize()
{
	// Initialize window, renderer, and scene
	window.Initialize();
	renderer.Initialize(window.GetWindow());
	scene.Initialize();

	VkDescriptorSetLayout descriptorSetLayout = renderer.CreateDescriptorSetLayout();
	VkDescriptorPool descriptorPool = renderer.CreateDescriptorPool(1); // Assuming only one terrain object

	Terrain terrain(0,10,1,renderer.GetDevice(), renderer.GetPhysicalDevice(), renderer.GetCommandPool(), renderer.GetGraphicsQueue());
	terrain.GenerateTerrain(descriptorSetLayout, descriptorPool);

	scene.AddGameObject(terrain.GetTerrainObject());
}

void Engine::MainLoop()
{
	while (!window.ShouldClose())
	{
		window.PollEvents();

		float deltaTime = window.GetDeltaTime();
		Update(deltaTime);
		Render();
	}
}

void Engine::Update(float deltaTime)
{
	scene.Update(deltaTime);
}

void Engine::Render()
{
	renderer.BeginFrame();
	scene.Render(renderer);
	renderer.EndFrame();
}

void Engine::Shutdown()
{
	// Clean up resources in reverse order
	scene.Shutdown();
	renderer.Shutdown();
	window.Shutdown();
}

Scene.h:

#pragma once

#include <vector>
#include "GameObject.h"
#include "Camera.h"
#include "Renderer.h"

class Scene
{
public:
	Scene();
	~Scene();

	void Initialize();
	void Update(float deltaTime);
	void Render(Renderer& renderer);
	void Shutdown();

	void AddGameObject(GameObject* gameObject);
	Camera& GetCamera();

private:
	std::vector<GameObject*> gameObjects;
	Camera camera;
};

Scene.cpp:

#include "Scene.h"

Scene::Scene()
{
}

Scene::~Scene()
{
	Shutdown();
}

void Scene::Initialize()
{
	// Initialize camera and game objects
	camera.SetPosition(glm::vec3(0.0f, 0.0f, -5.0f));

	// Add initial game objects
}

void Scene::Update(float deltaTime)
{
	// Update game objects and camera
	for (GameObject* gameObject : gameObjects)
	{
		gameObject->Update(deltaTime);
	}
}

void Scene::Render(Renderer& renderer)
{
	// Render game objects
	for (GameObject* gameObject : gameObjects)
	{
		gameObject->Render(renderer, camera);
	}

	// Submit rendering-related commands to Vulkan queues
}

void Scene::Shutdown()
{
	// Clean up game objects
	for (GameObject* gameObject : gameObjects)
	{
		delete gameObject;
		gameObject = nullptr;
	}
	gameObjects.clear();
	camera.Shutdown();
}

void Scene::AddGameObject(GameObject* gameObject)
{
	gameObjects.push_back(gameObject);
}

Camera& Scene::GetCamera()
{
	return camera;
}

GameObject.h:

#pragma once

#include <glm/glm.hpp>
#include "Mesh.h"
#include "Material.h"
#include "Camera.h"
#include "Renderer.h"

class GameObject
{
public:
	GameObject();
	~GameObject();

	void Initialize(const Mesh& mesh, const Material& material);
	void Update(float deltaTime);
	void Render(Renderer& renderer, const Camera& camera);
	void Shutdown();

	void SetPosition(const glm::vec3& position);
	void SetRotation(const glm::vec3& rotation);
	void SetScale(const glm::vec3& scale);

private:
	glm::mat4 modelMatrix;
	glm::vec3 position;
	glm::vec3 rotation;
	glm::vec3 scale;

	Mesh mesh;
	Material material;

	bool initialized = false;

	void UpdateModelMatrix();
};

GameObject.cpp:

#include "GameObject.h"
#include <glm/gtc/matrix_transform.hpp>

GameObject::GameObject()
	: position(0.0f), rotation(0.0f), scale(1.0f)
{
}

GameObject::~GameObject()
{
    if (initialized)
    {
        Shutdown();
    }
}

void GameObject::Initialize(const Mesh& mesh, const Material& material)
{
	this->mesh = mesh; // copy or share mesh data
	this->material = material; // copy or share material data
    this->initialized = true;
}

void GameObject::Update(float deltaTime)
{
	// Update position, rotation, scale, and other properties
	// Example: Rotate the object around the Y-axis
	rotation.y += deltaTime * glm::radians(90.0f);
	UpdateModelMatrix();
}

void GameObject::Render(Renderer& renderer, const Camera& camera)
{
    // Render this object using the renderer and camera
    VkDevice device = *renderer.GetDevice();

    // Bind mesh vertex and index buffers
    VkBuffer vertexBuffers[] = { mesh.GetVertexBuffer() };
    VkDeviceSize offsets[] = { 0 };
    vkCmdBindVertexBuffers(*renderer.GetCurrentCommandBuffer(), 0, 1, vertexBuffers, offsets);
    vkCmdBindIndexBuffer(*renderer.GetCurrentCommandBuffer(), mesh.GetIndexBuffer(), 0, VK_INDEX_TYPE_UINT32);

    // Update shader uniform buffers with modelMatrix, viewMatrix and projectionMatrix transforms
    struct MVP {
        glm::mat4 model;
        glm::mat4 view;
        glm::mat4 projection;
    } mvp;

    mvp.model = modelMatrix;
    mvp.view = camera.GetViewMatrix();
    mvp.projection = camera.GetProjectionMatrix();

    // Create a new buffer to hold the MVP data temporarily
    VkBuffer mvpBuffer;
    VkDeviceMemory mvpBufferMemory;
    BufferUtils::CreateBuffer(device, *renderer.GetPhysicalDevice(),
        sizeof(MVP), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        mvpBuffer, mvpBufferMemory);

    // Map the MVP data into the buffer and unmap
    void* data = nullptr;
    vkMapMemory(device, mvpBufferMemory, 0, sizeof(MVP), 0, &data);
    memcpy(data, &mvp, sizeof(MVP));
    vkUnmapMemory(device, mvpBufferMemory);

    // TODO: Modify your material, descriptor set, and pipeline to use this new mvpBuffer instead of
    // the default uniform buffer

    // Bind the DescriptorSet associated with the material
    VkDescriptorSet descriptorSet = material.GetDescriptorSet();
    vkCmdBindDescriptorSets(*renderer.GetCurrentCommandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, material.GetPipelineLayout(), 0, 1, &descriptorSet, 0, nullptr);

    // Call vkCmdDrawIndexed()
    uint32_t numIndices = static_cast<uint32_t>(mesh.GetIndices().size());
    vkCmdDrawIndexed(*renderer.GetCurrentCommandBuffer(), numIndices, 1, 0, 0, 0);

    // Cleanup the temporary buffer
    vkDestroyBuffer(device, mvpBuffer, nullptr);
    vkFreeMemory(device, mvpBufferMemory, nullptr);
}

void GameObject::Shutdown()
{
	// Clean up resources, if necessary
	// (depending on how Mesh and Material resources are managed)
    this->initialized = false;
}

void GameObject::SetPosition(const glm::vec3& position)
{
	this->position = position;
	UpdateModelMatrix();
}

void GameObject::SetRotation(const glm::vec3& rotation)
{
	this->rotation = rotation;
	UpdateModelMatrix();
}

void GameObject::SetScale(const glm::vec3& scale)
{
	this->scale = scale;
	UpdateModelMatrix();
}

void GameObject::UpdateModelMatrix()
{
	modelMatrix = glm::mat4(1.0f);
	modelMatrix = glm::translate(modelMatrix, position);
	modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
	modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
	modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
	modelMatrix = glm::scale(modelMatrix, scale);
}

Terrain.h:

#pragma once

#include "Simplex.h"
#include "Mesh.h"
#include "Material.h"
#include "GameObject.h"

class Terrain {
public:
    Terrain(int seed, int worldSize, float scale, VkDevice* device, VkPhysicalDevice* physicalDevice, VkCommandPool* commandPool, VkQueue* graphicsQueue);
    ~Terrain();

    void GenerateTerrain(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool);
    GameObject* GetTerrainObject();

private:
    void GenerateHeightMap();
    void ConvertHeightMapToMeshData();

    int seed;
    int worldSize;
    float scale;

    VkDevice* device;
    VkPhysicalDevice* physicalDevice;
    VkCommandPool* commandPool;
    VkQueue* graphicsQueue;

    SimplexNoise noise;
    std::vector<std::vector<float>> heightMap;
    std::vector<Vertex> vertices;
    std::vector<uint32_t> indices;

    Mesh terrainMesh;
    Material terrainMaterial;
    GameObject terrainObject;
};

Terrain.cpp:

#include "Terrain.h"

Terrain::Terrain(int seed, int worldSize, float scale, VkDevice* device, VkPhysicalDevice* physicalDevice, VkCommandPool* commandPool, VkQueue* graphicsQueue)
    : seed(seed), worldSize(worldSize), scale(scale), noise(seed), device(device), physicalDevice(physicalDevice), commandPool(commandPool), graphicsQueue(graphicsQueue) {
}

Terrain::~Terrain() {
    // Cleanup any resources associated with the Terrain class
    //terrainMesh.Cleanup();
    //terrainMaterial.Cleanup();
}

void Terrain::GenerateHeightMap() {
    heightMap = std::vector<std::vector<float>>(worldSize, std::vector<float>(worldSize, 0.0f));

    for (int x = 0; x < worldSize; ++x) {
        for (int z = 0; z < worldSize; ++z) {
            float noiseValue = noise.noise(x * scale, z * scale);
            heightMap[x][z] = noiseValue;
        }
    }
}

void Terrain::ConvertHeightMapToMeshData()
{
    vertices.clear();
    indices.clear();

    // Generate vertices
    for (int x = 0; x < worldSize; ++x)
    {
        for (int z = 0; z < worldSize; ++z)
        {
            Vertex vertex;
            vertex.position = glm::vec3(static_cast<float>(x), heightMap[x][z], static_cast<float>(z));

            // Set color based on height, feel free to customize this as desired
            float color = heightMap[x][z] * 0.5f + 0.5f; // Normalize the height value between 0 and 1
            vertex.color = glm::vec3(color, color, color);

            vertices.push_back(vertex);
        }
    }

    // Generate indices for rendering using triangle strips
    for (int x = 0; x < worldSize - 1; ++x)
    {
        for (int z = 0; z < worldSize - 1; ++z)
        {
            int topLeft = x * worldSize + z;
            int topRight = topLeft + 1;
            int bottomLeft = (x + 1) * worldSize + z;
            int bottomRight = bottomLeft + 1;

            // First triangle
            indices.push_back(topLeft);
            indices.push_back(bottomRight);
            indices.push_back(bottomLeft);

            // Second triangle
            indices.push_back(topLeft);
            indices.push_back(topRight);
            indices.push_back(bottomRight);
        }
    }
}

void Terrain::GenerateTerrain(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool)
{
    GenerateHeightMap();
    ConvertHeightMapToMeshData();
    terrainMesh.Initialize(vertices, indices, *device, *physicalDevice, *commandPool, *graphicsQueue);

    // Initialize terrainMaterial with the loaded shaders and texture
    // Assume descriptorSetLayout and descriptorPool are created before this call
    terrainMaterial.Initialize("C:/shaders/vert.spv", "C:/shaders/frag.spv", "C:/textures/texture.jpg", *device, descriptorSetLayout, descriptorPool, *physicalDevice, *commandPool, *graphicsQueue);

    terrainObject.Initialize(terrainMesh, terrainMaterial);
}

GameObject* Terrain::GetTerrainObject() {
    return &terrainObject;
}

Material.h:

#pragma once

#include <vulkan/vulkan.h>
#include "Texture.h"
#include "Shader.h"
#include <stdexcept>
#include <memory> // Don’t forget to include <memory>

class Material
{
public:
    Material();
    ~Material();

    void Initialize(const std::string& vertShaderPath, const std::string& fragShaderPath, const std::string& texturePath, VkDevice device, VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
    void Cleanup();

    void LoadTexture(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
    void LoadShaders(const std::string& vertFilename, const std::string& fragFilename, VkDevice device);

    VkDescriptorSet GetDescriptorSet() const;
    VkPipelineLayout GetPipelineLayout() const;

private:
    VkDevice device;
    std::shared_ptr <Shader> vertexShader;
    std::shared_ptr <Shader> fragmentShader;
    std::shared_ptr<Texture> texture;

    void CreateDescriptorSet(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool);
    void CreatePipelineLayout(VkDescriptorSetLayout descriptorSetLayout);

    VkDescriptorSet descriptorSet;
    VkPipelineLayout pipelineLayout;
};

Material.cpp:

#include "Material.h"

Material::Material()
    : device(VK_NULL_HANDLE), descriptorSet(VK_NULL_HANDLE), pipelineLayout(VK_NULL_HANDLE)
{
}

Material::~Material()
{
    Cleanup();
}

void Material::Initialize(const std::string& vertShaderPath, const std::string& fragShaderPath, const std::string& texturePath, VkDevice device, VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
    this->device = device;

    // Load shaders and texture
    LoadTexture(texturePath, device, physicalDevice, commandPool, graphicsQueue);
    LoadShaders(vertShaderPath, fragShaderPath, device);

    // Create descriptor set and pipeline layout
    CreateDescriptorSet(descriptorSetLayout, descriptorPool);
    CreatePipelineLayout(descriptorSetLayout);
}

void Material::CreateDescriptorSet(VkDescriptorSetLayout descriptorSetLayout, VkDescriptorPool descriptorPool)
{
    VkDescriptorSetAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
    allocInfo.descriptorPool = descriptorPool;
    allocInfo.descriptorSetCount = 1;
    allocInfo.pSetLayouts = &descriptorSetLayout;

    if (vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet) != VK_SUCCESS) {
        throw std::runtime_error("Failed to allocate descriptor sets!");
    }

    VkDescriptorImageInfo imageInfo{};
    imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    imageInfo.imageView = texture->GetImageView();
    imageInfo.sampler = texture->GetSampler();

    VkWriteDescriptorSet descriptorWrite{};
    descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    descriptorWrite.dstSet = descriptorSet;
    descriptorWrite.dstBinding = 0;
    descriptorWrite.dstArrayElement = 0;
    descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
    descriptorWrite.descriptorCount = 1;
    descriptorWrite.pImageInfo = &imageInfo;

    vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
}

void Material::CreatePipelineLayout(VkDescriptorSetLayout descriptorSetLayout)
{
    VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
    pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    pipelineLayoutInfo.setLayoutCount = 1;
    pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;

    if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) {
        throw std::runtime_error("Failed to create pipeline layout!");
    }
}

void Material::Cleanup()
{
	// Clean up resources, if necessary
	// (depending on how Shader and Texture resources are managed)
}

VkDescriptorSet Material::GetDescriptorSet() const
{
	return descriptorSet;
}

VkPipelineLayout Material::GetPipelineLayout() const
{
	return pipelineLayout;
}

void Material::LoadTexture(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	texture = std::shared_ptr<Texture>(new Texture{}, Texture::Cleanup); // Create a new Texture using shared_ptr
	texture->LoadFromFile(filename, device, physicalDevice, commandPool, graphicsQueue);
}

void Material::LoadShaders(const std::string& vertFilename, const std::string& fragFilename, VkDevice device)
{
	vertexShader = std::shared_ptr<Shader>(new Shader{}, Shader::Cleanup);
	fragmentShader = std::shared_ptr<Shader>(new Shader{}, Shader::Cleanup);
	vertexShader->LoadFromFile(vertFilename, device, VK_SHADER_STAGE_VERTEX_BIT);
	fragmentShader->LoadFromFile(fragFilename, device, VK_SHADER_STAGE_FRAGMENT_BIT);
}

Texture.h:

#pragma once

#include <vulkan/vulkan.h>

#include "stb_image.h" // Include the stb_image header
#include "BufferUtils.h"
#include <string>

class Texture
{
public:
	Texture();
	~Texture();

	void LoadFromFile(const std::string& filename, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);

	VkImageView GetImageView() const;
	VkSampler GetSampler() const;
	static void Cleanup(Texture* texture);

private:
	VkDevice device;
	VkImage image;
	VkDeviceMemory imageMemory;
	VkImageView imageView;
	VkSampler sampler;
	VkPhysicalDevice physicalDevice;
	VkCommandPool commandPool;
	VkQueue graphicsQueue;
	bool initialized = false;

	void CreateImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties);
	void TransitionImageLayout(VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels);
	void CreateImageView(VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels);
	void CreateSampler(uint32_t mipLevels);
	void CopyBufferToImage(VkBuffer buffer, uint32_t width, uint32_t height);
	

	// Additional helper functions for texture loading…
};

Mesh.h:

#pragma once

#include <vector>
#include <vulkan/vulkan.h>
#include <glm/glm.hpp>
#include "BufferUtils.h"

struct Vertex
{
	glm::vec3 position;
	glm::vec3 color;
};

class Mesh
{
public:
	Mesh();
	~Mesh();

	void Initialize(std::vector<Vertex> vertices, std::vector<uint32_t> indices, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
	void Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue);
	void Cleanup();

	const std::vector<Vertex>& GetVertices() const;
	const std::vector<uint32_t>& GetIndices() const;
	VkBuffer GetVertexBuffer() const;
	VkBuffer GetIndexBuffer() const;

	void SetVertices(const std::vector<Vertex>& vertices);
	void SetIndices(const std::vector<uint32_t>& indices);

private:
	VkDevice device;
	std::vector<Vertex> vertices;
	std::vector<uint32_t> indices;

	VkBuffer vertexBuffer;
	VkDeviceMemory vertexBufferMemory;
	VkBuffer indexBuffer;
	VkDeviceMemory indexBufferMemory;
};

Mesh.cpp:

#include "Mesh.h"

Mesh::Mesh()
	: device(VK_NULL_HANDLE), vertexBuffer(VK_NULL_HANDLE), vertexBufferMemory(VK_NULL_HANDLE), indexBuffer(VK_NULL_HANDLE), indexBufferMemory(VK_NULL_HANDLE)
{
}

Mesh::~Mesh()
{
	Cleanup();
}

void Mesh::Initialize(std::vector<Vertex> vertices, std::vector<uint32_t> indices, VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	this->vertices = vertices;
	this->indices = indices;
	this->device = device;

	Initialize(device, physicalDevice, commandPool, graphicsQueue);
	// Create vertex buffer and index buffer
	// (assuming you have helper functions CreateBuffer and CopyBuffer)
	// …
}

void Mesh::Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkCommandPool commandPool, VkQueue graphicsQueue)
{
	this->device = device;

	// Create vertex buffer and index buffer
	// (assuming you have helper functions CreateBuffer and CopyBuffer)
	// …

	// Declare and initialize stagingBuffer and bufferSize here

	VkBuffer stagingBuffer;
	VkDeviceMemory stagingBufferMemory;
	VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size();

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
		stagingBuffer, stagingBufferMemory);

	void* data;
	vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data);
	memcpy(data, vertices.data(), (size_t)bufferSize);
	vkUnmapMemory(device, stagingBufferMemory);

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		vertexBuffer, vertexBufferMemory);

	BufferUtils::CopyBuffer(device, commandPool, graphicsQueue, stagingBuffer, vertexBuffer, bufferSize);

	vkDestroyBuffer(device, stagingBuffer, nullptr);
	vkFreeMemory(device, stagingBufferMemory, nullptr);

	bufferSize = sizeof(indices[0]) * indices.size();
	VkBuffer stagingIndexBuffer;
	VkDeviceMemory stagingIndexBufferMemory;

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
		stagingIndexBuffer, stagingIndexBufferMemory);

	vkMapMemory(device, stagingIndexBufferMemory, 0, bufferSize, 0, &data);
	memcpy(data, indices.data(), (size_t)bufferSize);
	vkUnmapMemory(device, stagingIndexBufferMemory);

	BufferUtils::CreateBuffer(device, physicalDevice, bufferSize,
		VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		indexBuffer, indexBufferMemory);

	BufferUtils::CopyBuffer(device, commandPool, graphicsQueue, stagingIndexBuffer, indexBuffer, bufferSize);

	vkDestroyBuffer(device, stagingIndexBuffer, nullptr);
	vkFreeMemory(device, stagingIndexBufferMemory, nullptr);
}

void Mesh::Cleanup()
{
	if (device != VK_NULL_HANDLE)
	{
		if (vertexBuffer != VK_NULL_HANDLE)
		{
			vkDestroyBuffer(device, vertexBuffer, nullptr);
			vkFreeMemory(device, vertexBufferMemory, nullptr);
			vertexBuffer = VK_NULL_HANDLE;
			vertexBufferMemory = VK_NULL_HANDLE;
		}

		if (indexBuffer != VK_NULL_HANDLE)
		{
			vkDestroyBuffer(device, indexBuffer, nullptr);
			vkFreeMemory(device, indexBufferMemory, nullptr);
			indexBuffer = VK_NULL_HANDLE;
			indexBufferMemory = VK_NULL_HANDLE;
		}
	}
}

const std::vector<Vertex>& Mesh::GetVertices() const
{
	return vertices;
}

const std::vector<uint32_t>& Mesh::GetIndices() const
{
	return indices;
}

VkBuffer Mesh::GetVertexBuffer() const
{
	return vertexBuffer;
}

VkBuffer Mesh::GetIndexBuffer() const
{
	return indexBuffer;
}
void Mesh::SetVertices(const std::vector<Vertex>& vertices)
{
	this->vertices = vertices;
}

void Mesh::SetIndices(const std::vector<uint32_t>& indices)
{
	this->indices = indices;
}

I want to modify the code to make use Mesh objects as pointers instead of instances where possible. I'm hoping this will improve resource management. How can I modify the code to achieve this?