Skip to content

Small and intuitive cross-platform 3D game engine (DirectX 12 and Vulkan renderers).

License

Notifications You must be signed in to change notification settings

Flone-dnb/nameless-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nameless-engine

Supported platforms

  • Windows
  • Linux

Philosophy

The engine tries to focus on things that other engines miss:

  • Intuitive API
  • Simplicity (not in terms of "make a game in one click" but in terms of "there are not much things to learn" (compared to the usual C++ development/gamedev knowledge), for example: there's no custom/unusual build system for the engine, just use your usual CMake files)
  • Focus on KISS for engine internals
  • Documentation (engine source code is fully documented, every code entity is documented, even private, also there are lots of documentation-comments in the implementation code, there's also a manual that you can read)

Roadmap

Currently the engine is in a very early development state and right now it's still impossible to make games with this engine.

Here is the list of base features that needs to be implemented in order for you to be able to make games:

  • Reflection system
  • Serialization/deserialization for game assets
  • Garbage collector
  • Handling user input events
  • Config management (progress, settings, etc.)
  • Node system (Godot-like ECS alternative)
  • Profiler
  • GLTF/GLB import
  • Rendering (features for both DirectX 12 and Vulkan renderers)
    • Automatically test and pick the most suitable renderer/GPU based on the OS/hardware capabilities/user preferences at runtime
    • MSAA (Multisample anti-aliasing)
    • Transparent materials
    • Diffuse textures
    • Multiple materials per mesh
    • Frustum culling
    • Simple API for using custom vertex/pixel/fragment/compute shaders
    • Z-prepass
    • Forward+ (Tiled Forward) rendering
    • Light sources
      • Directional light
      • Spot light
      • Point light
    • Shadow mapping
      • Directional lights
      • Spot lights
      • Point lights
    • Cascading shadow maps
    • Space partitioning and multi-threading for frustum culling
    • Post-processing effects
    • HDR (High dynamic range)
    • Normal mapping
    • Cube mapping
    • Instancing
    • SSAO (Screen space ambient occlusion)
    • PBR (Physically based rendering)
    • Decals
    • Order independent transparency
    • Occlusion culling
    • Emissive materials
    • Light culling improvements
    • Multi-threaded command list/buffer generation
    • Shadow caster culling
    • GUI
  • Skeletal animations
  • Minimal scripting using AngelScript
  • Basic editor
  • Audio engine (integrate SoLoud)
    • 2D audio
    • 3D audio
    • Audio streaming
    • Sound effects
  • Physics engine (integrate Jolt or Bullet3)
    • Rigid body
    • Ragdoll
    • Soft body
    • Joints
  • Automatic LODs (Level of details)
  • AI and pathfinding
  • Particle effects

Once all base features will be implemented I will create a separate repository for examples and add a link to it here.

Documentation

Documentation for this engine consists of 2 parts: API reference (generated from C++ comments) and the manual (at docs/Manual.md), generated documentation includes both API reference and the manual (copied from docs/Manual.md).

In order to generate the documentation you need to have Doxygen installed.

The documentation can be generated by executing the doxygen command while being in the docs directory. If Doxygen is installed, this will be done automatically on each build.

Generated documentation will be located at docs/gen/html, open the index.html file from this directory to view the documentation.

Setup (Build)

Prerequisites:

  • compiler that supports C++23 (latest MSVC/Clang)
  • CMake
  • Doxygen
  • LLVM
  • Go
  • Vulkan SDK
  • prerequisites for Linux:
    • libtinfo.so might not be installed on your system but is required
    • libclang (needed for reflection code generator), after CMake is configured ext/Refureku/build/Bin/ will contain needed libraries, you would need to create the file /etc/ld.so.conf.d/nameless-engine.conf with the path to this directory and run sudo ldconfig so that these libraries will be found by the reflection generator

First, clone this repository:

git clone https://github.com/Flone-dnb/nameless-engine
cd nameless-engine
git submodule update --init --recursive

Then, if you've never used CMake before:

Create a build directory next to this file, open created build directory and type cmd in Explorer's address bar. This will open up a console in which you need to type this:

cmake -DCMAKE_BUILD_TYPE=Debug .. // for debug mode
cmake -DCMAKE_BUILD_TYPE=Release .. // for release mode

This will generate project files that you will use for development.

Update

To update this repository:

git pull
git submodule update --init --recursive

Code style

The following is a code style rules for engine developers not for game developers (although if you prefer you can follow these rules even if you're not writing engine code but making a game).

Mostly engine code style is controlled though clang-format and clang-tidy (although clang-tidy is only enabled for release builds so make sure your changes compile in release mode), configuration for both of these is located in the root directory of this repository. Nevertheless, there are several things that those two can't control, which are:

Use prefixes for variables/fields

Some prefixes are not controlled by clang-tidy:

  • for bool variables the prefix is b, example: bIsEnabled,
  • for integer variables (int, size_t, etc.) the prefix is i, example: iSwapChainBufferCount,
  • for string variables (std::string, std::string_view, etc.) the prefix is s, example: sNodeName,
  • for vector variables (std::vector, std::array, etc.) the prefix is v, example: vActionEvents,
  • additionally, if you're using a mutex to guard specific field(s) use std::pair if possible, here are some examples:
std::pair<std::recursive_mutex, gc<Node>> mtxParentNode;

struct LocalSpaceInformation {
    glm::mat4x4 relativeRotationMatrix = glm::identity<glm::mat4x4>();
    glm::quat relativeRotationQuaternion = glm::identity<glm::quat>();
};

std::pair<std::recursive_mutex, LocalSpaceInformation> mtxLocalSpace;

Sort included headers

Sort your #includes and group them like this:

// Standard.
#include <variant>
#include <memory>

// Custom.
#include "render/general/resources/GpuResource.h"

// External.
#include "vulkan/vulkan.h"

where Standard refers to headers from the standard library, Custom refers to headers from the engine/game and External refers to headers from external dependencies.

Directory/file naming

Directories in the src directory are named with 1 lowercase word (preferably without underscores), for example:

render\general\resources

used to store render-specific source code, general means API-independent (DirectX/Vulkan) and resources refers to GPU resources.

Files are named using CamelCase, for example: EditorGameInstance.h.

Some header rules

  • if you're using friend class specify it in the beginning of the class with a small description, for example:
/**
 * Represents a descriptor (to a resource) that is stored in a descriptor heap.
 * Automatically marked as unused in destructor.
 */
class DirectXDescriptor {
    // We notify the heap about descriptor being no longer used in destructor.
    friend class DirectXDescriptorHeap;

    // ... then goes other code ...

generally friend class is used to hide some internal object communication functions from public section to avoid public API user from shooting himself in the foot and causing unexpected behaviour.

  • don't duplicate access modifiers in your class/struct, so don't write code like this:
class Foo{
public:
    // ... some code here ...

private:
    // ... some code here ...

public: // <- don't do that as `public` was already specified earlier
    // ... some code here...
};
  • specify access modifiers in the following order: public, protected and finally private, for example:
class Foo{
public:
    // ... some code here ...

protected:
    // ... some code here ...

private:
    // ... some code here...
};
  • don't mix function with fields: specify all functions first and only then fields, for example:
class Foo{
public:
    void foo();

protected:
    void bar();

private:
    void somefunction1();

    void somefunction2();

    // now all functions were specified and we can specify all fields

    int iAnswer = 42;

    static constexpr bool bEnable = false;
};
  • don't store fields in the public/protected section, generally you should specify all fields in the private section and provide getters/setters in other sections

    • but there are some exceptions to this rule such as structs that just group a few variables - you don't need to hide them in private section and provide getters although if you think this will help or you want to do something in your getters then you can do that
    • for inheritance generally you should still put all base class fields in the private section and for derived classes provide a protected getter/setter if you need, although there are also some exceptions to this
  • put all static constexpr / static inline const fields in the bottom of your or class, for example:

private:
    // ... some code here ...

    /** Index of the root parameter that points to `cbuffer` with frame constants. */
    static constexpr UINT iFrameConstantBufferRootParameterIndex = 0;
}; // end of class
  • if your function can fail use engine's Error class as a return type (instead of logging an error and returning nothing), for example:
static std::variant<std::unique_ptr<VulkanResource>, Error> create(...);

[[nodiscard]] std::optional<Error> initialize(Renderer* pRenderer) override;
  • if your function returns std::optional<Error> mark it as [[nodiscard]]

Some implementation rules

  • avoid nesting, for example:

bad:

// Make sure the specified file exists.
if (std::filesystem::exists(shaderDescription.pathToShaderFile)) [[likely]] {
    // Make sure the specified path is a file.
    if (!std::filesystem::is_directory(shaderDescription.pathToShaderFile)) [[likely]] {
        // Create shader cache directory if needed.
        if (!std::filesystem::exists(shaderCacheDirectory)) {
            std::filesystem::create_directory(shaderCacheDirectory);
        }

        // ... some other code ...
    }else{
        return Error(fmt::format(
            "the specified shader path {} is not a file", shaderDescription.pathToShaderFile.string()));
    }
}else{
    return Error(fmt::format(
        "the specified shader file {} does not exist", shaderDescription.pathToShaderFile.string()));
}

good:

// Make sure the specified file exists.
if (!std::filesystem::exists(shaderDescription.pathToShaderFile)) [[unlikely]] {
    return Error(fmt::format(
        "the specified shader file {} does not exist", shaderDescription.pathToShaderFile.string()));
}

// Make sure the specified path is a file.
if (std::filesystem::is_directory(shaderDescription.pathToShaderFile)) [[unlikely]] {
        return Error(fmt::format(
            "the specified shader path {} is not a file", shaderDescription.pathToShaderFile.string()));
}

// Create shader cache directory if needed.
if (!std::filesystem::exists(shaderCacheDirectory)) {
    std::filesystem::create_directory(shaderCacheDirectory);
}
  • prefer to group your implementation code into small chunks without empty lines with a comment in the beginning, all chunks should be separated by 1 empty line, for example:
// Specify defined macros for this shader.
auto vParameterNames = convertShaderMacrosToText(macros);
for (const auto& sParameter : vParameterNames) {
    currentShaderDescription.vDefinedShaderMacros.push_back(sParameter);
}

// Add hash of the configuration to the shader name for logging.
const auto sConfigurationText = ShaderMacroConfigurations::convertConfigurationToText(macros);
currentShaderDescription.sShaderName += sConfigurationText;

// Add hash of the configuration to the compiled shader file name
// so that all shader variants will be stored in different files.
auto currentPathToCompiledShader = pathToCompiledShader;
currentPathToCompiledShader += sConfigurationText;

// Try to load the shader from cache.
auto result = Shader::createFromCache(
    pRenderer,
    currentPathToCompiledShader,
    currentShaderDescription,
    shaderDescription.sShaderName,
    cacheInvalidationReason);
if (std::holds_alternative<Error>(result)) {
    // Shader cache is corrupted or invalid. Delete invalid cache directory.
    std::filesystem::remove_all(
        ShaderFilesystemPaths::getPathToShaderCacheDirectory() / shaderDescription.sShaderName);

    // Return error that specifies that cache is invalid.
    auto err = std::get<Error>(std::move(result));
    err.addCurrentLocationToErrorStack();
    return err;
}

// Save loaded shader to shader pack.
pShaderPack->mtxInternalResources.second.shadersInPack[macros] =
    std::get<std::shared_ptr<Shader>>(result);