This repository has been archived on 2025-03-10. You can view files and clone it, but cannot push or open issues or pull requests.
NFRev1/docs/pages/2_tutorial.md
2022-01-05 12:30:55 -06:00

466 lines
16 KiB
Markdown

@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 <vector>
class CustomGamestate : public nf::Gamestate {
private:
nf::Entity entity1;
nf::Entity entity2;
std::vector<nf::Entity*> 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.