@page tutorial Tutorial @tableofcontents This tutorial aims to teach the basics of the engine and how to use it. First, follow the steps on the @ref install page. Once the template MSVC project is setup, you can begin here. @section nfArchTut Engine Architecture An NF app is made up of a set of states represented by the nf::Gamestate class. A gamestate has a set of objects and programmed behavior. When an nf::Application is running, it is either running a state or loading one. Below is an image that describes what the main thread of an NF app might be typically doing in the program's lifetime. @image html applifetime.png "The lifetime of a typical NF app" width=20% Using this architecture, you might not even write any functions that are called from `main`. Most of the code that programs the engine's behavior should be called in your state's [update function](@ref nf::Gamestate::update). To allow a translate unit to use the engine, you must include `NothinFancy.h`. This header contains every class and function you will need. --- @section createConfigTut Creating a Config The first step to creating an app is creating an nf::Config. A config describes how the engine should display on the screen. nf::Config has these fields: - `width` - The width of the window if `fullscreen` is set to `false` - `height` - The height of the window if `fullscreen` is set to `false` - `fullscreen` - `true` sets the display to the size of the monitor the app is opened on - `title` - The title of the window shown on the caption bar and taskbar To create a 1280 by 720 window with a title of "NF Example", you would write: ~~~cpp nf::Config conf; conf.width = 1280; conf.height = 720; conf.fullscreen = false; conf.title = "NF Example"; //Or... nf::Config conf2{ 1280, 720, false, "NF Example" }; ~~~ We then pass this config to an nf::Application @section createAppTut Creating and Configuring an Application The nf::Application class represents an instance of the engine. This is the point where you will attach your states and run your application. @note In a program and on a single machine, there can only be one single instance of this class at a time. Attempting to create mulitple will result in an error. The constructor takes in the nf::Config we previously created: ~~~cpp nf::Application app(conf); ~~~ Constructing an application doesn't do much. It merely allows you to access the member functions to setup your application and run it later. Here are some functions you might want to call at this point: - [setWindowIcon](@ref nf::Application::setWindowIcon) - Sets the icon of the window - [setWindowCursor](@ref nf::Application::setWindowCursor) - Sets the cursor's image when it is visible and inside the window @note If you never call setWindowIcon before running, the default window icon will be set for you. And here are the functions you **must** call before an app can run: - [addState](@ref nf::Application::addState) - Adds a state to an app so that it can access it later by a user-defined string identifier - [setDefaultState](@ref nf::Application::setDefaultState) - Sets the state to load after the logo state exits Once these functions have been called, the app can be run: ~~~cpp CustomGamestate* customState = new CustomGamestate; //Inherits nf::Gamestate app.addState(customState, "State 1"); //"State 1" is this state's identifier. app.setDefaultState("State 1"); //Will error without this app.run(); //Blocks until exit ~~~ @section createGamestateTut Creating a Gamestate To create a gamestate, you must create a class that publicly inherits nf::Gamestate and overrides its four virtual functions: - [onEnter](@ref nf::Gamestate::onEnter) - Called when the state is loading; Where member objects are initialized - [update](@ref nf::Gamestate::update) - Called every frame after loading is complete; Where custom behavior is defined - [render](@ref nf::Gamestate::render) - Called after update; Selects what to render - [onExit](@ref nf::Gamestate::onExit) - Called when the state is unloaded A gamestate class might look something like this: ~~~cpp class CustomGamestate : public nf::Gamestate { public: void onEnter() override; void update(float deltaTime) override; //Note the parameter void render(nf::Renderer& renderer) override; void onExit() override; //Implementations somewhere else... private: //Member objects here... }; ~~~ Once an app has been setup and run with an empty state, you should be met with a black screen after the logo state: @image html blankapp.png "A blank gamestate" width=50% Congratulations! You now have a basic NF app running. Now we can add objects to our world. The window can be closed with Alt + F4 or by the close button. For more about gamestates, see @ref gamestates. @section createEntitiesTut Adding 3D Objects In NF, all 3D objects are represented by the nf::Entity class. All entities have a 3D model, a position, rotation, scale, and type among others. The [type](@ref nf::Entity::Type) of the entity dictates how the object behaves in the physics simulation. @note At this point, it's probably a good idea to read the @ref obLifetime page. It discusses the creation and destruction of different objects including entities. To construct an entity, make it a member in your gamestate, or for dynamically created entites, allocate it on the heap and add the pointer to a `std::vector` of `nf::Entity*` to keep track of them. ~~~cpp #include class CustomGamestate : public nf::Gamestate { private: nf::Entity entity1; nf::Entity entity2; std::vector entities; //Rest of class definition... }; ~~~ In your gamestate's `onEnter` function, create the entity with [create](@ref nf::Entity::create): ~~~cpp void onEnter(float deltaTime) { //Places an immovable (default) cube at (0.0, 0.0, -5.0) entity1.create(nf::BaseAssets::cube, nf::Vec3(0.0, 0.0, -5.0)); //Places a movable sphere at (3.0, 1.0, -4.0) entity2.create(nf::BaseAssets::sphere, nf::Vec3(3.0, 1.0, -4.0), nf::Entity::Type::DYNAMIC); } ~~~ The last step is to render the objects by rendering them in your gamestate's `render` function: ~~~cpp void render(nf::Renderer& renderer) { renderer.render(entity1); renderer.render(entity2); } ~~~ Once the app is run, you should see two grey objects when the state loads: a static cube and a falling sphere. @image html objects.png "Our scene so far" width=50% Let's add another entity that will be our ground plane so that our sphere has a place to land. ~~~cpp floor.create(nf::BaseAssets::plane, nf::Vec3(0.0, -3.0, 0.0)); //This plane will be the same size of the cube by default, so let's scale it on every axis... floor.setScale(10.0f); ~~~ Currently, none of the keys on our keyboard (other than Alt + F4) does anything, so let's make escape close the app. @section inputTut Keyboard Input Every gamestate has a pointer member to the parent nf::Application called `app`. Use this member to access the input functions. In NF, there are two ways to check for key events: - [isKeyHeld](@ref nf::Application::isKeyHeld) - Returns true for every frame that the tested key is held for - [isKeyPressed](@ref nf::Application::isKeyPressed) - Returns true for only the first frame the key is down regardless of how long it stays down Both functions take in a key code. The key codes provided by NF all start with `NFI_` and an uppercase letter, a number, or word denoting a special key eg `NFI_W`, `NFI_5`, and `NFI_SPACE`. To quit the app when escape is pressed, add this to your `update` funciton: ~~~cpp if (app->isKeyPressed(NFI_ESCAPE)) app->quit(); ~~~ `app->quit()` will cause the engine to shut down on the next frame and return from `nf::Application::run`. Everything is dark and grey, so let's add some basic lighting. @section lightingTut Adding Lights Naturally, the nf::Light class represents a light. It is constructed, created, and rendered in the same way that an entity is. ~~~cpp light.create(nf::Vec3(0.0, 5.0, 0.0), nf::Vec3(1.0)); //Creates a completely white light ~~~ Just as with entities, you must also render the light in your `render` function. @image html basiclighting.png "Our scene with a light" width=50% Let's go on to controlling the view. @section controlCameraTut Controlling the Camera Every gamestate has a pointer member to a nf::Camera called `camera`. Use this to control the current camera. Just like entites, the camera has a [type](@ref nf::Camera::Type) too. The type dictates how the mouse interacts with moving the camea. By default, every gamestate starts with the camera in `UI` mode which means that the mouse is free to move across the window without affecting the camera in any way. To change to the first person mode, write this in your `onEnter` function somewhere: ~~~cpp camera->setType(nf::Camera::Type::FIRST_PERSON); ~~~ @note The current mouse sensitivity will be able to be changed in a future update. Now onto actually moving the camera with the classic WASD keys. Using our previous knowledge of keyboard input, we can write four consecutive `if` statements for each of the movement keys: ~~~cpp if (app->isKeyHeld(NFI_W)) //Move forward if (app->isKeyHeld(NFI_A)) //Move left if (app->isKeyHeld(NFI_S)) //Move backward if (app->isKeyHeld(NFI_D)) //Move right ~~~ The functions for moving the camera is as follows: - [move](@ref nf::Camera::move) - Moves the camera based off of an nf::Vec2 in (x, z) - [moveZ](@ref nf::Camera::moveZ) - Moves the camera forward and backward with an offset - [moveX](@ref nf::Camera::moveX) - Moves the camera left and right with an offset Since the `offset` here will be called every frame, it's effectively a velocity, and when we talk about velocities, it's important to discuss `update`'s only parameter, `deltaTime`. Delta time in this context is the amount of time that the previous fame took to produce. This time includes how long it takes to run your `update` and `render` functions as well as everything else in the engine that gets a frame on screen. Why is this important? Because it can cancel out framerate differences between different machines. Let's say that Computer A is a modern-day gaming rig with 8 cores and an RTX 2060. Computer A can run our game at a solid 60 FPS. Let's also say that Computer B is an older laptop that struggles to run the engine smoothly. It runs our game at around 30 FPS. If in our `update` function, we move the camera (or anything at all) by a set amount, Computer A will move our view twice the distance than Computer B in any amount of real time since there were twice the amount of frames drawn in that time. The solution is to multiply any velocity you use with this delta time. You will have to change your values around a little, but this will make speeds consistant across computers. With that, we can now complete our movement logic: ~~~cpp if (app->isKeyHeld(NFI_W)) camera->moveZ(10.0f * deltaTime); if (app->isKeyHeld(NFI_A)) camera->moveX(-10.0f * deltaTime); if (app->isKeyHeld(NFI_S)) camera->moveZ(-10.0f * deltaTime); if (app->isKeyHeld(NFI_D)) camera->moveX(10.0f * deltaTime); ~~~ We are now able to move around the world. @image html cameramovement.png "Our scene from a different angle" width=50% Now let's take care of that black background. @section createCubemapTut Adding a Cubemap (Skybox) A world texture is represented by the nf::Cubemap class. Rendering this object will display a texture in the world. We use the class the same way we use the others: ~~~cpp cubemap.create(nf::BaseAssets::cubemap); //No position or type though ~~~ After rendering, our world will have a background. @image html cubemap.png "Our scene with a background" width=50% @note Cubemaps do not emit light onto your scene. @section customAssetsTut Adding Your Assets NF's asset system builds your assets into NFPacks that the engine reads at runtime. The external tool `NFAssetBuilder.exe` creates these for you. You can then access these packs through the nf::AssetPack class. For a complete guide, please see @ref assets. @image html custommodel.png "An example of a custom model" width=50% @section createUITut Creating a UI NF currently has three classes of UI objects: - nf::Text - A string of text on the screen - nf::UITexture - Any 2D texture to put on the screen - nf::Button - A horizontal button that can be clicked with the mouse To create a text: ~~~cpp text.create("NF Test", nf::Vec2(0.8, 0.1)); text.setScale(2.0); std::string string = "More Text"; text.setText(string); ~~~ @note The default font is Microsoft's Segoe UI Light, but a text's font [can be changed](@ref customFonts). To create a texture on the UI: ~~~cpp texture.create(nf::BaseAssets::logo, nf::Vec2(0.1, 0.1)); ~~~ To create a clickable button: ~~~cpp button.create(nf::Vec2(0.1, 0.1), "Text on button"); //You can also center any of these three classes by calling button.centered(true, false); //where the first bool is the x-axis and the second is the y-axis ~~~ The default button textures [can also be changed](@ref customButtons). Since buttons are controlled by the mouse, they cannot be interacted with in our current camera mode. Let's add a keybind that will switch between the appropriate modes. ~~~cpp //In our update function... if (app->isKeyPressed(NFI_E)) camera->setType(camera->getType() == nf::Camera::Type::UI ? nf::Camera::Type::FIRST_PERSON : nf::Camera::Type::UI); if (button.isClicked()) { NFLog("Clicked!); } ~~~ @image html ui.png "Our new UI with a working button" width=70% @section soundTut Adding Sound Our app is silent as of now. To play a sound, create an nf::Sound object and call its [play](@ref nf::Sound::play) function. Creating a sound requires a custom asset to be loaded. See the [assets page](@ref customSounds). NF supports 3D sound. ~~~cpp sound.create(pack.get("Sound.ogg")); //In update somewhere... sound.play(); ~~~ If a sound is played like this (with no position set), it will sound as if it is coming from every direction. But if we set the position of the sound, it will sound as if it originates from that position. This can either be done by setting a static position in the world, or by specifying an existing nf::Entity, which will cause the sound to always play at that entity's origin (probably inside the model). ~~~cpp //Play at a static position: sound.setPosition(nf::Vec3(10.0, 25.0, 15.0)); //Play dynamically wherever the target entity is: sound.setEntity(entity2); ~~~ @section gamestateSwitchTut Switching Gamestates To unload the current state and load another state, call: ~~~cpp app->changeState("State 2"); //String identifier we defined earlier ~~~ @note The currently running state that calls [changeState](@ref nf::Application::changeState) does not stop running right away. The `update` and `render` functions of that state are called until the loading screen fades in completely. @section debuggingTut Debugging Your App NF has a number of @ref macros that you can use in your debug builds to help you develop your application. These macros can log messages, throw errors, time functions, and pause the engine. ~~~cpp nf::Vec3 pos = camera->getPosition(); std::string posStr = std::to_string(pos.x) + (std::string)", " + std::to_string(pos.y) + (std::string)", " + std::to_string(pos.z); NFLog("Current position: " + posStr); //This will print the current position of the camera every frame. ~~~ @section packagingTut Packaging and Distributing Your App @note Remember to only ever distribute your release build. The only external prerequisite when running a NF app is the 2022 64-bit MSVC redistributable. The installer is included in the `redist` folder in the engine download. Other than that, a build can be very simple: - **NFApp.exe** - The application binary which is named from the MSVC project - **assets** - The folder which holds your NFPacks - **base.nfpack** - The NFPack that holds both critical and default assets - **Your Assets** - The rest of the NFPacks your application needs These are the only files you need to package in your build.