Prerequisites
https://www.ultraengine.com/learn/cppsetup?lang=cpp
- Install Ultra Engine Pro
- Check the environment variable ULTRAENGINE (typically "C:\Program Files\Ultra Engine" or if you got Ultra Engine through Steam it will be "C:\Program Files (x86)\Steam\steamapps\common\Ultra Engine Pro").
- Install Visual Studio 2022 Community Edition
- During the installation process, make sure to select the "Desktop development with C++" option
- Create C++ project with Blank Project template
Map
Create a simple map call start.ultra with a brush as floor and a Empty aka pivot with FPSPlayer component
How to do it:
Now in Visual Studio (VS) you can compile and run main.cpp to see how it’s looks in game:
You can add skybox in Edit > World Settings (at top buttons in the Editor) by choosing skybox.dds from Materials\Environment\Default
Main menu, loading screen and GUI
Create a simple menu with couple brushes and camera and name this map menu.ultra
main.cpp with a code just for Loading screen almost without anything else:
#include "UltraEngine.h" #include "ComponentSystem.h" using namespace UltraEngine; shared_ptr<Window> window; shared_ptr<Framebuffer> framebuffer; //loading screen vars shared_ptr<World> loadingWorld; shared_ptr<Camera> loadingCamera; shared_ptr<Sprite> loadingText; shared_ptr<Sprite> loadingBackground; //for using in main loop shared_ptr<World> currentWorld; shared_ptr<Interface> currentUi; int main(int argc, const char* argv[]) { //Need it to make component from map's entites start working RegisterComponents(); //Computer screens auto displays = GetDisplays(); window = CreateWindow("Ultra Engine Game", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR); framebuffer = CreateFramebuffer(window); //need a dedicated world to be able render and to stop renedering loading screen wheen needed loadingWorld = CreateWorld(); float centerX = float(framebuffer->GetSize().x) * 0.5f; float centerY = float(framebuffer->GetSize().y) * 0.5f; float labelHeight = float(framebuffer->GetSize().y) * 0.2f; loadingBackground = CreateSprite(loadingWorld, framebuffer->size.width, framebuffer->size.height); loadingBackground->SetColor(0.2, 0.2, 0.2); loadingBackground->SetRenderLayers(2); auto font = LoadFont("Fonts/arial.ttf"); loadingText = CreateSprite(loadingWorld, font, "LOADING", labelHeight, TEXT_CENTER | TEXT_MIDDLE); loadingText->SetPosition(centerX, centerY + labelHeight * 0.5f); //0 layer - no render, 1 - default render, we will use 2 for UI and sprites loadingText->SetRenderLayers(2); //Creating camera for sprites, which needs to be orthographic (2D) for UI and sprites if they used as UI loadingCamera = CreateCamera(loadingWorld, PROJECTION_ORTHOGRAPHIC); loadingCamera->SetPosition(centerX, centerY, 0); //camera render layer should match with stuff that you want to be visible for this camera. RenderLayers is a bit mask, so you can combine few layers, but probably you don't need it in most cases loadingCamera->SetRenderLayers(2); currentWorld = loadingWorld; //simple minimum game loop while (window->Closed() == false) { //getting all events from queue - input, UI etc. while (PeekEvent()) { const Event ev = WaitEvent(); //You need to do it for UI in 3D scene if (currentUi) { currentUi->ProcessEvent(ev); } } if (currentWorld) { //Update game logic (positions, components etc.). By default 60 HZ and not depends on framerate if you have 60+ FPS currentWorld->Update(); //2nd param is VSync (true by default), 3rd is fps limit. Can by changed dynamically. currentWorld->Render(framebuffer); } } //if get here by closing window application will ends/closes return 0; }
Now if we start a game we will see this loading screen:
Before making new classes it's better to do enable "Show All Files" mode to be able see actual folders in the project:
And once we have a screen we can create a menu to load in. But let's get prepared to a future a bit and make custom events.
We will need it to use Event system with own events.
Add CustomEvents.h to Source folder:
#pragma once #include "UltraEngine.h" namespace UltraEngine { //load a game map const EventId EVENT_GAME_START = EventId(1001); //load a main menu const EventId EVENT_MAIN_MENU = EventId(1002); }
If you created a file manually then do Refresh in Solution Explorer.
Remember to include a class files to a project via sub menu called with right click on file in Solution Explorer.
Create MainMenu in Source folder.
MainMenu.h:
#pragma once #include "UltraEngine.h" using namespace UltraEngine; //Object is convenient class for a parent class due As() - casting, and Self() methods class MainMenu : public Object { protected: //constructor - you can think about it as a first method that will be called //protected in this case because we will use create() method for making a new Manu MainMenu(); //shared_ptr is smart pointer that will destroy object automatically once all pointer of this objects are out of scope //in perfect world you need to have maximum only one shared pointer to specific class instance/object as class member, because otherwise object may stay in memory when you don't expect it //for cases when you need pointer without keeping an object you can use std::weak_ptr and it's method .lock() to get a shared_ptr for specific code scope //2D camera for GUI shared_ptr<Camera> uiCamera; //Scene is map entities in first place and you need to keep as long as you need to keep map object shared_ptr<Scene> scene; void init(shared_ptr<Framebuffer> framebuffer); public: static std::shared_ptr<MainMenu> create(shared_ptr<Framebuffer> framebuffer); shared_ptr<Interface> ui; shared_ptr<World> world; };
MainMenu.cpp:
#include "UltraEngine.h" #include "MainMenu.h" #include "CustomEvents.h" //default empty constructor MainMenu::MainMenu() = default; //it's better to always use shared_ptr and it's a static dedicated method to make one std::shared_ptr<MainMenu> MainMenu::create(shared_ptr<Framebuffer> framebuffer) { //struct are kinda mini classes, using it in create methods for proper accesses struct Struct : public MainMenu {}; auto instance = std::make_shared<Struct>(); instance->init(framebuffer); return instance; } static bool NewGameButtonCallback(const Event& ev, shared_ptr<Object> extra) { EmitEvent(EVENT_GAME_START, nullptr, 0, 0, 0, 0, 0, nullptr, "start.ultra"); return true; } static bool ExitButtonCallback(const Event& ev, shared_ptr<Object> extra) { //to close application exit(0); return true; } void MainMenu::init(shared_ptr<Framebuffer> framebuffer) { world = CreateWorld(); scene = LoadMap(world, "Maps/menu.ultra"); //Load a font auto font = LoadFont("Fonts/arial.ttf"); //Create user interface auto frameSize = framebuffer->GetSize(); ui = CreateInterface(world, font, frameSize); ui->SetRenderLayers(2); //to make backgrount transparent ui->root->SetColor(0.0f, 0.0f, 0.0f, 0.0f); //Create camera for GUI uiCamera = CreateCamera(world, PROJECTION_ORTHOGRAPHIC); uiCamera->SetPosition(float(frameSize.x) * 0.5f, float(frameSize.y) * 0.5f, 0); uiCamera->SetRenderLayers(2); //for correct rendering above 3D scene uiCamera->SetClearMode(CLEAR_DEPTH); //Menu buttons auto newGameButton = CreateButton("New game", frameSize.width / 2 - 100, 125, 200, 50, ui->root); ListenEvent(EVENT_WIDGETACTION, newGameButton, NewGameButtonCallback); auto exitButton = CreateButton("Exit", frameSize.width / 2 - 100, 200, 200, 50, ui->root); ListenEvent(EVENT_WIDGETACTION, exitButton, ExitButtonCallback); }
Let's get back to main.cpp. We have files to include:
#include "MainMenu.h" #include "CustomEvents.h"
Also we need menu as global var:
shared_ptr<MainMenu> menu;
Add those fuctions above main one, we will need them for event system:
static bool StartGameEventCallback(const Event& e, shared_ptr<Object> extra) { //nothing just for now return true; } static bool MainMenuEventCallback(const Event& e, shared_ptr<Object> extra) { menu = MainMenu::create(framebuffer); //switching current render and update targets for loop currentWorld = menu->world; currentUi = menu->ui; return true; }
Put this above main loop:
//to show Loading screen before Main Menu loadingWorld->Render(framebuffer); //ListenEvent are needed to do something in callback function when specific even from specfic source (or not, if 2nd param is nullptr) emitted ListenEvent(EVENT_GAME_START, nullptr, StartGameEventCallback); ListenEvent(EVENT_MAIN_MENU, nullptr, MainMenuEventCallback); //let's try it out! EmitEvent(EVENT_MAIN_MENU);
Just in case if something one wrong, current main.cpp file main.cpp
It's time to to take a look on our fresh menu!
If you did not blink you could also see a Loading screen, at lest in debug mode. Unfortunately white screen before Loading screen can be noticed way easier and i don't know yet a way to fix it.
We need to modify FPSPlayer component just a little bit. Add to its header FPSPlayer.h
bool doResetMousePosition = true;
And new condition for mouse position reset in FPSPlayer.cpp in Update()
if (doResetMousePosition) { window->SetMousePosition(cx, cy); }
One big thing left - a game to play. Add Game class to project:
Game.h:
#pragma once #include "UltraEngine.h" #include "Components/Player/FPSPlayer.h" using namespace UltraEngine; class Game : public Object { protected: shared_ptr<Camera> uiCamera; shared_ptr<Scene> scene; shared_ptr<FPSPlayer> player; shared_ptr<Widget> menuPanel; Game(); void init(shared_ptr<Framebuffer> framebuffer, WString mapPath); public: //to show/hide game menu on Esc static bool GameMenuButtonCallback(const Event& ev, shared_ptr<Object> extra); static std::shared_ptr<Game> create(shared_ptr<Framebuffer> framebuffer, WString mapPath); shared_ptr<Interface> ui; shared_ptr<World> world; };
Game.cpp:
#include "UltraEngine.h" #include "Game.h" #include "CustomEvents.h" Game::Game() = default; std::shared_ptr<Game> Game::create(shared_ptr<Framebuffer> framebuffer, WString mapPath) { struct Struct : public Game {}; auto instance = std::make_shared<Struct>(); instance->init(framebuffer, mapPath); return instance; } bool Game::GameMenuButtonCallback(const Event& ev, shared_ptr<Object> extra) { if (KEY_ESCAPE == ev.data && extra) { auto game = extra->As<Game>(); bool isHidden = game->menuPanel->GetHidden(); game->menuPanel->SetHidden(!isHidden); //we can get a game window anywhere, but take in mind that it will return nullptr, if window is not active ;) auto window = ActiveWindow(); //checking just in case if we actually got a window if (window) { //hiding cursor when hiding a menu and vice versa window->SetCursor(isHidden ? CURSOR_DEFAULT : CURSOR_NONE); } if (game->player) {//to stop cursor reset to center when menu on game->player->doResetMousePosition = !isHidden; } } //If the callback function returns false no more callbacks will be executed and no event will be added to the event queue. return false; } static bool MainMenuButtonCallback(const Event& ev, shared_ptr<Object> extra) { EmitEvent(EVENT_MAIN_MENU); return true; } static bool ExitButtonCallback(const Event& ev, shared_ptr<Object> extra) { exit(0); return true; } void Game::init(shared_ptr<Framebuffer> framebuffer, WString mapPath) { world = CreateWorld(); scene = LoadMap(world, mapPath); for (auto const& entity : scene->entities) { auto foundPlayer = entity->GetComponent<FPSPlayer>(); if (foundPlayer) { player = foundPlayer; break; } } auto font = LoadFont("Fonts/arial.ttf"); //Create user interface for game menu auto frameSize = framebuffer->GetSize(); ui = CreateInterface(world, font, frameSize); ui->SetRenderLayers(2); ui->root->SetColor(0.0f, 0.0f, 0.0f, 0.0f); uiCamera = CreateCamera(world, PROJECTION_ORTHOGRAPHIC); uiCamera->SetPosition(float(frameSize.x) * 0.5f, float(frameSize.y) * 0.5f, 0); uiCamera->SetRenderLayers(2); uiCamera->SetClearMode(CLEAR_DEPTH); //widgets are stays without extra shared pointers because parent widet, ui->root in this case, keep them //to remove widget you should do widget->SetParent(nullptr) menuPanel = CreatePanel(frameSize.width / 2 - 150, frameSize.height / 2 - 125 / 2, 300, 250, ui->root); auto menuButton = CreateButton("Main menu", 50, 50, 200, 50, menuPanel); ListenEvent(EVENT_WIDGETACTION, menuButton, MainMenuButtonCallback); auto exitButton = CreateButton("Exit", 50, 150, 200, 50, menuPanel); ListenEvent(EVENT_WIDGETACTION, exitButton, ExitButtonCallback, nullptr); //we don't need game menu on screen while playing menuPanel->SetHidden(true); //and we will need it once hitting Esc button ListenEvent(EVENT_KEYUP, nullptr, GameMenuButtonCallback, Self()); //take in mind that extra param will be kept as shared_ptr in callback ^ }
Add new Game include and global var to main.cpp:
#include "Game.h" shared_ptr<Game> game;
Update function callbacks:
static bool StartGameEventCallback(const Event& e, shared_ptr<Object> extra) { //destroying a main menu menu.reset(); //to show loading screen loadingWorld->Render(framebuffer); game = Game::create(framebuffer, "Maps/" + e.text); //switching current render and update targets for loop currentWorld = game->world; currentUi = game->ui; //to avoid crash when engine will try to proccess prev ui, catching in main loop throw std::invalid_argument("Game"); return true; } bool MainMenuEventCallback(const Event& e, shared_ptr<Object> extra) { //destroying a game instance if one existed game.reset(); //to show loading screen loadingWorld->Render(framebuffer); menu = MainMenu::create(framebuffer); //switching current render and update targets for loop currentWorld = menu->world; currentUi = menu->ui; throw std::invalid_argument("Main menu"); return true; }
Find EmitEvent(EVENT_MAIN_MENU) and wrap in try-catch:
try { EmitEvent(EVENT_MAIN_MENU); } catch (const std::invalid_argument& e) { //do nothing }
Same for ui processing:
try { if (currentUi) { currentUi->ProcessEvent(ev); } } catch (const std::invalid_argument& e) { //Stop processing old ui }
And in the end we have a game with own menu:
All created and modified classes:
Maps:
Repository: https://github.com/Dreikblack/CppTutorialProject/tree/menu-loading-screen-and-gui
- 1
0 Comments
Recommended Comments
There are no comments to display.