Hazel Documentation

Hazel is a real-time interactive application and rendering engine created by Studio Cherno and community volunteers. Check out the main website for more information about Hazel.

This site will continue to grow and house the documentation for using Hazel. Check out the Getting Started page for information on how to build and run Hazel.

Getting Started

Requirements

One of our goals is to make Hazel as easy as possible to build - if you're having any difficulties or weird errors, please let us know. We currently only support building on Windows 10 and Windows 11 with Visual Studio 2022, Visual Studio 2019 is no longer supported. The minimum supported version of Visual Studio 2022 is 17.2.0, Hazel may not compile on versions before that. You also need the following installed:

Here you'll find a list of all the third-party tools and SDKs that you'll need to install in order to build Hazel:

Make sure that you add everything except for the .NET Framework SDK to your PATH environment variable. Most likely the installers will give you the option to so automatically.

Building and Running

Assuming that's all installed and ready to go, you can proceed with the following:

  1. Clone the repository: git clone --recursive https://gitlab.com/ChernoProjects/Hazel-dev.git
  2. Run Scripts/Setup.bat - this will download required libraries and make sure everything is setup correctly
  3. Open Hazel.sln and build either Debug or Release x64 - Hazelnut should be the startup project so really you can just hit F5 to build and debug the startup project. By default, this will load the Sandbox project found in Hazelnut/SandboxProject/Sandbox.hproj
  4. Open Hazelnut/SandboxProject/Sandbox.sln and build in either Debug or Release. This will build the C# module of the Sandbox project which is necessary to properly play the various scenes in the project.

.NET Framework

Hazel makes use of C# as a scripting language, and because of that we also provide a "Hazel-ScriptCore" project, which contains Hazels C# API. This however means that in order to build Hazel you need to have the .NET Framework SDK installed. Hazel makes use of .NET Framework 4.7.2, and all projects that are made in Hazel also use that specific version.

If you're using Visual Studio to build Hazel you'll have to open the Visual Studio Installer program, and make sure you've selected the ".NET desktop development" workload from the "Workloads" tab, you can find an example of this in the image below.

DotNETSDKInstallation

You may be required to restart your computer after installing the workload.

Vulkan

Hazel requires Vulkan SDK 1.3.204.1 to be installed, and the VULKAN_SDK environment variable set to your installation path. If you do not have the correct version installed, the Setup script should offer to download and install the correct version for you.

The Vulkan SDK installer now offers to download and install shader debug libraries - you must install these libraries if you would like to build Hazel in the Debug configuration. To do so, simply check the (Optional) Debuggable Shader API Libraries - 64 bit option in the Select Components part of the installer, as seen in the image below.

VulkanSDKInstaller

You can also download and install the Vulkan SDK manually if you wish, or if the Setup scripts have failed.

Pulling the latest code

The master branch is required to always be stable, so there should never be any build errors or major faults. Once you've pulled the latest code, remember to run Scripts/Setup.bat again to make sure that any new files or configuration changes will be applied to your local environment.

Next Steps

Probably an overview of Hazelnut and the basic architecture.

Supported Platforms

Here you can find a list of the platforms that Hazel currently supports.

If you can't find the platform you're looking for on this page you should assume that Hazel does not currently, and most likely never will, support that platform.

Fully Supported

These are the platforms that Hazel has been tested on, and we've determined that Hazel should run without any serious problems.

If you find an issue relating to any of these platforms please open an issue here: https://gitlab.com/chernoprojects/Hazel-dev/-/issues

  • Windows 10 (64-bit)
  • Windows 11 (64-bit)

NOTE: Hazel currently only considers 64-bit Windows versions supported, and it's unlikely that 32-bit support will ever be added

Unsupported Platforms

These are platforms that Hazel most likely won't work on.

  • Windows 7
  • MacOS
  • Linux Based Platforms (May eventually be supported)

Supported Editors + Toolchains

