Jump to content

Andy90

Members
  • Posts

    189
  • Joined

  • Last visited

Blog Entries posted by Andy90

  1. Andy90

    C++ Classes
    I had already created a similar window handler for Leadwerks in a previous project. As I'm currently importing my project into the new engine, I thought it was time to refine this functionality.
    But what exactly can the Window Handler do?
    As the name suggests, it manages multiple "UI Windows" so that only one is displayed at a time. This is particularly useful when working with different windows for the UI, such as one for inventory, another for a crafting menu, and yet another for dialog options, etc. The Window Handler prevents multiple windows from being open simultaneously, as it closes all others before opening a new one.
    How does the Window Handler work?
    The functionality is kept simple: You create a new variable of type UIWindowHandler and generate your "UI Windows" using the UIWindow class, which you then add to the UIWindowHandler. The UIWindow class automatically creates the desired panels. By using `window_manager->ShowWindow("window_1");`, for example, you can open a specific window. In my project, I've added the UIWindowHandler to a global header so that my components can access it.
    UIWindowHandler.h
    #pragma once #include "UltraEngine.h" #include "UIWindow.h" using namespace UltraEngine; using namespace std; class UIWindowHandler { public: vector<UIWindow*> windows; UIWindowHandler(); void AddWindow(UIWindow* window); void CloseAllWindows(); void ShowWindow(UIWindow* window); void ShowWindow(string widnowName); }; UIWindowHandler.cpp
    #include "UltraEngine.h" #include "UIWindowHandler.h" UIWindowHandler::UIWindowHandler() { } void UIWindowHandler::AddWindow(UIWindow* window) { this->windows.push_back(window); } void UIWindowHandler::CloseAllWindows() { for (auto window : this->windows) { window->CloseWindow(); } } void UIWindowHandler::ShowWindow(UIWindow* window) { this->CloseAllWindows(); window->ShowWindow(); } void UIWindowHandler::ShowWindow(string widnowName) { this->CloseAllWindows(); for (auto window : this->windows) { if (window->name == widnowName) { window->ShowWindow(); } } } UIWindow.h
    #pragma once #include "UltraEngine.h" using namespace UltraEngine; class UIWindow { private: shared_ptr<Interface> ui; public: string name; shared_ptr<Widget> panel; UIWindow(string name, shared_ptr<Interface> ui); UIWindow(string name, float x, float y, float width, float height, shared_ptr<Interface> ui); bool IsHidden(); void CloseWindow(); void ShowWindow(); }; UIWindow.cpp
    #include "UltraEngine.h" #include "UIWindow.h" UIWindow::UIWindow(string name, shared_ptr<Interface> ui) { this->name = name; this->ui = ui; this->panel = CreatePanel(0, 0, 128, 128, ui->root); } UIWindow::UIWindow(string name, float x, float y, float width, float height, shared_ptr<Interface> ui) { this->name = name; this->ui = ui; this->panel = CreatePanel(x, y, width, height, ui->root); } bool UIWindow::IsHidden() { if (this->panel != nullptr) { if (this->panel->GetHidden()) { return true; } } return false; } void UIWindow::CloseWindow() { this->panel->SetHidden(true); } void UIWindow::ShowWindow() { this->panel->SetHidden(false); }  
  2. Andy90

    C++ Components
    This component generates a Mouse Movement Controller featuring a free-look mode. Typically, such character controllers find application in ARPG (Action Role-Playing Game) or RTS (Real-Time Strategy) games. In this setup, you observe the character from a top-down perspective and direct their movement to a specified position by clicking the left mouse button.

    The Component itself need the terrain name and the name for the walk animation. Also your character needs an idle animation. 
    You can switch into the "free look mode" by pressing the tab key. Within the "free look mode" you are able to move the camera with "W" "A" "S" "D" free around. If you press tab again you switch the mode back to the character. 
    1. C++ Header
    #pragma once #include "UltraEngine.h" #include "GatherWaterTask.h" using namespace UltraEngine; class MouseMovementController : public Component { private: shared_ptr<Map> scene; shared_ptr<NavMesh> nav_mesh; bool freeLook = false; public: string terrainName; string animationName; Vec3 destination; shared_ptr<NavAgent> agent; shared_ptr<Terrain> terrain; shared_ptr<Camera> camera; MouseMovementController(); void Start(); void Update(); bool Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags); bool Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags); shared_ptr<Component> Copy(); };  
    2. C++ Source File
    #include "UltraEngine.h" #include "MouseMovementController.h" MouseMovementController::MouseMovementController() { this->name = "MouseMovementController"; } /// <summary> /// Setup the Controller /// </summary> void MouseMovementController::Start() { auto entity = GetEntity(); ///Create the camera and attach them to the player character this->camera = CreateCamera(entity->GetWorld()); Vec3 currentcameraposition = TransformPoint(0, 15, 5, entity, nullptr); //this->camera->SetParent(entity); this->camera->SetPosition(currentcameraposition, true); this->camera->SetRotation(Vec3(45, 0, 0)); this->camera->SetFov(40); this->camera->Point(entity); ///Creates a navmesh ToDo: Has to changed when the NavMesh bug is fixed this->nav_mesh = CreateNavMesh(entity->GetWorld(), 5, 8, 8); this->nav_mesh->SetPosition(0, 0, 0); this->nav_mesh->Build(); ///Create a new NavAgent for the player character this->agent = CreateNavAgent(this->nav_mesh); entity->Attach(agent); } //Update method, called once per loop void MouseMovementController::Update() { auto entity = GetEntity(); auto window = ActiveWindow(); auto world = entity->GetWorld(); if (window) { ///Set the destination if the player hits the left mouse button if (window->MouseHit(MOUSE_LEFT)) { auto mousePosition = window->GetMousePosition(); auto pick = this->camera->Pick(window->GetFramebuffer(), mousePosition.x, mousePosition.y, 0, true); if (pick.success) { auto pos = pick.position; if (pick.entity->name == this->terrainName) { this->destination = pos; } } } ///Move to the destination if the destination is not null if (this->destination != NULL) { if (entity->position.DistanceToPoint(this->destination) > 1.0f) { this->agent->Navigate(this->destination); auto model = entity->As<Model>(); model->Animate(this->animationName); } else { this->agent->Stop(); auto model = entity->As<Model>(); model->Animate("idle"); this->destination == NULL; } } ///Toggle the freelook mode if (window->KeyHit(KEY_TAB)) { if (this->freeLook) { this->freeLook = false; } else { this->freeLook = true; Vec3 currentcameraposition = TransformPoint(0, 15, 0, GetEntity(), nullptr); this->camera->SetPosition(currentcameraposition, true); this->camera->SetRotation(Vec3(90, 0, 0)); } } ///Position the camera according to the selected mode if (!this->freeLook) { Vec3 currentcameraposition = TransformPoint(0, 15, 5, GetEntity(), nullptr); this->camera->SetPosition(currentcameraposition, true); this->camera->Point(entity); } else { Vec3 camPos = this->camera->GetPosition(); if (window->KeyDown(KEY_W)) { camPos.z += 0.5f; } else if (window->KeyDown(KEY_S)) { camPos.z -= 0.5f; } else if (window->KeyDown(KEY_A)) { camPos.x -= 0.5f; } else if (window->KeyDown(KEY_D)) { camPos.x += 0.5f; } this->camera->SetPosition(camPos); } } } bool MouseMovementController::Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags) { this->scene = scene; this->terrainName = (String)properties["terrainName"]; this->animationName = (String)properties["animationName"]; for (auto e : scene->entities) { if (e->name == terrainName) { this->terrain = std::static_pointer_cast<Terrain>(e); } } return true; } bool MouseMovementController::Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags) { properties["terrainName"] = (String)terrain->name; properties["animationName"] = (String)this->animationName; return true; } shared_ptr<Component> MouseMovementController::Copy() { return std::make_shared<MouseMovementController>(*this); } 3. JSON File
    { "component": { "properties": [ { "name": "terrainName", "label": "Terrain Name", "value": "" }, { "name": "animationName", "label": "Animation Name", "value": "" } ] } }  
  3. Andy90
    In this article, I would like to delve into the concept of "Game Logs" and explain why they play an important role. First, however, we should clarify what exactly a Game Log is.
    The Game Log allows us to log and temporarily store various actions in the game. Furthermore, with the help of the Game Log, we can inform independent systems about various actions. A good example of this is a simple quest system, similar to the one in World of Warcraft. In such systems, we want to complete quests by, for example, looting a specific item, killing a certain enemy, or interacting with a specific NPC. But how are the individual quests supposed to know exactly when we loot an item? Sure, one could simply search through the inventory. However, that would only work for quest types where we are supposed to loot an item, and not for those where we are supposed to kill a specific enemy or talk to an NPC. This is where the Game Log proves to be extremely useful, as our quests can simply search through the entries to update themselves accordingly.
    In the past few days, I have been working on implementing such a system in C++. It is important to note that this is not a component; instead, a "Global.h" file is required, where the log is defined as a global variable.
    GameLog.h
    #pragma once #include "UltraEngine.h" using namespace UltraEngine; /// <summary> /// Event types /// </summary> enum GameLogEvent { GAME_LOG_LOOT_ITEM, GAME_LOG_KILL_ENEMY }; /// <summary> /// Game Log entrys /// </summary> struct GameLogEntry { String uuid; uint64_t timestamp; GameLogEvent event; table data; }; /// <summary> /// GameLogListner forward declaration /// </summary> class GameLogListener; /// <summary> /// Handles the game log entrys /// </summary> class GameLog { private: vector<shared_ptr<GameLogListener>> m_listeners; vector<GameLogEntry> m_log; public: void Log(GameLogEvent event, table eventData); void AddListener(shared_ptr<GameLogListener> listener); void LogLootItem(String itemName, int itemCount); void LogKilledEnemy(String enemyName); void Clean(uint64_t time); vector<GameLogEntry> FetchLog(uint64_t startTime); }; GameLog.cpp
    #include "UltraEngine.h" #include "GameLog.h" #include "GameLogListener.h" void GameLog::Log(GameLogEvent event, table eventData) { GameLogEntry entry; entry.uuid = Uuid(); entry.timestamp = Millisecs(); entry.event = event; entry.data = eventData; m_log.push_back(entry); for (auto listener : m_listeners) { if (listener->Event == event) { listener->Callback(eventData); } } Print("Added data to gamelog"); } void GameLog::AddListener(shared_ptr<GameLogListener> listener) { m_listeners.push_back(listener); } void GameLog::LogLootItem(String itemName, int itemCount) { table data; data["itemName"] = itemName; data["itemCount"] = itemCount; Log(GAME_LOG_LOOT_ITEM, data); } void GameLog::LogKilledEnemy(String enemyName) { table data; data["enemyName"] = enemyName; Log(GAME_LOG_KILL_ENEMY, data); } void GameLog::Clean(uint64_t time) { vector<GameLogEntry> newEntrys; for (auto entry : m_log) { auto diff = Millisecs() - entry.timestamp; if (diff <= time) { newEntrys.push_back(entry); } else { Print("Entry " + entry.uuid + " out of time!"); } } m_log = newEntrys; } vector<GameLogEntry> GameLog::FetchLog(uint64_t startTime) { vector<GameLogEntry> result; for (auto entry : m_log) { if (entry.timestamp > startTime) { result.push_back(entry); } } return result; }  
    In the file GameLog.h, we find an enum with various events, to which more can be added. Currently, there are entries for looting an item and killing an enemy.
    Furthermore, the structure "GameLogEntry" defines various variables, including a string for a UUID, a long variable for the timestamp, the event, and the data of the entry.
    Following that is the main class, which contains various functions as well as two lists. One list is for so-called hooks, and the other is for the log itself.
    Here's an overview of the functions:
     
    Log(): Adds entries to the log. AddListener(): Adds a callback (no callback is needed in this article). LogLootItem(): Simplifies adding a loot event to the game's log. LogKilledEnemy(): Simplifies adding a kill event. Clean(uint64_t time): Deletes all entries older than the specified parameter in milliseconds. vector<GameLogEntry> FetchLog(uint64_t startTime): Returns a list of log entries whose timestamp is greater than the start time in the parameter.  
    If we want to determine whether we have looted an item or killed a specific enemy, we can simply retrieve the log. It is important to store the time of the last query so that we only receive the entries that are still unknown to us.
    void VitalUI::UpdateNotifications() { m_notifyer->ClearItems(); auto fetchTime = Millisecs() - 5000; auto log = g_log.FetchLog(fetchTime); for (auto entry : log) { if (entry.event == GAME_LOG_LOOT_ITEM) { m_notifyer->AddItem("Looted " + String(entry.data["itemName"]) + " (X" + String(entry.data["itemCount"]) + ")"); } } if (m_notifyer->items.size() > 0) { m_notifyer->Redraw(); } } To ensure that the log does not grow infinitely, it is advisable to clear it regularly. In my implementation, I clear the log every 6 minutes in the main loop. That should be sufficient to process all events.
    while (window->Closed() == false and window->KeyDown(KEY_ESCAPE) == false) { while (PeekEvent()) { const Event ev = WaitEvent(); g_ui->GetInterface()->ProcessEvent(ev); switch (ev.id) { case EVENT_WIDGETACTION: break; case EVENT_WINDOWCLOSE: return 0; break; } } world->Update(); world->Render(framebuffer, false, 0); g_log.Clean(60000); } As you can see, a Game Log has become extremely practical and indispensable in modern games. If you have any questions about this article, feel free to contact me. Additionally, it's worth mentioning that the log can be used for many other purposes and is not limited to just a quest system.
     
    Gamelog.zip
  4. Andy90
    Due to several inquiries about how I created my inventory, crafting, and storage systems, I would like to explain my approach in more detail. In this blog post, I won't use any code but rather focus on explaining the underlying logic.
    1. The InventoryItem Class

    The foundation of these systems is an Item class, which I abstractly created to represent different types of items (materials, placeable objects, weapons, consumables, etc.). The reason for an abstract class is straightforward: it allows me to pack all items into a vector and call defined functions based on the situation, such as OnPickup, OnDrop, and OnUse.
     Additionally, I've created an Item Definition JSON, which defines items, including details like icon, name, maximum stack size, and item type. However, this isn't crucial and can be skipped.
    2. The Gridview Widget

    After defining my base class, I created a custom widget for a GridView, as I introduced in a previous post. However, this widget doesn't use the previously created Item class; instead, it has its own GridView items. Why? Because it's crucial to separate data from the GUI, and I can use it in various other projects. The GridView serves to visually represent the items, and we'll delve into how it works later.
    3. How to Display InventoryItems within the Widget

    Now that I have the basic Item class and a GridView for displaying inventory content, let's move on to the inventory itself. The Inventory class is not a component; rather, it's a simple C++ class that includes a vector for items and various functions to add, remove, and retrieve items. Additionally (for simplicity), I created a reference to a Grid Widget for the inventory. When a change in items occurs, this GridView is updated. The process involves removing GridView items and adding new GridView items based on the inventory's items.
    Now I have an inventory that shows the items the player has in their inventory.
    4. New Items for the Player

    Now let's talk about how the player obtains items, which is quite simple. For this, I created a component that assigns items to the inventory based on various conditions, such as the passage of time.
    5. The Crafting UI & Concept

    Since we can now gather items and see them in the inventory, let's do more with these items. We want to craft ItemZ from ItemX and ItemY. For crafting itself, I've also created a component (named Workbench) and its own UI. This is a bit more complex, so I'll try to explain it as precisely as possible.
    The UI has two GridViews and a ListView, along with various other widgets that aren't as important. What's crucial is that I only have one UI managing the workbenches. For this, I created two global variables: one for the panel (the UI) and one for the component that's currently the owner of the UI. When the player opens the UI using a workbench, the UI owner is also set simultaneously. It can only be reopened after being closed, as the owner is then set back to a null pointer. When opening, I load all recipes into the ListView. The recipes themselves are defined in a JSON file. When the player selects an item they want to craft, the required materials are loaded into the first GridView. This serves purely as visual information and has no further functions.
    6. Drag & Drop Items

    Now let's discuss how I allow the player to place items from their inventory into the crafting window. For this, I created another class called ItemMover. Its task is to temporarily pick up an item and then transfer it. I use the events sent by the GridView. When the player performs a mouse drag, I now pick up the item(s) from the inventory slot and set it as a reference to the ItemMover. If I then receive the MouseDown event on another GridView, I check if the ItemMover has an item. If so, I add the item from the Mover to the Workbench's vector, update the UIs, and delete the item from the Mover. To make this look visually appealing with drag and drop, I created a simple widget that becomes visible when the ItemMover holds an item and is always set to the mouse's position. It becomes invisible on item drop.
    7. The Crafting itself

    Crafting itself is again quite simple. When the player clicks the button, the Workbench checks if the vector of materials (using the Item class again) has enough of the required items. If yes, the item is crafted and added to the vector of materials.
    8. Get the Crafted items into the inventory

    To allow the player to now take out the crafted item, we again use the ItemMover and drag-and-drop functions.
    9. Done!

    As you can see, the whole system is quite complex, making it challenging to provide any specific files due to many dependencies among themselves and with other components. I hope I have explained it comprehensibly. If you have further questions, feel free to ask them in the comments.
     
    Watch the video how it works https://streamable.com/r2d0x5
×
×
  • Create New...