Fisheye is a 3D game engine written and usable in C++. The intent was to create an easy to use, performant, and customizable platform for game developement.
This is a university project made by two people in ~two months.
No generative AI was used in the making of this project.
fisheye-vid.mp4
-
Scene graph, gameobjects and components
A game is composed of tree-like scenes; hierarchies of gameobjects, using an Entity-Component approach (as used by the Unity engine). Components define the behaviors of gameobjects. The user can create new components easily.
-
Deffered rendering
The rendering happens in two passes. First, we draw all the rendering informations (normals, albedos, PBR data, ...) in world-space to 2D buffers, then we use the data to generate the final texture. This saves time on lighting calculation, allows us to have a very big number of lights in the scene with only a small performance cost.
-
Physics engine
Our physics engine runs on its own loop and is partly inspired on a Valve talk on the Source engine, in order to compute and solve collisions efficiently.
The physics engine is still experimental. Things may not work as expected (particularily rotation-wise :s )
For now, the physics update loop runs on the same thread as the render update loop. This will change in the future.
-
Rendering and physics servers
For efficient processing of batch operations, we took inspiration from ECS architecture and the Godot game engine. Rendering code and physics-related code is ran by servers akin to ECS systems, which work fluidly around the scene hierarchy and result in more efficient processing.
-
Additional demo candy
A fully functional voxel and chunking system, with simple world generation based on noise functions, has been implemented for the 'voxel submarine' demo. The demo and its components are also present
We'll create a very simple game to show how one may use the engine.
Let's start by creating a Game in our main function, setting some of its parameters, initialize openGL and start the update loops.
#include "engine/includes/core.h" // imports core engine features
#include "engine/includes/components.h" // imports base engine components
void main (){
Game game;
game.settings.windowWidth = 1280;
game.settings.windowHeight = 720;
game.init(); // initialize graphical backend
setupGameScene(game); // function detailed in the next part
game.start(); // start update and physics loops
game.waitGameStopped(); // wait for the quit signal before terminating
}Build and launch using the provided Cmake. This gives us a fully black window.
Let us add a cube and a light to our scene. We fill up the setupScene function called in our main.
void setupScene(Game& game){
Scene scene; // Create a scene to contain our gameobjects
GameObject world; // root of our scene
// load an obj file
Mesh cube = ResourceLoader::load_mesh_obj("../game/resources/meshes/supercube.obj");
// We create a PBR material object. A handle is the (possibly shared) owner of a resource
// As long as one handle exists, the resource is not freed up (akin to a shared_ptr)
cube.material = Handle<MaterialPBR>(Texture("../game/resources/textures/logo.jpg"));
// Add a 'Transform' compoment to the world
// addComponent creates the component on the gameobject and returns a pointer to it.
// The pointer is guaranteed to be valid for the lifetime of the gameobject.
C_Transform* t = world->addComponent<C_Transform>();
t->setPosition(glm::vec3(0, -0.2, 0)); // using glm as a placeholder, as we'll be creating our own algebra module.
t->setScale(glm::vec3(0.1, 0.1, 0.1));
// add a mesh component (directly on the world for now)
world->addComponent<C_Mesh>() -> mesh = cube;
// light
GameObject light;
auto* lightComponent = light->addComponent<C_Light>();
lightComponent->light.color = glm::vec3(1.0, 1.0, 1.0);
light->addComponent<C_Transform>()->setPosition(glm::vec3(10.0, 10.0, 5.0));
world->addChild(std::move(light)); // We give the light to the world (amen)
lightComponent->light.intensity = 100.0; // but the discarded object is still valid to use as a non-owning pointer to the gameobject
scene.setRoot(std::move(world)); // we give the world to the scene
game.setScene(std::move(scene)); // and the scene to the game
}We get the following scene:
From there, we could add more components to further customize the behavior of our gameobjects.
In order to customize the behavior of the gameobjects in our world, we can create new components. We do this by inheriting from the Component class.
This gives us five virtual functions that we may override to our liking :
- onEnterScene() & onExitScene(), called when the owning gameobject enters or exits a scene
- onUpdate($\delta_t$)), onPhysicsUpdate($\delta_t$) et onLateUpdate($\delta_t$) are called at different stages of the frame's computation.
By inheriting other components (and creating new virtual functions to override), we can easily and organically expand the capabilities of our gameobjects.
For example, the C_RigidBody Component offers an onContact(Contact) function, which allows inheriting components to execute code on a physics collision.
Let's create a player controller component. This also demonstrates the user inputs system:
class C_PlayerController: public Component {
public:
float movement_speed = 2.0;
void jump();
void inputCallback(const InputEvent & e){
if (e == "move_up" && e.pressed() && isOnGround){
jump();
}
C_PlayerController(){
// user input callback registration
// The lambda is necessary as we can't cast a method to a generic function signature
Input::addInputListener(
[& /*capture ambient scope*/](const InputEvent & e){
inputCallback(e);
}
);
// Defined here for the sake of the example. It would be better to define them once and for all at the game start.
Input::addKeybind( "move_right",
KeySpec(BINDTYPE_KEYBOARD, GLFW_KEY_D)
);
Input::addKeybind("move_left",
KeySpec(BINDTYPE_KEYBOARD, GLFW_KEY_A)
);
Input::addKeybind("move_up",
KeySpec(BINDTYPE_MOUSE, GLFW_MOUSE_BUTTON_1)
);
}
virtual void _onUpdate(float delta) override{
delta *= movement_speed;
// check if we do have a transform
if (!getOwner()->hasComponent<C_Transform>()) return;
auto* transform = getOwner()->getComponent<C_Transform>();
if (Input::isInputPressed("move_right")){
transform->move(glm::vec3(1, 0, 0) * delta);
}
if (Input::isInputPressed("move_left")){
transform->move(glm::vec3(-1, 0, 0) * delta);
}
}
};Then, we just need to add this component to a gameobject in order to move it around:
For now, those 9 compoments are available in the engine :
- C_Transform
- C_RigidBody
- C_Collider
- C_Mesh
- C_Camera
- C_Light
- C_PlayerController*
- C_MapManager*
- C_voxelMesh*
*created for the 'voxel submarine' demo
In the future, we'd like to create a diverse and extensive library of authored components.
Thanks and enjoy !