Hazel will in theory support any IDE or toolchain that https://premake.github.io/docs/Using-Premake#using-premake-to-generate-project-files supports, however we've only tested the toolchains/IDEs listed below.

  • Visual Studio 2022
  • Visual Studio 2019 (No longer supported)
  • CodeLite

Developer Guide

This document is for Hazel engineers working on the engine.

Etiquette

The following rules apply to all engineers working on the engine codebase. The motivation behind these is to support a positive collaborative environment, assist in producing a higher quality product for developers and end-users, and keep the code itself as high quality as possible.

1. Code Ownership

Being a Game Engine, Hazel naturally divides itself into many various discrete systems which also need to work together well. Typically, these various systems will have owners - engineers who are responsible for a particular system or section of code, who are usually (but not always) the original authors. There may also be some overlap or multiple owners for certain systems. Usually ownership is implicit upon an engineer writing code; that is to say that an engineer automatically becomes the owner of code that they write (especially true for systems they've designed and implemented). There can always be exceptions to this of course, which falls onto the responsibility of the project's technical directors.

As the name implies, owners are responsible for code which they own. Whenever engineers need to stray into code territory that they do not own, and determine that this foreign code needs to be modified, it's important that certain processes are followed. These processes can be summarized by just a few points:

  • Do not modify any code that you do not own unless there is an important reason to (eg. you need to extend a system to accept new input from a feature you're working on). Needlessly changing code is a problem because whenever anything changes, there could be undesirable side-effects which you couldn't possibly even envision - because you didn't write the code!
  • If you've determined that you do in fact have a good reason to change code, contact the owner and state your intentions. This is important because the owner responsible for the code will know more about it (and all of its implications, "load-bearing" properties, etc.) than you, and this collaboration can ensure that what you're doing is correct and minimizes any possible negative effects from your changes. It's also just polite to give the owner a heads up, because after all it is their code that you're coming in and changing.
  • If there is an emergency - there is a critical bug in the engine that you can fix ASAP even though you don't own the code, contact your Technical Director and inform them of this change.

Just to be clear - you can change code that you own without needing to notify anyone, and ultimately for whatever reason. The burden of reponsibility falls on you in that case, because after all you are the owner of the code you're modifying.

2. Self-Documenting Code

One of Hazel's major engineering goals is to promote easy-to-read, good, clean code. This is a very subjective description, however the idea is that your code should be as readable as possible, even to "junior" engineers. Of course there are areas that simply cannot be simplified, or by doing so would affect the engine negatively, but in all areas this idea of clear, simple, and self-documenting code should be followed. This is more thoroughly explained in the Code Style section with specific examples, and includes things like naming symbols descriptively, preferring verbose descriptive code to auto-generated templates or functional style programming which produces more abstract engineer-facing code that can be harder to understand and see the full extent of computation taking place.

Your code should ideally be able to read like an English book as much as possible. If you can make your function make more sense by expanding variable names or adding extra comments - do that!

Code Style

By now Hazel shouldn't really need a Code Style guide, because there is enough existing code in the codebase to answer every stylistic question you might have. Long story short - if you don't know what style to write your code in, look around! There's plenty of code, and your goal is to blend in. Nevertheless, I will try and officially summarize the main points here.

Naming

Classes, functions, member functions, enums, namespaces, source code file names, and most other "titles" are always in Pascal Case (like the Win32 API and Microsoft's C# code style).

Local variables (including parameters) are always camel case. Private member variables are prefixed with m_ and then start with a capital letter (eg. m_VariableName). Static variables (either in a translation unit or class) are prefixed with an s_ and also start with a capital letter (eg. s_StaticVariable).

Here is an example which demonstrates all of these points:


namespace MyNamespace {

    static int s_StaticInt = 0;

    static void MyFunction();

    class ExampleClass
    {
    public:
        void ThisIsMyFunction(int inputVariable)
        {
            m_MemberVariable = inputVariable;

            std::string localVariable = std::to_string(inputVariable);
        }
    private:
        int m_MemberVariable = 0;
    };

}

To be continued...

C# API Structure

This page will simply explain the general structure of the C# API, what features / systems belong where, what files are relevant to what topic, etc...

Hazel-ScriptCore Folder Structure

All the C# source files belong in Hazel-ScriptCore/Source, no core .cs files should ever exist outside this folder.

Attributes/

This folder isn't too useful for extending just the C# API, it's more useful if you want to extend the Hazelnut Editor. But in short this folder should contain any custom C# attributes that we want to add.

Audio/

Contains all the Audio data / utility classes, but NOT the AudioComponent

Core/

This folder contains some useful core classes, e.g Application.cs, Input.cs and Log.cs. These classes are usually implemented as static for simplicity and ease-of-use.

This folder also contains one of the most important files you'll need for extending the C# API: InternalCalls.cs. Which is the file that contains all methods that we need to implement in C++.

Math/

Contains all the math classes, you'll rarely need to add new files to this folder, but you may want to add methods to the classes that are already defined in those files.

Mathf.cs is where you'd put general math utility methods like Abs, Min, Max, Lerp etc...

Physics/

Contains all Physics data and utility classes / structs, it's the folder where all the collider data classes are defined, it also contains the static Physics class that contains useful physics related methods like Raycast, Overlap methods, etc...

Renderer/

This folder contains all the renderer / graphics related classes like Mesh, StaticMesh, and Material, usually you won't have to extend these classes in any way.

Scene/

This is the main folder that you'll be interested in if you want to extend the core C# API, it contains the Components.cs, Entity.cs and Scene.cs.

The Components.cs file is where you'll modify existing components, and add new components when you want to expose them from C++. I'll provide an example later on that shows roughly how to expose or extend components.

The Entity.cs file contains the Hazel Entity class, which all (or most) game scripts will be sub classed from, you'll very rarely have to modify this file, but if you have to keep in mind that most of the modifications you want to make can probably be done through a component instead. And please try not to break backwards compatibility when you change this file, we want to avoid breaking game scripts as much as possible.

The Scene.cs will most likely not be used by game scripts directly too much, it contains methods related to interacting with the scene, spawning prefabs, creating and destroying entities, finding entities by ID or by name. Most game scripts will be interacting with the equivalent methods in Entity.cs instead of interacting with the Scene class directly.

Adding Internal Calls (Interop with C# and C++)

You should only add an internal call if you want C# code to call some function in C++, you might want this if you want the added performance of C++, or because you need to interface with, or expose a C++ API to C#.

I'll start by providing guidelines for adding internal calls, please make sure that you follow these guidelines, they're here to make it far easier for others to understand and maintain code that you write, and for the sake of consistency.

Guidelines for writing internal calls in C#

When you add the internal calls to the C# API here are some things you need to consider:

  1. Internal methods should always be defined inside the InternalCalls.cs file. Never in the classes that use these methods.
  2. Internal methods should always be defined inside of a #region block to keep the file organized. You can take a look at how other regions are written.
  3. Internal methods should always be named in this format: ClassName_FunctionName, e.g TransformComponent_GetTranslation, or MeshComponent_GetMaterial.
  4. Internal methods should always be defined as internal static extern
  5. Internal methods should always have the same name in C++ as in C#, so a method called TransformComponent_GetTranslation should also be called that in C++.
  6. Internal methods must be marked with this attribute: [MethodImpl(MethodImplOptions.InternalCall)]
  7. If you need to pass primitive types (numerical types, bool, char or object) to an internal call they should be copied, or passed as out, not passed as ref.
  8. If you need an internal method to return a primitive type, it should either have that type as the return type, or it should be passed as an out parameter. E.g internal static extern bool, or internal static extern void SomeClass_SomeFunc(out bool result), use out if you need to return multiple parameters.
  9. If you need to pass a struct to the internal call you have to pass it as either ref or out depending on if the C++ function will read from, or write to the struct
  10. Passing a struct as ref means that the C++ function will only read from the struct, never write to it. It's your responsibility to follow this rule, Mono won't enforce it.
  11. Passing a struct as out means that the C++ function will read from or write to the struct
  12. Passing a struct as out or ref means that the C++ function will take in an equivalent C++ struct as a pointer
  13. Enums should NOT be passed as ref or out! They're just primitive types, they can be passed as is if there's a C++ equivalent enum, or they can be passed as int.

These are some of the basic things you need to keep in mind, feel free to let me know if you think these guidelines should be updated!

Guidelines for writing internal calls in C++

When you add the internal calls to the C++ API these are some things you need to keep in mind:

  1. The C++ implementation of an internal call should always be declared in the ScriptGlue.h file, and defined in ScripGlue.cpp.
  2. In order to register the internal call with the C++ API you have to add this line: HZ_ADD_INTERNAL_CALL(ClassName_FunctionName); to ScriptGlue::RegisterInternalCalls, preferably in the same order that it's declared in InternalCalls.cs.
  3. If an internal method as to log a message it should preferably log these messages to the Hazelnut console, e.g using HZ_CONSOLE_LOG_INFO. This isn't required though and mainly depends on what you're logging.
  4. Always add internal calls inside the InternalCalls namespace in C++. This is required in order to register the function

There's understandably a lot more to keep in mind, but the key point is to follow these guidelines, and to make sure the code you add is consistent with the code that's already in the C++ and C# API.

Example

Here's a very basic example of how to add an internal call to the scripting API, we'll start with declaring the internal call in C#:

Imagine we have a custom struct in C#, and we want to populate that struct with some data from C++, and have our internal call return true if it succeeds. Here's what struct might look like:

// You have to add this attribute to structs that you want to pass to C++
[StructLayout(LayoutKind.Sequential)]
public struct MyCustomData
{
	public float MyFloat;
	public int SomeInt;
}
// InternalCalls.cs

...

#region MyCustomComponent

[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern bool MyCustomComponent_GetCustomStruct(ulong entityID, out MyCustomData outData);

#endregion

...

Here you can see we define the internal call, it returns a bool, and takes in two parameters, the `entityID´ parameter is just for consistency here, all C# components have to pass the entity's ID to C++ so the engine can know what entity the component belongs to.

And it also takes our custom struct as an out parameter, this means that we expect C++ to write some data into this struct, but it can also read from it.

That's really all you need to do in order to define the method in C#. Then in C++ we'll add this code:

// MyCustomData struct (can be a class in C++ but use struct where possible)
struct MyCustomData
{
	float MyFloat;
	int SomeInt;
};

// ScriptGlue.h

namespace InternalCalls {

...

#pragma region MyCustomComponent

bool MyCustomComponent_GetCustomStruct(uint64_t entityID, MyCustomData* outData);

#pragma endregion

...
}

and then in ScriptGlue.cpp we add the implementation of this function:

// ScriptGlue.cpp

namespace InternalCalls {

...

#pragma region MyCustomComponent

bool MyCustomComponent_GetCustomStruct(uint64_t entityID, MyCustomData* outData)
{
	// NOTE: Dummy code, look at the existing internal calls for reference
	if (!is_entity_valid)
	{
		// Log some error here
		return false;
	}

	if (!entity_has_MyCustomComponent)
	{
		// Should almost never happen but log error if it does
		return false;
	}

	*outData = myCustomComponent.MyData;
	// OR:
	*outData.MyFloat = someFloatValue;
	*outData.SomeInt = someIntValue;
	return true;
}

#pragma endregion

...
}

This just covers the basics of adding internal calls, but it should give you an idea as to how it's done.

Exposing Components from C++ to C#

This page will tell you all the necessary steps you'll need to follow in order to expose a component from C++ to C#.

Here are the outline of the step you'll need to complete to expose a component to C#:

  1. Add HZ_REGISTER_MANAGED_COMPONENT(MyCustomComponent); to ScriptGlue::RegisterComponentTypes.
  2. Implement the C# component by defining a class in Components.cs and make it inherit from Component.
  3. Add the necessary internal calls to let the C# component interact with the C++ component and engine systems.

Please try to keep the HZ_REGISTER_MANAGED_COMPONENT in the same order as the the components internal calls, so if your internal calls are defined underneath e.g the RigidBodyComponent in ScriptGlue.h you should register your component and internal calls underneath the RigidBodyComponent in ScriptGlue::RegisterComponentTypes and ScriptGlue::RegisterInternalCalls. This helps keep the files organized.

The way you write a C# component highly depends on the component, but the basics are this:

  1. You need to add a class to Components.cs that inherits from Component, like this: public class MyComponent : Component.
  2. You need to add methods or properties that calls the necessary internal methods. (Refer to the previous page for info on how to do this)
  3. Any internal method that needs to interface with your component has to take in the ID of the entity, you can access the ID from within the component by accessing Entity.ID.
  4. Entity IDs are 64-bit unsigned integers, the C# type for that is called ulong, and the C++ type is called uint64_t.

Quick Note on Heap Allocations in C#

One thing that has been a problem in the Hazel script core since the start are heap allocations when calling internal methods. I won't go too deep into heap allocations as a concept, but in C# when you create a new instance of a class by calling new MyClass(), that will result in that object being allocated on the heap.

In Hazels script core we've often been returning a new instance of a class after calling an internal method, here's a quick example from the MeshColliderComponent:


// This is not great because every time we call `MeshColliderComponent.ColliderMesh`
// we allocate a brand new `Mesh` instance every time even if the mesh hasn't changed internally.
public Mesh ColliderMesh
{
	get
	{
		InternalCalls.MeshColliderComponent_GetColliderMesh(Entity.ID, out AssetHandle outMeshHandle);
		return new Mesh(outMeshHandle);
	}
}

// This is the better way of implementing this property:
// First we add a private memeber called m_ColliderMesh.
private Mesh m_ColliderMesh = null;

// Secondly we add an AssetHandle property called ColliderMeshHandle,
// this is what actually get's the handle of the Mesh
public AssetHandle ColliderMeshHandle
{
	get
	{
		if (!InternalCalls.MeshColliderComponent_GetColliderMesh(Entity.ID, out AssetHandle colliderHandle))
			return AssetHandle.Invalid;
		return colliderHandle;
	}
}

// Lastly we add a method that will return the Mesh.
public Mesh GetColliderMesh()
{
	// Here we make sure that the mesh handle that's stored internally is still valid
	// and return null if it isn't
	if (!ColliderMeshHandle.IsValid())
		return null;

	// Here we check if we either haven't set m_ColliderMesh or if the internal
	// mesh has changed, if so we create a new Mesh instance
	if (m_ColliderMesh == null || m_ColliderMesh.Handle != ColliderMeshHandle)
		m_ColliderMesh = new Mesh(ColliderMeshHandle);

	// Otherwise we just return the cached version of the Mesh instance
	return m_ColliderMesh;
}

I will say that this likely doesn't improve performance, in fact it might have slightly more overhead (it's still not noticeable), but this does reduce the number of heap allocations.

So why do we want to avoid heap allocations? Well for one they can cause performance issues if you allocate objects on the heap every frame, but it would also cause the C# Garbage Collector to run more often. I won't go into a deep explanation as to what the Garbage Collector is or why it's useful, the important part is that it cleans up unused C# objects, and doing so can cause a significant frame drop. So we want to prevent the Garbage Collector from running as much as possible, it will eventually run regardless so it's impossible to completely prevent it from running, and we don't want to prevent it, but we want to delay it.

Caching objects in C# isn't always the best thing to do, you don't really need to cache primitive types unless you really want to, and structs are usually allocated on the stack (although they can be allocated on the heap sometimes) so they're not as necessary to cache.

Properties or Methods?

Should you use properties or methods for interacting with the C++ side of things? Well, it depends. I'd say if you're doing caching that would result in similar code as the example above you should probably use a method, not a property.

I won't go into too much detail on this since it's more a question of good C# code rather than Hazel specific code.

Retrieving Assets

Assets in Hazel are reference counted, and should always be wrapped in a Ref instance. Meaning you should pretty much never allocate an Asset on the stack, since Hazel uses intrusive reference counting, meaning the reference count is stored in the asset instance, not in the Ref instance.

In order to retrieve an Asset you have to request it from the AssetManager class.

Example 1

...
Ref<PhysicsMaterial> material = AssetManager::GetAsset<PhysicsMaterial>(assetID);
...

Example 2

It's also possible to get an asset by the file path (relative to the projects "Assets" directory).

AssetHandle assetID = AssetManager::GetAssetHandleFromFilePath("Physics/PlayerMaterial.hpm");
Ref<PhysicsMaterial> material = AssetManager::GetAsset<PhysicsMaterial>(assetID);
// OR
Ref<PhysicsMaterial> material = AssetManager::GetAsset<PhysicsMaterial>("Physics/PlayerMaterial.hpm");

NOTE: Referencing Assets by ID is the preferred method

Creating New Assets

Most of the time you won't need to create assets programitcally, but Hazel does support doing so.

Ref<PhysicsMaterial> newMaterial = AssetManager::CreateNewAsset<PhysicsMaterial>("MyMaterial.hpm", "Physics/", ...);

The first two parameters are filename and directoryPath. After the first two parameters you can pass a variable number of parameters that will be used to construct the Asset instance.

Ref<SomeTextAsset> asset = AssetManager::CreateNewAsset<SomeTextAsset>("MyTextFile.txt", "Docs/", "This string will be passed to SomeTextAsset's constructor!");

Implementing New Assets

NOTE: This article won't be too useful for anyone who isn't actively developing Hazel, but you can still learn a lot about how the asset system works

Adding custom assets can be a complex task, this guide will show a simple overview of integrating any custom asset with the existing Asset System in such a way that it will accessible from the Content Browser in Hazelnut.

Converting Your Data Structure To Be An Asset


The Data Structure

In order to make your data structure (class or struct) compatible with the Asset System, it must extend the Asset class.

For this example we will be writing a simplified version of the PhysicsMaterial class:

struct PhysicsMaterial : public Asset
{
	float StaticFriction;
	float DynamicFriction;
	float Bounciness;

	PhysicsMaterial() = default;
	PhysicsMaterial(float staticFriction, float dynamicFriction, float bounciness);
};

The Asset Type

Inheriting from the Asset class isn't enough though. The Asset System needs a way to identify any asset, which is why we need to expand the AssetType enum.

The enum currently looks like this:

enum class AssetType : uint16_t
{
	None = 0,
	Scene = 1,
	MeshAsset = 2,
	Mesh = 3,
	Material = 4,
	Texture = 5,
	EnvMap = 6,
	Audio = 7
};

We will go ahead and add a value called PhysicsMat, and assign it to a value of 8.

enum class AssetType : uint16_t
{
	None = 0,
	Scene = 1,
	MeshAsset = 2,
	Mesh = 3,
	Material = 4,
	Texture = 5,
	EnvMap = 6,
	Audio = 7,
	PhysicsMat = 8
};

NOTE: You shouldn't change the value of the already existing enum values. Just add yours to the end, and give it the next available value!

We also need to modify these two functions in order for our asset to be tracked properly:

inline AssetType AssetTypeFromString(const std::string& assetType)
{
	if (assetType == "None")        return AssetType::None;
	if (assetType == "Scene")       return AssetType::Scene;
	if (assetType == "MeshAsset")   return AssetType::MeshAsset;
	if (assetType == "Mesh")        return AssetType::Mesh;
	if (assetType == "Material")    return AssetType::Material;
	if (assetType == "Texture")     return AssetType::Texture;
	if (assetType == "EnvMap")      return AssetType::EnvMap;
	if (assetType == "Audio")       return AssetType::Audio;

	HZ_CORE_ASSERT(false, "Unknown Asset Type");
	return AssetType::None;
}

inline const char* AssetTypeToString(AssetType assetType)
{
	switch (assetType)
	{
		case AssetType::None:        return "None";
		case AssetType::Scene:       return "Scene";
		case AssetType::MeshAsset:   return "MeshAsset";
		case AssetType::Mesh:        return "Mesh";
		case AssetType::Material:    return "Material";
		case AssetType::Texture:     return "Texture";
		case AssetType::EnvMap:      return "EnvMap";
		case AssetType::Audio:       return "Audio";
	}

	HZ_CORE_ASSERT(false, "Unknown Asset Type");
	return "None";
}

We need to ensure that our enum value can be properly converted from and to a string.

inline AssetType AssetTypeFromString(const std::string& assetType)
{
	if (assetType == "None")        return AssetType::None;
	if (assetType == "Scene")       return AssetType::Scene;
	if (assetType == "MeshAsset")   return AssetType::MeshAsset;
	if (assetType == "Mesh")        return AssetType::Mesh;
	if (assetType == "Material")    return AssetType::Material;
	if (assetType == "Texture")     return AssetType::Texture;
	if (assetType == "EnvMap")      return AssetType::EnvMap;
	if (assetType == "Audio")       return AssetType::Audio;
	if (assetType == "PhysicsMat")	return AssetType::PhysicsMat;

	HZ_CORE_ASSERT(false, "Unknown Asset Type");
	return AssetType::None;
}

inline const char* AssetTypeToString(AssetType assetType)
{
	switch (assetType)
	{
		case AssetType::None:        return "None";
		case AssetType::Scene:       return "Scene";
		case AssetType::MeshAsset:   return "MeshAsset";
		case AssetType::Mesh:        return "Mesh";
		case AssetType::Material:    return "Material";
		case AssetType::Texture:     return "Texture";
		case AssetType::EnvMap:      return "EnvMap";
		case AssetType::Audio:       return "Audio";
		case AssetType::PhysicsMat:  return "PhysicsMat";
	}

	HZ_CORE_ASSERT(false, "Unknown Asset Type");
	return "None";
}

Our asset class will also need to override the GetAssetType() method from Asset, and it will need to provide a static method called GetStaticType().

NOTE: Your Asset won't work if you don't provide these methods! If you don't provide GetStaticType() the code may not even compile!

struct PhysicsMaterial : public Asset
{
	float StaticFriction;
	float DynamicFriction;
	float Bounciness;

	PhysicsMaterial(float staticFriction, float dynamicFriction, float bounciness);

	static AssetType GetStaticType() { return AssetType::PhysicsMat; }
	virtual AssetType GetAssetType() const override { return GetStaticType(); }
};

This is required by certain templated methods in the Asset System.

If you want to associate your asset with a specific file type, you need to modify the s_AssetExtensionMap map in Hazel/Asset/AssetExtensions.h.

In theory this is all the code neccessary in order to have the Asset System recognize your asset. This isn't enough to have the Asset Manager import your asset correctly however. In order to have that functionality we need to implement an AssetSerializer.

Importing/Exporting The Asset


Writing the Serializer

In order to allow the Asset System to import the asset correctly we need to provide a class that the it can interface with. This is done by creating a class that inherits from the AssetSerializer class.

The AssetSerializer class contains two pure virtual methods.

virtual void Serialize(const AssetMetadata& metadata, const Ref<Asset>& asset) const = 0;
virtual bool TryLoadData(const AssetMetadata& metadata, Ref<Asset>& asset) const = 0;

AssetSerializer::Serialize is called when the Asset System requires an asset to be saved to disk. AssetSerializer::TryLoadData is called when an asset that hasn't been imported yet is requested by the engine.

Let's go ahead and implement those methods in a class called PhysicsMaterialSerializer.

NOTE: Hazel serializes data using YAML for certain assets. This isn't always the case though.

class PhysicsMaterialSerializer : public AssetSerializer
{
public:
	virtual void Serialize(const AssetMetadata& metadata, const Ref<Asset>& asset) const override
	{
		// We can call .As<>() on a Ref instance in order to cast it to a different type
		Ref<PhysicsMaterial> material = asset.As<PhysicsMaterial>();

		YAML::Emitter out;

		out << YAML::BeginMap;
		out << YAML::Key << "StaticFriction" << material->StaticFriction;
		out << YAML::Key << "DynamicFriction" << material->DynamicFriction;
		out << YAML::Key << "Bounciness" << material->Bounciness;
		out << YAML::EndMap;

		// It's important that you use AssetManager::GetFileSystemPath(metadata) here since metadata.FilePath isn't in the correct format
		std::ofstream fout(AssetManager::GetFileSystemPath(metadata.FilePath));
		fout << out.c_str();
	}

	virtual bool TryLoadData(const AssetMetadata& metadata, Ref<Asset>& asset) const override
	{
		// Load the YAML file from disk
		// It's important that you use AssetManager::GetFileSystemPath(metadata) here since metadata.FilePath isn't in the correct format
		std::ifstream stream(AssetManager::GetFileSystemPath(metadata));
		if (!stream.is_open())
			return false; // We couldn't load the file for some reason, signal the Asset System that we failed
 
		std::stringstream strStream;
		strStream << stream.rdbuf();

		YAML::Node data = YAML::Load(strStream.str());

		float staticFriction = data["StaticFriction"].as<float>();
		float dynamicFriction = data["DynamicFriction"].as<float>();
		float bounciness = data["Bounciness"].as<float>();

		// In order to create a RefCounted object we need to call Ref<Type>::Create(Args)
		asset = Ref<PhysicsMaterial>::Create(staticFriction, dynamicFriction, bounciness);

		// Here we assign the asset id to this instance of the asset
		asset->Handle = metadata.Handle;

		// We successfully loaded the asset
		return true;
	}
};

It's worth noting that the way you load the asset data heavily depends on the type of asset you're implementing. Here's an example of how Hazel loads Textures:

bool TextureSerializer::TryLoadData(const AssetMetadata& metadata, Ref<Asset>& asset) const
{
	asset = Texture2D::Create(AssetManager::GetFileSystemPathString(metadata));
	asset->Handle = metadata.Handle;

	bool result = asset.As<Texture2D>()->Loaded();

	if (!result)
		asset->SetFlag(AssetFlag::Invalid, true);

	return result;
}

Registering the Serializer

Now that we've written the serializer implementation, we need to register it to our AssetType value, so that the Asset Manager can understand that our serializer is meant to be use with our new asset type.

In order to do this we must modify the AssetImporter::Init() method in Hazel/Asset/AssetImporter.cpp. That method currently looks like this:

void AssetImporter::Init()
{
	s_Serializers[AssetType::Texture] = CreateScope<TextureSerializer>();
	s_Serializers[AssetType::MeshAsset] = CreateScope<MeshAssetSerializer>();
	s_Serializers[AssetType::Mesh] = CreateScope<MeshSerializer>();
	s_Serializers[AssetType::EnvMap] = CreateScope<EnvironmentSerializer>();
}

As you can see we call CreateScope<T> with the serializer we want to register as the template argument, and store it in a map that has an AssetType as a key. This allows the Asset Manager to query the map for a given AssetType and get the serializer for that type back.

We need to add our new serializer to this map, making sure to have our own AssetType as the key.

void AssetImporter::Init()
{
	s_Serializers[AssetType::Texture] = CreateScope<TextureSerializer>();
	s_Serializers[AssetType::MeshAsset] = CreateScope<MeshAssetSerializer>();
	s_Serializers[AssetType::Mesh] = CreateScope<MeshSerializer>();
	s_Serializers[AssetType::EnvMap] = CreateScope<EnvironmentSerializer>();

	// Associate our serializer with our AssetType
	s_Serializers[AssetType::PhysicsMat] = CreateScope<PhysicsMaterialSerializer>();
}

And that should be all you need to do in order to integrate your asset with the Asset Manager! This will also ensure that the ContentBrowserPanel deals with your asset properly!