Jump to content

C++ Beginner's Guide - first map, Main Menu, Loading Screen and GUI


Dreikblack

60 views

 Share

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:

image.thumb.png.d531001fa979287483c5e3691b18d936.png

You can add skybox in Edit > World Settings (at top buttons in the Editor) by choosing skybox.dds from Materials\Environment\Default
image.png.bd8059101bd1a69116f0f7bc73951a5a.png

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:

image.thumb.png.cd71500a6807d7c17912c70bab8fb49d.png

Before making new classes it's better to do enable "Show All Files" mode to be able see actual folders in the project:
image.png?ex=676c2b4e&is=676ad9ce&hm=9726c99cebedb5e3ed8519c76023df8a57bb48c9bedb799b0cdeb7aed9949b74&

 

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!

image.thumb.png.26bb6a95fc99bc42596f2f4ace54bdd6.png

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:

image.thumb.png.754ac879af57796db1f32a581d6da1e2.png

All created and modified classes:

FPS game with menu.zip

Maps:

Maps.zip

Repository: https://github.com/Dreikblack/CppTutorialProject/tree/menu-loading-screen-and-gui

  • Like 1
 Share

0 Comments


Recommended Comments

There are no comments to display.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...