Search the Community
Showing results for tags 'C++'.
-
In this tutorial we will add features to real-time tactic/strategy game in Ultra Engine fro prev tutorial: Plan: 1. Add flag model to use when pointing where to go for units 2. Make save/load system with quick save 3. Make blood particle emitter which will be used on hits and use it in Unit::Methood 4. Make blood decals which will be on the ground after death and spawn it in Unit::Kill 5. Download and use sounds for units getting damage. In this tutorial used 2117 Build from Steam Beta branch (Dev in Standalone version). In 0.9.8 version Component::Load/Save are protected. Build number can be found in the Editor Help->About Flag Flag will represent a place that player units will try to reach. Model can be found here: https://sketchfab.com/3d-models/triangle-flag-adadd59fd56d44f7b0354d1caba38f95 Download .glb version and put it to new folder "triangle_flag" in Models. Convert to mdl in the Editor. By default this model is too small. Set Scale 5.0 in Transform tab and in Tools click Reset Transform (works properly atm only for non-animated models) to save new size for good. Also click on Collapse in Tools to unite all meshes so it would be shown like single entity in a scene (otherwise this entity will have some kids which is not critical, but it's better to make it simpler). triangle_flag.zip Flag prefab: 1. Put triangle_flag model to scene 2. In Physics Collision type - None and Pick Mode - None 3. Attach WayPoint component 4. In Appearance tab in add "Save" to tags - it will be needed eventually for game save/load system 5. Save as prefab called FlagWayPoint in Prefabs folder FlagWayPoint.zip Updating classes Let's update Unit class for using getting an entity as target point and save system later. Add new method to Unit.h: void goTo(shared_ptr<Entity> targetPointEntity, bool isForced = false); In Unit.cpp method implementation: void Unit::goTo(shared_ptr<Entity> targetPointEntity, bool isForced) { if (targetPointEntity) { isForcedMovement = isForced; targetPoint = targetPointEntity; goTo(); } } Add to Start() a new tag: entity->AddTag("Save"); And also in Start() after that: if (seq != -1) { //to disable pain state at end of pain animation model->skeleton->AddHook(seq, count - 1, EndPainHook, Self()); } Add this: if (health <= 0) { seq = model->FindAnimation(deathName); int count = model->CountAnimationFrames(seq); model->Animate(deathName, 1.0f, 250, ANIMATION_ONCE, count - 1); } It's needed to make Unit make lie down in death animation if it was killed when game was save. Add a little bit below after healthBar initialization nex tline: healthBar->SetScale((float)health / (float)maxHealth, 1, 1); So health bar would have correct size after a game loading. We also want to save selected state after a loading so add first line to Load and second to Save: if (properties["isSelected"].is_boolean()) isSelected = properties["isSelected"]; properties["isSelected"] = isSelected; After line this line in Kill method: ListenEvent(EVENT_TIMERTICK, removeEntityTimer, RemoveEntityCallback, Self()); Add this: //not saving if supposed to be deleted anyway entity->RemoveTag("Save"); Current Unit: Unit.zip First version of TopDownCamera had an issue that would appear after game load: Update init() method to make it update pivot after game load: if (gameCamera && !targetPivot.lock()) { gameCamera->Listen();//for positional sound gameCamera->SetSweptCollision(true);//for removing pop up effect after quick move/turn gameCamera->SetRotation(CAMERA_PITCH, gameCamera->GetRotation(true).y, gameCamera->GetRotation(true).z); auto targetPivotShared = CreatePivot(gameCamera->GetWorld()); targetPivotShared->SetPickMode(PICK_NONE); sceneWeak.lock()->AddEntity(targetPivotShared); targetPivot = targetPivotShared; } //setting position and rotation here in case of game load gameCamera->SetParent(nullptr); auto targetPivotShared = targetPivot.lock(); auto targetPosition = getCirleCenter(gameCamera->GetPosition(true), gameCamera->GetRotation(true)); targetPosition.y = 0; targetPivotShared->SetPosition(targetPosition); targetPivotShared->SetRotation(0, gameCamera->GetRotation(true).y, gameCamera->GetRotation(true).z); gameCamera->SetParent(targetPivotShared); return true; TopDownCamera.zip In StrategyController.cpp update Start to add tag for Save next to another tag line entity->AddTag("Save"); To Save method add saving selected units: properties["selectedUnits"] = {}; int index = 0; for (auto const& selectedUnitWeak : selectedUnits) { auto selectedUnit = selectedUnitWeak.lock(); if (selectedUnit) { properties["selectedUnits"][index] = selectedUnit->GetUuid(); index++; } } Add their loading to Load: selectedUnits.clear(); if (properties["selectedUnits"].is_array()) { for (int i = 0; i < properties["selectedUnits"].size(); i++) { auto unit = scene->GetEntity(properties["selectedUnits"][i]); if (unit) { selectedUnits.push_back(unit); } } } Find this line for (auto const& entityWeak : selectedUnits) { Replace a code from starting from line above this line and ended 8 lines lower with this snippet: } else if (!selectedUnits.empty()) { auto flag = LoadPrefab(camera->GetWorld(), "Prefabs/FlagWayPoint.pfb"); if (flag) { flag->SetPosition(pick.position); } for (auto const& entityWeak : selectedUnits) { auto entityUnit = entityWeak.lock(); if (entityUnit && entityUnit->GetComponent<Unit>()) { if (flag) { entityUnit->GetComponent<Unit>()->goTo(flag, true); } else { entityUnit->GetComponent<Unit>()->goTo(pick.position, true); } } } } Or just download full class: StrategyController.zip Custom Save/Load system Current official system have a lot of flaws which are listed here: Most of them will be fixed, but something like no loading entities, which were added while run time like flags in this game, may still persist. This one is resolved by loading prefabs. You can also add manual recreation for most types of entities, depends on needs, but prefabs should be enough in most cases. Game class have a lot of changes, which were made to create custom save system. Whole 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> fpsPlayer; shared_ptr<Widget> menuPanel; shared_ptr<Widget> gameSavedLabel; //hide gameSavedLabel on timer shared_ptr<Timer> gameSavedLabelTimer; Game(); void init(shared_ptr<Framebuffer> framebuffer, WString mapPath); //update basic entity properties - position, rotation, tags void loadEntity(shared_ptr<Entity> entity, table entityTable); void loadGame(table saveTable); void saveGame(WString saveName); static bool QuickSaveGameCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra); static bool HideGameSavedLabelCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra); 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); //for game loading static std::shared_ptr<Game> create(shared_ptr<Framebuffer> framebuffer, table saveTable); 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; } std::shared_ptr<Game> Game::create(shared_ptr<Framebuffer> framebuffer, table saveTable) { struct Struct : public Game {}; auto instance = std::make_shared<Struct>(); std::string mapName = saveTable["MapPath"]; instance->init(framebuffer, WString(mapName)); instance->loadGame(saveTable); 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); if (game->fpsPlayer) { //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); } game->fpsPlayer->doResetMousePosition = !isHidden; } //If the callback function returns false no more callbacks will be executed and no event will be added to the event queue. //to avoid double call return false; } return true; } 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) { fpsPlayer = 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 - 300 / 2, 300, 250, ui->root); gameSavedLabel = CreateLabel("GAME SAVED", frameSize.width / 2 - 100, 50, 200, 30, ui->root); gameSavedLabel->SetFontScale(2.0f); gameSavedLabel->SetHidden(true); 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 ^ ListenEvent(EVENT_KEYUP, nullptr, QuickSaveGameCallback, Self()); } void Game::loadEntity(shared_ptr<Entity> entity, table entityTable) { if (entityTable["position"].is_array() && entityTable["position"].size() == 3) { entity->SetPosition(entityTable["position"][0], entityTable["position"][1], entityTable["position"][2], true); } if (entityTable["rotation"].is_array() && entityTable["rotation"].size() == 3) { entity->SetRotation(entityTable["rotation"][0], entityTable["rotation"][1], entityTable["rotation"][2], true); } if (entityTable["tags"].is_array()) { for (int i = 0; i < entityTable["tags"].size(); i++) { entity->AddTag(std::string(entityTable["tags"][i])); } } } void Game::loadGame(table saveTable) { //old-new entity id vector<std::pair<String, String>> uuids; std::set<String> newEntities; //entites that was not in scene will be deleted once Components will be loaded and gets needed entities std::set<shared_ptr<Entity>> entitiesToRemoveFromScene; //iterating std::map by key (uuid) and value (entityTable) instead of pair for (auto& [uuid, entityTable] : saveTable["SavedEntities"]) { auto entity = scene->GetEntity(uuid); //load properties for saved entity that was initially on map if (entity) { loadEntity(entity, entityTable); //or if it was not we can recreate prefab at least } else if (entityTable["prefabPath"].is_string()) { //spawn saved entity that was not initially on map auto spawnedEntity = LoadPrefab(world, String(entityTable["prefabPath"])); if (!spawnedEntity) { continue; } scene->AddEntity(spawnedEntity); if (entityTable["isInScene"].is_boolean() && !entityTable["isInScene"]) { entitiesToRemoveFromScene.insert(spawnedEntity); } loadEntity(spawnedEntity, entityTable); uuids.push_back(std::pair(uuid, spawnedEntity->GetUuid())); newEntities.insert(spawnedEntity->GetUuid()); } } //delete not saved entities for (auto const& entity : world->GetTaggedEntities("Save")) { //does newEntities containes curent entity if (newEntities.find(entity->GetUuid()) != newEntities.end()) { //skip new entity continue; } //if supposed to be saved and was not due being removed when save was made then remove it from scene table entityTable = saveTable["SavedEntities"][entity->GetUuid()]; if (entityTable.empty()) { scene->RemoveEntity(entity); } } //saving table++ as a string auto saveString = String(saveTable.to_json()); //replace entities ids so component would use new ones for (auto const& [oldUuid, newUuid] : uuids) { saveString = saveString.Replace(oldUuid, newUuid); } //converting back to table++ from string saveTable = ParseJson(saveString); //Load saved data to components for (auto const& entity : world->GetTaggedEntities("Save")) { auto& entityTable = saveTable["SavedEntities"][entity->GetUuid()]; for (auto const& component : entity->components) { component->Load(entityTable, nullptr, scene, LOAD_DEFAULT, nullptr); } } //starting components now when all data is there for (auto const& entity : world->GetTaggedEntities("Save")) { for (auto const& component : entity->components) { component->Start(); } } //removing from scene entites that scene had not for (auto const& entity : entitiesToRemoveFromScene) { scene->RemoveEntity(entity); } } void Game::saveGame(WString saveName) { table saveTable; //saving map path to use it later to load correct map saveTable["MapPath"] = RelativePath(scene->path).ToUtf8String(); saveTable["SavedEntities"] = {}; for (auto const& entity : world->GetTaggedEntities("Save")) { table entityTable; for (auto const& component : entity->components) { component->Save(entityTable, nullptr, scene, SAVE_DEFAULT, nullptr); } //just to make save file more readable if (!entity->name.empty()) { entityTable["name"] = entity->name.ToUtf8String(); } //saving position and rotation of entity to restore them in Load auto position = entity->GetPosition(true); entityTable["position"] = {}; entityTable["position"][0] = position.x; entityTable["position"][1] = position.y; entityTable["position"][2] = position.z; auto rotation = entity->GetRotation(true); entityTable["rotation"] = {}; entityTable["rotation"][0] = rotation.x; entityTable["rotation"][1] = rotation.y; entityTable["rotation"][2] = rotation.z; entityTable["tags"] = {}; //to remove it from scene later once everything restored inc case if only components supposed to keep this entity entityTable["isInScene"] = scene->GetEntity(entity->GetUuid()) ? true : false; int tagIndex = 0; for (auto& tag : entity->tags) { entityTable["tags"][tagIndex] = tag.ToUtf8String(); tagIndex++; } //save prefab path to be able restore entity if it was added to scene later as prefab auto prefab = entity->GetPrefab(); if (prefab) { entityTable["prefabPath"] = RelativePath(prefab->GetPath()).ToUtf8String(); } //using entity id as key for its properties saveTable["SavedEntities"][entity->GetUuid()] = entityTable; } SaveTable(saveTable, saveName); //showing "Game Saved" labl for 2 seconds gameSavedLabel->SetHidden(false); gameSavedLabelTimer = UltraEngine::CreateTimer(2000); ListenEvent(EVENT_TIMERTICK, gameSavedLabelTimer, HideGameSavedLabelCallback, Self()); } bool Game::QuickSaveGameCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra) { if (KEY_F5 == ev.data && extra) { auto game = extra->As<Game>(); game->saveGame("QuickSave.save"); } return true; } bool Game::HideGameSavedLabelCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra) { if (extra && extra->As<Game>()) { auto game = extra->As<Game>(); game->gameSavedLabel->SetHidden(true); game->gameSavedLabelTimer->Stop(); game->gameSavedLabelTimer = nullptr; } return false; } Game.zip Add new functions to main.cpp: void LoadGame(WString savePath) { table saveTable = LoadTable(savePath); if (saveTable == nullptr) { return; } menu.reset(); //to avoid random error in World::Update() game.reset(); loadingWorld->Render(framebuffer); if (currentWorld) currentWorld.reset(); if (currentUi) currentUi.reset(); game = Game::create(framebuffer, saveTable); currentWorld = game->world; currentUi = game->ui; } bool QuickLoadGameCallback(const Event& event, shared_ptr<Object> extra) { if (KEY_F9 == event.data) { LoadGame("QuickSave.save"); } return true; } And add ListenEvent next to other similar lines: ListenEvent(EVENT_KEYUP, nullptr, QuickLoadGameCallback); main.zip Now you can save and load game with F5 and F9 respectively Making Particle Effect Download this blood texture made by TobiasM: https://opengameart.org/content/blood-splat It's not optimal for particle emitter, but good enough for learning purpose. Put into Materials\Particles folder and convert to .dds in the Editor. Generate a material from dds texture. Add to any scene particle emitter: In Appearance tab of Particles choose a recently made material. Now we able to see result when we will change this material properties. Open material: 1. Choose Lambertian shader family - it has better perfomance than PBR and it can make a difference eventually with many particle emitters which produce dozens particles. 2. In Blend tab check Transparent checkbox. Now it particles would look like that: Open Particles tab of Particle Emitter in entity properties: 1. Particle count - how many particles of this emitter exist at same time, left it as 50 2. Emission shape - shape of area where particles will be spawn 3. Emission Area - size of this area (width, height, depth). If 0 then particles spawns at same point. Left it as 0. 4. Burst frequency - how often particles spawns. Will be 800 5. Burst count - how much particles spawns in every burst. Enter 50 6. Colors - how particle looks at spawn and despawn 7. Velocity - direction and speed - make it 0 8. Acceleration - same but it's accel 9. Size - width and height of particle. No depth, because every particle is just a billboard sprite. Makie it 0.2/0.2 10. Radius. First value - relative size (i.e. 1.0 is 100%) for particle at start and second value is size when particles despawns. Value - 1.0/1.0 11. Turbulence - random speed in random direction. With 0 velocity it will make particles move in different directions. Make it 300.0 12. Rotation speed - how fast particle rotate. Make it 0 Download ParticleEffect to ParticleEffect component to Components\Appearance folder. Include into project and add to ComponentSystem as usual. Add this component to an emitter. It's needed to make blood hit effect to dissapear before it would burst again. Temporary checkbox on and Duration 700. Name emitter as BloodHit and save it as prefab "BloodHit.phb" in Prefabs folder Let's use it for units. In Unit.cpp in Damage method add next code somewhere above auto now = world->GetTime(); auto bloodHit = LoadPrefab(world, "Prefabs/BloodHit.pfb"); auto entity = GetEntity(); if (bloodHit) { auto bloodHitPosition = entity->GetPosition(true); //to lift it up to entity center bloodHitPosition.y = bloodHitPosition.y + entity->GetBounds(BOUNDS_GLOBAL).size.height / 2; bloodHit->SetPosition(bloodHitPosition, true); //prefabs component are not started after its load so will do it manually for (auto const& component : bloodHit->components) { component->Start(); } } Making decal Copy blood_splat.dds texture to Materials\Decals folder. Generate a new material from it and name BloodPuddle. Edit this material: 1. Keep PBR as shader family. 2. In Blend tab check Transparent checkbox. 3. In Surface Metalness 0, and Roughness 50. Add new decal to scene - 256x256 size with 16 height (or 250x250 and 20) Choose new material in appearance. Save this decal as prefab wiht BloodPuddle name. We will spawn this decal after unit death. In Unit::Kill() method add above auto model = entity->As<Model>(); line following code: auto scene = sceneWeak.lock(); auto bloodPuddle = LoadPrefab(entity->GetWorld(), "Prefabs/BloodPuddle.pfb"); if (bloodPuddle && scene) { auto bloodHitPosition = entity->GetPosition(true); bloodPuddle->SetPosition(bloodHitPosition, true); //to keep it scene->AddEntity(bloodPuddle); } Pain sounds Download sounds for Warrok: https://opengameart.org/node/132772 And for Paladin: https://opengameart.org/content/pain-sounds-by-emopreben In Sounds folder create Units subfolder and put there bear_02.ogg from 1st pack and "Painsounds v2 - Track 5 - Urggh.ogg" from 2nd Add to properties in Unit.json: { "label": "Pain sound", "name": "painSound", "value": "", "filecategory": "SOUND" }, New member in Unit.h header: shared_ptr<Sound> painSound; In Load method in Unit.cpp will load attached in the Editor sound: if (properties["painSound"].is_string()) painSound = LoadSound(std::string(properties["painSound"])); And play this sound in Damage() above auto now = world->GetTime(); line: if (painSound) { entity->EmitSound(painSound); } Unit class: FinalUnit.zip Now open Paladin prefab and add their pain sound in Unit component. Same for Warror. Delete units from Strategy map and add them again from prefabs. Remember to break prefab after that. strategy.zip Cache In debug mode you could notice micro-freezes when prefabs are loaded every time when it's not at the map. Thats because unload from memory an assets that have no shared_pointer to them. We can fix by making cache in Game class. Add to Game.h new member: vector<shared_ptr<Entity>> prefabCache; And at the end of init method in Game.cpp: //caching prefabCache.push_back(LoadPrefab(world, "Prefabs/BloodPuddle.pfb")); prefabCache.push_back(LoadPrefab(world, "Prefabs/BloodHit.pfb")); prefabCache.push_back(LoadPrefab(world, "Prefabs/FlagWayPoint.pfb")); for (auto const& prefab : prefabCache) { if (prefab) { prefab->SetHidden(true); } } GameCache.zip Now Debug mode will be smoother since assets loaded on game load and not in run time when components uses them. In Release mode it would be also necessary to do for heavy assets. Final version on github: https://github.com/Dreikblack/CppTutorialProject/tree/4-save-load-particles-decals
-
2 downloads
Little component that can be used for Particle Emitter that should be deleted in time Temporary - will component keep entity pointer to delete it once time is out Reducing Effect - decrease particles Velocity and Turbulence with time Duration - time before entity pointer will be deleted Case uses: explosions, blood hits, bleeding effect etc. -
In this tutorial we will make a simple real-time tactic/strategy game in Ultra Engine. Plan: 1. Download and import ground material, 2 characters models with animations 2. Create Unit component with bot behavior. 3. Add control over player units. 4. Making prefabs and new map In this tutorial used 2038 Build from Steam Beta branch (Dev in Standalone version). Build number can be found in the Editor Help->About Asset import Download and unpack ground material from there in Materials\Ground folder Make in Models folder Characters in it Warrok and Paladin subfolders. Now login into https://www.mixamo.com/ Find there Paladin character and download with these settings without animations to get T-Pose as standard bind pose: Now we need animations from the Sword And Shield series (type Shield into the search field on top to filter them): Idle, Run, lash, Impact, Death. Hit "In Place" Checkbox to avoid character offset while animations: Put these .fbx to Paladin folder. Convert them into .mdl with right click in project tab of the Editor if there were not already If material was not created correctly then take pngs textures from "Paladin WProp J Nordstrom.fbm" folder which was created by converted and put in Paladin folder Convert them to DDS with Right Click. You can delete png and fbx files now. Now Paladin should look textured. Rename "Paladin WProp J Nordstrom" intro "Paladin" just for convenience. Open Paladin.mdl with double click to open Model Viewer. Let's load animations into this model from models with animations ("Sword And Shield Idle.mdl" etc): We need to rename those animations to use them properly later - open the Model tab, select animation to rename. You can play it to find out which is what. Lower will appear Animation panel where you can click on name to rename it: Let's call these animations: Idle, Attack, Pain, Death. There is also animation with T-Pose which can be deleted with Tools->Remove Sequence. We don't need animations files anymore and Paladin folder content should looks like that: One more thing needs to be done for model - Collider. In View toggle Show Collider, open Model tab, select SKIN_MESH and in Physics choose Cylinder collider. Offset and Size settings: We need worth enemy for our paladins - download Warrok model and animations from Mutant series - Idle, Run, Dying, Punch. For pain i chose "Standing React Large From Right" animation. Next do the same stuff as it was with Paladin - material, animations etc. In Transform make scale 0.85 so it would match Paladin size. Collider settings: Unit component After preparing character models we now need a component which will have: Unit params: health, speed, damage etc. Playing animations Bot behaviour - move and attack nearby enemies Input for player to move and attack something specific Create in "Source\Components\AI" Unit.json, Unit.h, Unit.cpp files and include last two into project. Unit.json: { "component": { "properties": [ { "name": "enabled", "label": "Enabled", "value": true }, { "name": "isFullPlayerControl", "label": "Full Player Control", "value": false }, { "name": "isPlayer", "label": "Is Player Unit", "value": false }, { "name": "team", "label": "Team", "value": 1, "options": [ "Neutral", "Good", "Bad" ] }, { "name": "health", "label": "Health", "value": 100 }, { "name": "maxHealth", "label": "Max Health", "value": 100 }, { "name": "speed", "label": "Speed", "value": 3.0 }, { "name": "attackRange", "label": "Attack Range", "value": 2.0 }, { "name": "attackDamage", "label": "Attack Damage", "value": 30 }, { "name": "attackFrame", "label": "Attack Frame", "value": 5 }, { "name": "painCooldown", "label": "Pain Cooldown", "value": 1000 }, { "name": "decayTime", "label": "Decay Time", "value": 10000 }, { "name": "target", "label": "Target", "value": null }, { "name": "targetPoint", "label": "Target Point", "value": null }, { "name": "attackName", "label": "Attack Name", "value": "Attack" }, { "name": "idleName", "label": "Idle Name", "value": "Idle" }, { "name": "painName", "label": "Pain", "value": "Pain" }, { "name": "deathName", "label": "Death", "value": "Death" }, { "name": "runName", "label": "Run", "value": "Run" } ], "inputs": [ { "name": "Enable" }, { "name": "Disable" } ] } } Parameters descriptions can be found in Unit.h: #pragma once #include "UltraEngine.h" #include "../BaseComponent.h" using namespace UltraEngine; //abstract class which will be a parent for other units classes such as Beast and Hunter //partly based of Enemy/Monster/Player default classes class Unit : public BaseComponent { protected: //so it could be added for entity with FPS Player component bool isFullPlayerControl = false; int health = 100; int maxHealth = 100; //used for AI navigation, weak_ptr just to make sure that component will not keep it if stays after map unload somehow std::weak_ptr<NavMesh> navMesh; //unique per entity so shared_ptr //NavAgent used to create to plot navigation paths in NavMesh std::shared_ptr<NavAgent> agent; //how far AI see its enemies in meters float perceptionRadius = 10; //how long to pursue when out of radius float chaseMaxDistance = perceptionRadius * 2; //is target a priority bool isForcedTarget = false; //target to follow and attack if possible std::weak_ptr<Entity> targetWeak; //to avoid fighting bool isForcedMovement = false; //which distance to point should be to reach it float targetPointDistance = 0.5f; //place to reach std::shared_ptr<Entity> targetPoint; //is attack animation playing bool isAttacking = false; //when attack started uint64_t meleeAttackTime = 0; //do damage in meleeAttackTiming after attack start int attackFrame = 5; float attackRange = 2.0f; int attackDamage = 30; //pain/git hit state bool isInPain = false; //can't start new pain animation immediately to avoid infinite stugger int painCooldown = 300; //when pain animation started uint64_t painCooldownTime; //how fast unit is float speed = 3.0; //when to try scan again uint64_t nextScanForTargetTime = 0ULL;//unsigned long long //animations names WString attackName; WString idleName; WString painName; WString deathName; WString runName; //health bar above unit shared_ptr<Sprite> healthBar; shared_ptr<Sprite> healthBarBackground; bool isSelected = false; //to keep camera pointer for unit health bars std::weak_ptr<Camera> cameraWeak; //to be able to remove entity inside of component later std::weak_ptr<Scene> sceneWeak; //time in ms before delete model after a death, 0 of disabled int decayTime = 10000; shared_ptr<Timer> removeEntityTimer; static bool RemoveEntityCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra); virtual void scanForTarget(); bool goTo(); //pick filter static bool RayFilter(shared_ptr<Entity> entity, shared_ptr<Object> extra); //attack target if in range static void AttackHook(shared_ptr<Skeleton> skeleton, shared_ptr<Object> extra); //disable attacking state static void EndAttackHook(shared_ptr<Skeleton> skeleton, shared_ptr<Object> extra); //disable pain state static void EndPainHook(shared_ptr<Skeleton> skeleton, shared_ptr<Object> extra); public: int team = 0;//0 neutral, 1 player team, 2 enemy bool isPlayer = false; Unit(); shared_ptr<Component> Copy() override; void Start() override; bool Load(table& t, shared_ptr<Stream> binstream, shared_ptr<Scene> scene, const LoadFlags flags, shared_ptr<Object> extra) override; bool Save(table& t, shared_ptr<Stream> binstream, shared_ptr<Scene> scene, const SaveFlags flags, shared_ptr<Object> extra) override; //deal a damage to this unit by attacker void Damage(const int amount, shared_ptr<Entity> attacker) override; //kill this unit by attacker void Kill(shared_ptr<Entity> attacker) override; bool isAlive(); void Update() override; bool isEnemy(int otherUnitTeam) const; void goTo(Vec3 positionToGo, bool isForced = false); void attack(shared_ptr<Entity> entityToAttack, bool isForced = false); void select(bool doSelect = true); }; #pragma once #include "UltraEngine.h" #include "Unit.h" #include "../Logic/WayPoint.h" using namespace UltraEngine; Unit::Unit() { name = "Unit"; attackName = "Attack"; idleName = "Idle"; painName = "Pain"; deathName = "Death"; runName = "Run"; } shared_ptr<Component> Unit::Copy() { return std::make_shared<Unit>(*this); } void Unit::Start() { auto entity = GetEntity(); auto model = entity->As<Model>(); //for custom save/load system entity->AddTag("Unit"); if (!isFullPlayerControl) { //checking efficiently if Unit have a nav mesh if (!navMesh.expired()) { //1 m radius because of Beast long model, 0.5 would better otherwise, 2 m height agent = CreateNavAgent(navMesh.lock(), 0.5, 2); agent->SetMaxSpeed(speed); agent->SetPosition(entity->GetPosition(true)); agent->SetRotation(entity->GetRotation(true).y); entity->SetPosition(0, 0, 0); //becase models rotated by back entity->SetRotation(0, 180, 0); entity->Attach(agent); } entity->SetCollisionType(COLLISION_PLAYER); entity->SetMass(0); entity->SetPhysicsMode(PHYSICS_RIGIDBODY); } if (model) { auto seq = model->FindAnimation(attackName); if (seq != -1) { int count = model->CountAnimationFrames(seq); //to disable attack state at end of attack animation model->skeleton->AddHook(seq, count - 1, EndAttackHook, Self()); //to deal damage to target at range at specific animation frame model->skeleton->AddHook(seq, attackFrame, AttackHook, Self()); } seq = model->FindAnimation(painName); if (seq != -1) { int count = model->CountAnimationFrames(seq); //to disable pain state at end of pain animation model->skeleton->AddHook(seq, count - 1, EndPainHook, Self()); } } if (!isFullPlayerControl) { int healthBarHeight = 5; healthBar = CreateSprite(entity->GetWorld(), maxHealth, healthBarHeight); if (team == 1) { healthBar->SetColor(0, 1, 0); } else { healthBar->SetColor(1, 0, 0); } healthBar->SetPosition(0, 0, 0.00001f); healthBar->SetRenderLayers(2); healthBarBackground = CreateSprite(entity->GetWorld(), maxHealth, healthBarHeight); healthBarBackground->SetColor(0.1f, 0.1f, 0.1f); //to put it behind health bar healthBarBackground->SetPosition(0, 0, 0.00002f); healthBarBackground->SetRenderLayers(2); } auto world = entity->GetWorld(); shared_ptr<Camera> camera; for (auto const& cameraEntity : world->GetTaggedEntities("Camera")) { camera = cameraEntity->As<Camera>(); break; } if (!camera) { for (auto const& cameraEntity : world->GetEntities()) { camera = cameraEntity->As<Camera>(); if (camera) { break; } } } cameraWeak = camera; BaseComponent::Start(); } bool Unit::Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Scene> scene, const LoadFlags flags, shared_ptr<Object> extra) { sceneWeak = scene; if (properties["isFullPlayerControl"].is_boolean()) isFullPlayerControl = properties["isFullPlayerControl"]; if (properties["isPlayer"].is_boolean()) isPlayer = properties["isPlayer"]; if (properties["team"].is_number()) team = properties["team"]; if (properties["health"].is_number()) health = properties["health"]; if (properties["maxHealth"].is_number()) maxHealth = properties["maxHealth"]; if (properties["attackDamage"].is_number()) attackDamage = properties["attackDamage"]; if (properties["attackRange"].is_number()) attackRange = properties["attackRange"]; if (properties["attackFrame"].is_number()) attackFrame = properties["attackFrame"]; if (properties["painCooldown"].is_number()) painCooldown = properties["painCooldown"]; if (properties["enabled"].is_boolean()) enabled = properties["enabled"]; if (properties["decayTime"].is_number()) decayTime = properties["decayTime"]; if (properties["attackName"].is_string()) attackName = properties["attackName"]; if (properties["idleName"].is_string()) idleName = properties["idleName"]; if (properties["deathName"].is_string()) deathName = properties["deathName"]; if (properties["painName"].is_string()) painName = properties["painName"]; if (properties["runName"].is_string()) runName = properties["runName"]; if (properties["target"].is_string()) { std::string id = properties["target"]; targetWeak = scene->GetEntity(id); } else { targetWeak.reset(); } if (properties["targetPoint"].is_string()) { std::string id = properties["targetPoint"]; targetPoint = scene->GetEntity(id); } else { targetPoint = nullptr; } if (properties["isForcedMovement"].is_boolean()) isForcedMovement = properties["isForcedMovement"]; if (properties["position"].is_array() && properties["position"].size() == 3) { GetEntity()->SetPosition(properties["position"][0], properties["position"][1], properties["position"][2]); } if (properties["rotation"].is_array() && properties["rotation"].size() == 3) { GetEntity()->SetRotation(properties["rotation"][0], properties["rotation"][1], properties["rotation"][2]); } navMesh.reset(); if (!scene->navmeshes.empty()) { navMesh = scene->navmeshes[0]; } return BaseComponent::Load(properties, binstream, scene, flags, extra); } bool Unit::Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Scene> scene, const SaveFlags flags, shared_ptr<Object> extra) { properties["isFullPlayerControl"] = isFullPlayerControl; properties["isPlayer"] = isPlayer; properties["team"] = team; properties["health"] = health; properties["enabled"] = enabled; if (targetWeak.lock()) { properties["target"] = targetWeak.lock()->GetUuid(); } if (targetPoint) { properties["targetPoint"] = targetPoint->GetUuid(); } properties["isForcedMovement"] = isForcedMovement; auto position = GetEntity()->GetPosition(true); properties["position"] = {}; properties["position"][0] = position.x; properties["position"][1] = position.y; properties["position"][2] = position.z; auto rotation = GetEntity()->GetRotation(true); properties["rotation"] = {}; properties["rotation"][0] = rotation.x; properties["rotation"][1] = rotation.y; properties["rotation"][2] = rotation.z; return BaseComponent::Save(properties, binstream, scene, flags, extra); } void Unit::Damage(const int amount, shared_ptr<Entity> attacker) { if (!isAlive()) { return; } health -= amount; auto world = GetEntity()->GetWorld(); if (!world) { return; } auto now = world->GetTime(); if (health <= 0) { Kill(attacker); } else if (!isInPain && now - painCooldownTime > painCooldown) { isInPain = true; isAttacking = false; auto model = GetEntity()->As<Model>(); if (model) { model->StopAnimation(); model->Animate(painName, 1.0f, 100, ANIMATION_ONCE); } if (agent) { agent->Stop(); } } if (healthBar) { //reducing health bar sprite width healthBar->SetScale((float)health / (float)maxHealth, 1, 1); } //attack an atacker if (!isForcedMovement && !isForcedTarget) { attack(attacker); } } void Unit::Kill(shared_ptr<Entity> attacker) { auto entity = GetEntity(); if (!entity) { return; } auto model = entity->As<Model>(); if (model) { model->StopAnimation(); model->Animate(deathName, 1.0f, 250, ANIMATION_ONCE); } if (agent) { //This method will cancel movement to a destination, if it is active, and the agent will smoothly come to a halt. agent->Stop(); } //to remove nav agent entity->Detach(); agent = nullptr; //to prevent it being obstacle entity->SetCollisionType(COLLISION_NONE); //to prevent selection entity->SetPickMode(PICK_NONE); isAttacking = false; healthBar = nullptr; healthBarBackground = nullptr; if (decayTime > 0) { removeEntityTimer = UltraEngine::CreateTimer(decayTime); ListenEvent(EVENT_TIMERTICK, removeEntityTimer, RemoveEntityCallback, Self()); } } bool Unit::isAlive() { return health > 0 && GetEntity(); } bool Unit::RemoveEntityCallback(const UltraEngine::Event& ev, shared_ptr<UltraEngine::Object> extra) { auto unit = extra->As<Unit>(); unit->removeEntityTimer->Stop(); unit->removeEntityTimer = nullptr; unit->sceneWeak.lock()->RemoveEntity(unit->GetEntity()); return false; } void Unit::scanForTarget() { auto entity = GetEntity(); auto world = entity->GetWorld(); if (world) { //We only want to perform this few times each second, staggering the operation between different entities. //Pick() operation is kinda CPU heavy. It can be noticeable in Debug mode when too much Picks() happes in same game cycle. //Did not notice yet it in Release mode, but it's better to have it optimized Debug as well anyway. auto now = world->GetTime(); if (now < nextScanForTargetTime) { return; } nextScanForTargetTime = now + Random(100, 200); auto entityPosition = entity->GetPosition(true); Vec3 positionLower = entityPosition; positionLower.x = positionLower.x - perceptionRadius; positionLower.z = positionLower.z - perceptionRadius; positionLower.y = positionLower.y - perceptionRadius; Vec3 positionUpper = entityPosition; positionUpper.x = positionUpper.x + perceptionRadius; positionUpper.z = positionUpper.z + perceptionRadius; positionUpper.y = positionUpper.y + perceptionRadius; //will use it to determinate which target is closest float currentTargetDistance = -1; //GetEntitiesInArea takes positions of an opposite corners of a cube as params for (auto const& foundEntity : world->GetEntitiesInArea(positionLower, positionUpper)) { auto foundUnit = foundEntity->GetComponent<Unit>(); //targets are only alive enemy units if (!foundUnit || !foundUnit->isAlive() || !foundUnit->isEnemy(team) || !foundUnit->GetEntity()) { continue; } float dist = foundEntity->GetDistance(entity); if (dist > perceptionRadius) { continue; } //check if no obstacles like walls between units auto pick = world->Pick(entity->GetBounds(BOUNDS_RECURSIVE).center, foundEntity->GetBounds(BOUNDS_RECURSIVE).center, perceptionRadius, true, RayFilter, Self()); if (dist < 0 || currentTargetDistance < dist) { targetWeak = foundEntity; currentTargetDistance = dist; } } } } void Unit::Update() { if (!GetEnabled() || !isAlive()) { return; } auto entity = GetEntity(); auto world = entity->GetWorld(); auto model = entity->As<Model>(); if (!world || !model) { return; } if (isFullPlayerControl) { return; } //making health bar fllow the unit auto window = ActiveWindow(); if (window && healthBar && healthBarBackground) { auto framebuffer = window->GetFramebuffer(); auto position = entity->GetBounds().center; position.y += entity->GetBounds().size.height / 2;//take top position of unit shared_ptr<Camera> camera = cameraWeak.lock(); if (camera) { //transorming 3D position into 2D auto unitUiPosition = camera->Project(position, framebuffer); //sprite Y coordinate start from bottom of screen and projected from top unitUiPosition.y = framebuffer->size.height - unitUiPosition.y; unitUiPosition.x -= healthBarBackground->size.width / 2; healthBar->SetPosition(unitUiPosition.x, unitUiPosition.y); healthBarBackground->SetPosition(unitUiPosition.x, unitUiPosition.y); bool doShow = isSelected || (health != maxHealth && !isPlayer); healthBar->SetHidden(!doShow); healthBarBackground->SetHidden(!doShow); } } //can't attack or move while pain animation if (isInPain) { return; } bool isMoving = false; //ignore enemies and move if (isForcedMovement && goTo()) { return; } //atacking part if (!isMoving) { auto target = targetWeak.lock(); // Stop attacking if target is dead if (target) { float distanceToTarget = entity->GetDistance(target); bool doResetTarget = false; if (distanceToTarget > chaseMaxDistance && !isForcedTarget) { doResetTarget = true; } else { for (auto const& targetComponent : target->components) { auto targetUnit = targetComponent->As<Unit>(); if (targetUnit && !targetUnit->isAlive()) { doResetTarget = true; isForcedTarget = false; } break; } } if (doResetTarget) { target.reset(); targetWeak.reset(); if (agent) { agent->Stop(); } } } if (isAttacking && target != nullptr) { //rotating unit to target float a = ATan(entity->matrix.t.x - target->matrix.t.x, entity->matrix.t.z - target->matrix.t.z); if (agent) { agent->SetRotation(a + 180); } } if (!target) { scanForTarget(); } if (target) { float distanceToTarget = entity->GetDistance(target); //run to target if out of range if (distanceToTarget > attackRange) { if (agent) { agent->Navigate(target->GetPosition(true)); } model->Animate(runName, 1.0f, 250, ANIMATION_LOOP); } else { if (agent) { agent->Stop(); } //start attack if did not yet if (!isAttacking) { meleeAttackTime = world->GetTime(); model->Animate(attackName, 1.0f, 100, ANIMATION_ONCE); isAttacking = true; } } return; } } if (targetPoint && goTo()) { return; } if (!isAttacking) { model->Animate(idleName, 1.0f, 250, ANIMATION_LOOP); if (agent) { agent->Stop(); } } } bool Unit::RayFilter(shared_ptr<Entity> entity, shared_ptr<Object> extra) { shared_ptr<Unit> thisUnit = extra->As<Unit>(); shared_ptr<Unit> pickedUnit = entity->GetComponent<Unit>(); //skip if it's same team return pickedUnit == nullptr || pickedUnit && pickedUnit->team != thisUnit->team; } void Unit::AttackHook(shared_ptr<Skeleton> skeleton, shared_ptr<Object> extra) { auto unit = std::dynamic_pointer_cast<Unit>(extra); if (!unit) { return; } auto entity = unit->GetEntity(); auto target = unit->targetWeak.lock(); if (target) { auto pos = entity->GetPosition(true); auto dest = target->GetPosition(true) + target->GetVelocity(true); //attack in target in range if (pos.DistanceToPoint(dest) < unit->attackRange) { for (auto const& targetComponent : target->components) { auto base = targetComponent->As<BaseComponent>(); if (base) { base->Damage(unit->attackDamage, entity); } } } } } void Unit::EndAttackHook(shared_ptr<Skeleton> skeleton, shared_ptr<Object> extra) { auto unit = std::dynamic_pointer_cast<Unit>(extra); if (unit) { unit->isAttacking = false; } } void Unit::EndPainHook(shared_ptr<Skeleton> skeleton, shared_ptr<Object> extra) { auto unit = extra->As<Unit>(); if (unit) { unit->isInPain = false; if (unit->isAlive() && unit->GetEntity()->GetWorld()) { unit->painCooldownTime = unit->GetEntity()->GetWorld()->GetTime(); } } } bool Unit::isEnemy(int otherUnitTeam) const { return team == 1 && otherUnitTeam == 2 || team == 2 && otherUnitTeam == 1; } void Unit::goTo(Vec3 positionToGo, bool isForced) { auto entity = GetEntity(); if (entity) { isForcedMovement = isForced; targetPoint = CreatePivot(entity->GetWorld()); targetPoint->SetPosition(positionToGo); goTo(); } } bool Unit::goTo() { bool doMove = false; auto entity = GetEntity(); auto model = entity->As<Model>(); if (targetPoint && agent) { doMove = agent->Navigate(targetPoint->GetPosition(true), 100, 2.0f); if (doMove) { //checking distance to target point on nav mesh float distanceToTarget = entity->GetDistance(agent->GetDestination()); if (distanceToTarget < targetPointDistance) { auto wayPoint = targetPoint->GetComponent<WayPoint>(); if (wayPoint && wayPoint->getNextPoint()) { targetPoint = wayPoint->getNextPoint(); doMove = true; } else { targetPoint.reset(); doMove = false; } } else { doMove = true; } } if (doMove && model) { model->Animate(runName, 1.0f, 250, ANIMATION_LOOP); } } return doMove; } void Unit::attack(shared_ptr<Entity> entityToAttack, bool isForced) { targetWeak.reset(); if (!entityToAttack || !entityToAttack->GetComponent<Unit>() || entityToAttack->GetComponent<Unit>()->team == team) { return; } targetPoint.reset(); isForcedMovement = false; isForcedTarget = isForced; targetWeak = entityToAttack; } void Unit::select(bool doSelect) { isSelected = doSelect; } For Unit class you need the WayPoint component from the previous tutorial. Also can be download here: Remember adding new component to ComponentSystem.h Unit files:Unit.zip Strategy Controller component Strategy Controller will be used to control player units: Selecting a unit by left click, doing it with Control will add new unit to already selected Clicking on something else reset unit selection Holding left mouse button will create selection box that will select units in it once button released Right click to make units go somewhere ignoring enemies or attacking specific enemy Its path will be: "Source\Components\Player" StrategyController.json: { "component": { "properties": [ { "name": "playerTeam", "label": "Player Team", "value": 1 } ] } } StrategyController.h #pragma once #include "UltraEngine.h" using namespace UltraEngine; class StrategyController : public Component { protected: vector<std::weak_ptr<Entity>> selectedUnits; //Control key state bool isControlDown = false; int playerTeam = 1; std::weak_ptr<Camera> cameraWeak; shared_ptr<Sprite> unitSelectionBox; //first mouse position when Mouse Left was pressed iVec2 unitSelectionBoxPoint1; //height of selection box float selectHeight = 4; //mouse left button state bool isMouseLeftDown = false; //draw or hide selection box void updateUnitSelectionBox(); bool selectUnitsByBox(shared_ptr<Camera> camera, shared_ptr<Framebuffer> framebuffer, iVec2 unitSelectionBoxPoint2); void deselectAllUnits(); static bool RayFilter(shared_ptr<Entity> entity, shared_ptr<Object> extra); public: StrategyController(); ~StrategyController() override; shared_ptr<Component> Copy() override; bool Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags, shared_ptr<Object> extra) override; bool Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags, shared_ptr<Object> extra) override; void Update() override; void Start() override; bool ProcessEvent(const Event& e) override; }; StrategyController.cpp #include "UltraEngine.h" #include "StrategyController.h" #include "../AI/Unit.h" StrategyController::StrategyController() { name = "StrategyController"; } shared_ptr<Component> StrategyController::Copy() { return std::make_shared<StrategyController>(*this); } StrategyController::~StrategyController() = default; void StrategyController::Start() { auto entity = GetEntity(); entity->AddTag("StrategyController"); //Listen() needed for calling ProcessEvent() in component when event happen Listen(EVENT_MOUSEDOWN, nullptr); Listen(EVENT_MOUSEUP, nullptr); Listen(EVENT_MOUSEMOVE, nullptr); Listen(EVENT_KEYUP, nullptr); Listen(EVENT_KEYDOWN, nullptr); //optimal would be setting component to a camera if (entity->As<Camera>()) { cameraWeak = entity->As<Camera>(); } else { //otherwise let's get it by tag for (auto const& cameraEntity : GetEntity()->GetWorld()->GetTaggedEntities("Camera")) { cameraWeak = cameraEntity->As<Camera>(); break; } } // 1/1 size for pixel accuarcy scaling unitSelectionBox = CreateSprite(entity->GetWorld(), 1, 1); //transparent green color unitSelectionBox->SetColor(0, 0.4f, 0.2, 0.5f); unitSelectionBox->SetPosition(0, 0, 0.00001f); unitSelectionBox->SetRenderLayers(2); unitSelectionBox->SetHidden(true); //to make sprite transparent auto material = CreateMaterial(); material->SetShadow(false); material->SetTransparent(true); material->SetPickMode(false); //Unlit removes any effect that would light draw on material material->SetShaderFamily(LoadShaderFamily("Shaders/Unlit.fam")); unitSelectionBox->SetMaterial(material); } bool StrategyController::Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags, shared_ptr<Object> extra) { Component::Load(properties, binstream, scene, flags, extra); if (properties["playerTeam"].is_number()) playerTeam = properties["playerTeam"]; return true; } bool StrategyController::Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags, shared_ptr<Object> extra) { Component::Save(properties, binstream, scene, flags, extra); properties["playerTeam"] = playerTeam; return true; } void StrategyController::Update() { updateUnitSelectionBox(); } bool StrategyController::ProcessEvent(const Event& e) { auto window = ActiveWindow(); if (!window) { return true; } auto mousePosition = window->GetMousePosition(); auto camera = cameraWeak.lock(); switch (e.id) { case EVENT_MOUSEDOWN: if (!camera) { break; } if (e.data == MOUSE_LEFT) { unitSelectionBoxPoint1 = iVec2(mousePosition.x, mousePosition.y); isMouseLeftDown = true; //move or attack on Right Click } else if (e.data == MOUSE_RIGHT) { //getting entity under cursor auto pick = camera->Pick(window->GetFramebuffer(), mousePosition.x, mousePosition.y, 0, true); if (pick.success && pick.entity) { auto unit = pick.entity->GetComponent<Unit>(); if (unit && unit->isAlive() && unit->team != playerTeam) { for (auto const& entityWeak : selectedUnits) { auto entityUnit = entityWeak.lock(); if (entityUnit && entityUnit->GetComponent<Unit>()) { entityUnit->GetComponent<Unit>()->attack(pick.entity, true); } } } else { for (auto const& entityWeak : selectedUnits) { auto entityUnit = entityWeak.lock(); if (entityUnit && entityUnit->GetComponent<Unit>()) { entityUnit->GetComponent<Unit>()->goTo(pick.position, true); } } } } } break; case EVENT_MOUSEUP: if (!camera) { break; } //unit selection on Left Click if (e.data == MOUSE_LEFT) { if (!selectUnitsByBox(camera, window->GetFramebuffer(), iVec2(mousePosition.x, mousePosition.y))) { auto pick = camera->Pick(window->GetFramebuffer(), mousePosition.x, mousePosition.y, 0, true); if (pick.success && pick.entity) { auto unit = pick.entity->GetComponent<Unit>(); if (unit && unit->isPlayer && unit->isAlive()) { if (!isControlDown) { deselectAllUnits(); } selectedUnits.push_back(pick.entity); unit->select(); } else { deselectAllUnits(); } } else { deselectAllUnits(); } } isMouseLeftDown = false; } break; case EVENT_MOUSEMOVE: break; case EVENT_KEYUP: if (e.data == KEY_CONTROL) { isControlDown = false; } break; case EVENT_KEYDOWN: if (e.data == KEY_CONTROL) { isControlDown = true; } break; } return true; } void StrategyController::deselectAllUnits() { for (auto const& entityWeak : selectedUnits) { auto entityUnit = entityWeak.lock(); if (entityUnit && entityUnit->GetComponent<Unit>()) { entityUnit->GetComponent<Unit>()->select(false); } } selectedUnits.clear(); } void StrategyController::updateUnitSelectionBox() { if (!isMouseLeftDown) { unitSelectionBox->SetHidden(true); } else { auto window = ActiveWindow(); if (window) { auto mousePosition = window->GetMousePosition(); iVec2 unitSelectionBoxPoint2(mousePosition.x, mousePosition.y); iVec2 upLeft(Min(unitSelectionBoxPoint1.x, unitSelectionBoxPoint2.x), Min(unitSelectionBoxPoint1.y, unitSelectionBoxPoint2.y)); iVec2 downRight(Max(unitSelectionBoxPoint1.x, unitSelectionBoxPoint2.x), Max(unitSelectionBoxPoint1.y, unitSelectionBoxPoint2.y)); //don't show Selection Box if it's only few pixels and could be single click to select unit if ((downRight.x - upLeft.x < 4) || (downRight.y - upLeft.y < 4)) { unitSelectionBox->SetHidden(true); return; } unitSelectionBox->SetPosition(upLeft.x, window->GetFramebuffer()->GetSize().height - downRight.y); auto width = downRight.x - upLeft.x; auto height = downRight.y - upLeft.y; //changing sprite size via scale, just size is readonly unitSelectionBox->SetScale(width, height, 1); unitSelectionBox->SetHidden(false); } } } bool StrategyController::selectUnitsByBox(shared_ptr<Camera> camera, shared_ptr<Framebuffer> framebuffer, iVec2 unitSelectionBoxPoint2) { if (!unitSelectionBox || unitSelectionBox->GetHidden() || !camera || !framebuffer) { return false; } iVec2 upLeft(Min(unitSelectionBoxPoint1.x, unitSelectionBoxPoint2.x), Min(unitSelectionBoxPoint1.y, unitSelectionBoxPoint2.y)); iVec2 downRight(Max(unitSelectionBoxPoint1.x, unitSelectionBoxPoint2.x), Max(unitSelectionBoxPoint1.y, unitSelectionBoxPoint2.y)); auto pick1 = camera->Pick(framebuffer, upLeft.x, upLeft.y, 0, true, RayFilter); auto pick2 = camera->Pick(framebuffer, downRight.x, downRight.y, 0, true, RayFilter); if (!pick1.success || !pick2.success) { return false; } deselectAllUnits(); //first param GetEntitiesInArea should has lower coordinates than second Vec3 positionLower = Vec3(Min(pick1.position.x, pick2.position.x), Min(pick1.position.y, pick2.position.y), Min(pick1.position.z, pick2.position.z)); Vec3 positionUpper = Vec3(Max(pick1.position.x, pick2.position.x), Max(pick1.position.y, pick2.position.y), Max(pick1.position.z, pick2.position.z)); positionUpper.y = positionUpper.y + selectHeight; for (auto const& foundEntity : camera->GetWorld()->GetEntitiesInArea(positionLower, positionUpper)) { auto foundUnit = foundEntity->GetComponent<Unit>(); //targets are only alive enemy units if (!foundUnit || !foundUnit->isAlive() || !foundUnit->isPlayer || foundUnit->team != playerTeam) { continue; } selectedUnits.push_back(foundUnit->GetEntity()); foundUnit->select(); } return true; } bool StrategyController::RayFilter(shared_ptr<Entity> entity, shared_ptr<Object> extra) { shared_ptr<Unit> pickedUnit = entity->GetComponent<Unit>(); //skip if it's unit return pickedUnit == nullptr; } StrategyController.zip Also we need top down camera component, you can find it here, if you don't have it yet: Prefabs Create Prefabs folder in project root folder. Open the Editor, add camera to empty scene, call it StrategyCamera and add to it TopDownCamera and StrategyController components. You might also want to change a FOV in Camera tab in entity properties. To make a prefab do right click on camera in Scene tab and press "Save as Prefab": Once you want to change something without having to do it on every map, just open the prefab .pfb file and do a change there. Now create a Units subfolder in the Prefabs folder for two units. Add to scene Paladin model. Add Paladin component, click on "Is Player Unit" checkbox and change Attack Frame to 40 as it's a frame when sword will hit a target: Save as Prefab in Pefabs/Units as Paladin.pfb You can remove now Paladin from scene and add Warrok. In its Unit component team will be Bad, health and max health 120, speed 2.0, attack range 1.5, attack damage 25, and Attack Frame 22 as it's attack faster. Simple Strategy map creation Create big flat brush as a ground Add Navigation map - change Agent Radius to 0.5 and Agent Height 2.0 so Warrok could fit it. Tile size be default is 8 m² = Voxel size (0.25) * Tile Resolution (32). It's better not to touch those until you know what you are doing. To change Nav Mesh size just change Tile count in first line to make it fit created ground. For 40 m² it will be 5x5 tiles. Drag a ground material from Project tab "\Materials\Ground\Ground036.mat" to the ground. If it's blurry, select brush, choose Edit Face mode and increase a scale there. Select translate mode and drag'n'drop to scene prefabs StrategyCamera, Warrok and Paladin. You can copy entities with Shift + dragging. To edit a prefab entity you will need to "break" prefab. To do it click on the Lock icon in properties and Ok in the dialog. Also you need to break prefab to make component get real Scene, because otherwise in it will be prefab scene in Component's Load() Simple map is ready. You can make it a little bit complicated by adding couple cycled WayPoints for Warroks. Add WayPoint to Target Point. strategy.zip In Game.cpp update GameMenuButtonCallback function to avoid cursor being hidden: 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); if (game->player) { //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); } 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. //to avoid double call return false; } return true; } Game and Menu.zip In result should be something like that: Final version here: https://github.com/Dreikblack/CppTutorialProject/tree/3-making-strategy-game
-
-
C++ Ultra Beginner's Guide #2 - making and using components
Dreikblack posted a blog entry in Ultra Tutorials
In this tutorial we will make a newcomponent, which will be moving an entity to way points and movement start will be activated by trigger zone Let's start with making WayPoint component: In the Ultra Editor click plus button in Project tab: Now open Visual Studio. Refresh Soution Editor if it was already open to see new component files in Source/Component/Logic folder. Include into project WayPoint.h and WayPoint.cpp. Add WayPoint include to Source/ComponentSystem.h #include "Components/Logic/WayPoint.h" In RegisterComponents() add: RegisterComponent<WayPoint>(); You might do RegisterComponent first and add include with VS help Open WayPoint.json "properties" is a list of component's fields avaible for edit in the Editor "name" - actual name of property that will be used in code later "label" - just a name to display in the Editor "value" - initial value that property will have after map load by default. Can be changed in the Editor for specific entity. "options" - allows to choose int value in Combo Box in the Editor. First option - 0 value Default value here also defines type of this property. New component made via editor have all possible types. Replace WayPoint.json content with: { "component": { "properties": [ { "name": "nextPoint", "label": "Next point", "value": null }, { "name": "doStayOnPoint", "label": "Do stay on point", "value": false } ] } } nextPoint - another WayPoint, where platform will move to once reach this one doStayOnPoint - wait for command before moving to next WayPoint Take a note that the Editor sees only json which could be same for LUA and C++ projects which allows to work on same map even if people have different engine versions (Pro/Standard) or make a level before programming components. Replace WayPoint.h content with: #pragma once #include "UltraEngine.h" using namespace UltraEngine; class WayPoint : public Component { protected: //another WayPoint's entity, where platform will move to once reaching this one //it should be weak pointer for avoiding circular dependency when entities can't be removed from scene because they keep shared pointers to each other std::weak_ptr<Entity> nextPoint; public: //wait for command before moving to next WayPoint bool doStayOnPoint; WayPoint(); //override specifier ensures that the method is virtual and is overriding a virtual function from a base class //it means that this class method will be called even if class instance was casted to base class //it allows to change class behaviour from base one //Start is called when Load() of all components was called already void Start() override; //will be called on map load bool Load(table& t, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags, shared_ptr<Object> extra) override; //Can be used to save current component state on map save bool Save(table& t, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags, shared_ptr<Object> extra) override; //get next WayPoint's entity shared_ptr<Entity> getNextPoint() const; //can be used to specify what should and what should not be copied to new class instance shared_ptr<Component> Copy() override; }; Replace WayPoint.cpp content with: #pragma once #include "UltraEngine.h" #include "WayPoint.h" using namespace UltraEngine; WayPoint::WayPoint() { //name should always match class name for correct component work name = "WayPoint"; doStayOnPoint = false; } void WayPoint::Start() { //empty for this case } bool WayPoint::Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags, shared_ptr<Object> extra) { //internally entity saves in the Editor as String unique id //can be empty if this way point is final if (properties["nextPoint"].is_string()) { std::string id = properties["nextPoint"]; nextPoint = scene->GetEntity(id); if (properties["doStayOnPoint"].is_boolean()) { doStayOnPoint = properties["doStayOnPoint"]; } } return true; } bool WayPoint::Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags, shared_ptr<Object> extra) { if (nextPoint.lock()) { properties["nextPoint"] = nextPoint.lock()->GetUuid(); properties["doStayOnPoint"] = doStayOnPoint; } return true; } shared_ptr<Entity> WayPoint::getNextPoint() const { return nextPoint.lock(); } shared_ptr<Component> WayPoint::Copy() { return std::make_shared<WayPoint>(*this); } Let's create our floating object component and call it WayMover WayMover.json: { "component": { "properties": [ { "name": "moveSpeed", "label": "Move Speed", "value": 4.0 }, { "name": "nextPoint", "label": "Next point", "value": null }, { "name": "doDeleteAfterMovement", "label": "Del after move", "value": false } ], "inputs": [ { "name": "DoMove" } ], "outputs": [ { "name": "EndMove" } ] } } moveSpeed - how fast entity will move to way point doDeleteAfterMovement - auto remove entity when it's reach final waypoint. Can be used for door, that goes into walls or floor inputs - it's commands for components, that usually triggered by another components via flowgrough outputs - commands that component sends to other components inputs via FireOutputs("EndMove"); in the component code WayMover.h: #pragma once #include "UltraEngine.h" using namespace UltraEngine; class WayMover : public Component { protected: float moveSpeed; bool isMoving; std::weak_ptr<Entity> nextPoint; bool doDeleteAfterMovement; //need scene pointer to remove entity if doDeleteAfterMovement true //should be weak_ptr to avoid circular dependency std::weak_ptr<Map> sceneWeak; public: WayMover(); std::shared_ptr<Component> Copy() override; bool Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags, shared_ptr<Object> extra) override; bool Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags, shared_ptr<Object> extra) override; //CallMethod procceds input signals std::any CallMethod(shared_ptr<Component> caller, const WString& name, const std::vector<std::any>& args) override; void Update() override; //called when reaching next WayPoint void moveEnd(); }; WayMover.cpp: #pragma once #include "UltraEngine.h" #include "WayMover.h" #include "../Logic/WayPoint.h" using namespace UltraEngine; WayMover::WayMover() { name = "WayMover"; moveSpeed = 4.0f; isMoving = false; doDeleteAfterMovement = false; } shared_ptr<Component> WayMover::Copy() { return std::make_shared<WayMover>(*this); } bool WayMover::Load(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const LoadFlags flags, shared_ptr<Object> extra) { if (properties["moveSpeed"].is_number()) { moveSpeed = properties["moveSpeed"]; } if (properties["isMoving"].is_boolean()) { isMoving = properties["isMoving"]; } if (properties["doDeleteAfterMovement"].is_boolean()) { doDeleteAfterMovement = properties["doDeleteAfterMovement"]; } if (properties["nextPoint"].is_string()) { std::string id = properties["nextPoint"]; nextPoint = scene->GetEntity(id); } sceneWeak = scene; return Component::Load(properties, binstream, scene, flags, extra); } bool WayMover::Save(table& properties, shared_ptr<Stream> binstream, shared_ptr<Map> scene, const SaveFlags flags, shared_ptr<Object> extra) { properties["moveSpeed"] = moveSpeed; properties["isMoving"] = isMoving; properties["doDeleteAfterMovement"] = doDeleteAfterMovement; if (nextPoint.lock()) { properties["nextPoint"] = nextPoint.lock()->GetUuid(); } return Component::Save(properties, binstream, scene, flags, extra); } std::any WayMover::CallMethod(shared_ptr<Component> caller, const WString& name, const std::vector<std::any>& args) { //start moving by triggering by another component if (name == "DoMove") { isMoving = true; } return false; } void WayMover::Update() { if (!isMoving) { return; } auto entity = GetEntity(); auto wayPoint = nextPoint.lock(); if (!entity || !wayPoint) { return; } //60 HZ game loop, change to own value if different to keep same final speed float speed = moveSpeed / 60.0f; auto targetPosition = wayPoint->GetPosition(true); //moving to point with same speed directly to point no matter which axis auto pos = entity->GetPosition(true); float distanceX = abs(targetPosition.x - pos.x); float distanceY = abs(targetPosition.y - pos.y); float distanceZ = abs(targetPosition.z - pos.z); float biggestDelta = distanceZ; if (distanceX > distanceY && distanceX > distanceZ) { biggestDelta = distanceX; } else if (distanceY > distanceX && distanceY > distanceZ) { biggestDelta = distanceY; } float moveX = MoveTowards(pos.x, targetPosition.x, speed * (distanceX / biggestDelta)); float moveY = MoveTowards(pos.y, targetPosition.y, speed * (distanceY / biggestDelta)); float moveZ = MoveTowards(pos.z, targetPosition.z, speed * (distanceZ / biggestDelta)); entity->SetPosition(moveX, moveY, moveZ); if (entity->GetPosition(true) == targetPosition) { moveEnd(); } } void WayMover::moveEnd() { auto wayPoint = nextPoint.lock(); bool doStay = false; if (wayPoint) { doStay = wayPoint->GetComponent<WayPoint>()->doStayOnPoint; wayPoint = wayPoint->GetComponent<WayPoint>()->getNextPoint(); nextPoint = wayPoint; } if (doStay || !wayPoint) { isMoving = false; FireOutputs("EndMove"); //deleting entity if need to, after reaching final way point if (!doStay && !wayPoint && doDeleteAfterMovement && !sceneWeak.expired()) { auto scene = sceneWeak.lock(); scene->RemoveEntity(GetEntity()); } } } Now we can use just made component in practice. One of things that can be made is door or secret wall activated by player actions and this door will move a little bit inward and then to the side inside of wall. After that invisible now door will be removed. Create a walls with a empty place between them. Create couple of Empty/pivots and attach WayPoints to them. First WayPoint place a same place where door will be, but offset a bit deep into. In Scene tab grab and drag 2nd WayPoint to Nex Point field of 1st WayPoint. Place 2nd WayPoint insde of the wall. Create a door between walls. Attach WayMover component to it. Grab and drag 1st WayPoint to door's WayMover Next Point field. Enable "Del after Move" in WayMover component Create a box before door, make its collision type a trigger: Add Collision Trigger component to it. Open Flowgraph (2nd button at left side of the Editor). Drag and Drop trigger and door to it from Scene tab. In different order, but same result in video format: Result should looks something like that in game: New components and map: NewComponentsTutorFiles.zip On GitHub: https://github.com/Dreikblack/CppTutorialProject/tree/2-making-and-using-components -
1 download
Component to move an entity to WayPoints: Move Speed - how much velocity entity will have while moving doDeleteAfterMovement - auto remove entity when it's reach final waypoint. Can be used for door, that goes into walls or floor Input "DoMove" - make entity move to next point Output "EndMove" - happens when entity stops after reaching final way point or if this way poiont has enabled doStayOnPoint -
0 downloads
Simple component which can be used to naviage bots or objects nextPoint - null if it's a final onem otherwise add another entity with this component to make a chain doStayOnPoint - can be checked by object that uses WayPoints to find out if it should stay after reaching this point and wait a command before moving to next one -
C++ Beginner's Guide - first map, Main Menu, Loading Screen and GUI
Dreikblack posted a blog entry in Ultra Tutorials
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: FPS game with menu.zip Maps: Maps.zip Repository: https://github.com/Dreikblack/CppTutorialProject/tree/menu-loading-screen-and-gui -
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": "" } ] } }
-
I've been spending the past few days trying to figure out how to add SDL2 to Leadwerks, as it has an additional Lua binding that would be nice to have. Not to mention the gamepad support (which is the main reason because just adding XInput only would give support for XBox 360 and Dualshock 3 controllers). Not having any experience with an engine like Leadwerks, application building, or libraries has severely impacted my ability to figure out what to do in my current situation. I know I need to include the library and expose it to Lua, but I don't know how to do that, and I don't know how to actually add it to the engine either. I've taken a look at some resources online, such as lazyfoo, and it hasn't really helped me understand. Any help or other resources that could be passed along to me would be greatly appreciated. Thanks!
- 4 replies
-
- programing
- lua
-
(and 2 more)
Tagged with:
-
Version 1.0.0
14 downloads
This small component makes it possible to play and stop animations for models. Various settings are available: AutoPlay: Actually self-explanatory. If this option is activated, the animation is played directly at startup. Loop: If activated, the animation is played in a loop. Name: The name of the animation. Speed: The speed of the animation. Bleed (255): Animation bleeding. In addition, there are two inputs for the Nodegraph: "PlayAnimation" and "StopAnimation". These functions can also be called via C++. -
Wanted to get a start on these after today's discussion. Here is the first draft of the logic components. I'm kind of limited on what I can do without any argument support. I've made the following: Auto - Used to start things on Start or Level Load. (Inspired by logic_auto) Relay - Used as a switch or a delay between connections. You can toggle the component to cut or rejoin connections. (Inspired by logic_relay) RelayTimer - Fires outputs on every interval. (Inspired by logic_timer) PrintMessage - Simply prints whatever to console. This is only my first draft to continue the discussion! Logic.zip
-
This component will copy the rotation of another entity, with adjustable smoothing. MatchRotation.zip
-
Hello I have just got Ultra Engine and have not got it to compile yet. I added missing ATL modules inside visual studio installer, made a new c++ project multiple times and set the optimization to disabled in the project settings. Need help at fixing these errors. The Project is called StereoWindow and i have the exact same problem with another project.
-
I am working my way through some of the sample code and making slight modifications. I am stuck with a very simple problem. I cannot manage to get a model to successfully render a texture. I am using slightly modified code from the leadwerks docs and a stock material. Any insight to my challenged code/thought process is appreciated. #include "App.h" using namespace Leadwerks; App::App() : window(NULL), context(NULL), world(NULL), camera(NULL) {} App::~App() { delete world; delete window; } bool App::Start() { window = Window::Create(); context = Context::Create(window); world = World::Create(); camera = Camera::Create(); camera->SetRotation(5, 0, 0); camera->Move(0, 0, -6); Light* light = DirectionalLight::Create(); light->SetRotation(35, 35, 0); Model* model = NULL; model = Model::Create(); model->SetColor(1.0, 0.0, 1.0); model->SetPosition(0, 0, 0); material = Material::Load("Materials/Concrete/concrete_dirty.mat"); Surface* surface = model->AddSurface(); surface->AddVertex(-0.5, -0.5, 0, 0, 0, -1); surface->AddVertex(0.5, -0.5, 0, 0, 0, -1); surface->AddVertex(0.5, 0.5, 0, 0, 0, -1); surface->AddVertex(-0.5, 0.5, 0, 0, 0, -1); surface->AddTriangle(2, 1, 0); surface->AddTriangle(0, 3, 2); surface->UpdateAABB(); model->SetMaterial(material,1); model->UpdateAABB(Entity::LocalAABB | Entity::GlobalAABB); return true; } bool App::Loop() { if (window->Closed() || window->KeyDown(Key::Escape)) return false; Time::Update(); world->Update(); world->Render(); context->Sync(); return true; }
-
-
Over the past 2 months or so, I was given an assignment to create a program as a part of a college assignment, I chose to use this API As it was recently recommend to me by a friend, I went with an app to help people play dungeons and dragons, as someone who has been playing it for a while I constantly found myself forgetting things or misplaying for various reasons. Included in the app so far, is a character tab that keeps track of everything needed by a player , a pop out dice roller, a spell search engine that can pop out and display more information about a given spell I am currently working on a way to incorporate stat blocks to allow for a greater utility to the user. Would love to hear some feedback.
-
Since this was a hot topic recently, I've written code to preload model files from a map and display a progress bar. Then, when you load the map, it should load much faster since you already "preloaded" the models and the ones in the map will be instances (copies) of existing ones. This is an alternative to Map::Load's hook, which may be a better way to go about this. #include "Leadwerks.h" using namespace Leadwerks; std::string mapfile = "maps/map.map"; string model_file[100]; // Stores list of model files in map Model *preload_model[100]; // Array to preload model files to int modelcount=0; // Counts total number of models found in map file int main(int argc, const char *argv[]) { Leadwerks::Window* window = Leadwerks::Window::Create(); Context* context = Context::Create(window); // Load map as just a file Stream* stream = FileSystem::ReadFile(mapfile); if(stream==NULL) { cout << "Could not open map file to read." << endl; exit(1); } // Put MDL paths/files in a list and get count while(!stream->EOF()) { string line = stream->ReadLine(); // If what we read is a model line (ends with .mdl) if(line.size()>3 && line.compare(line.size() - 4, 4, ".mdl") == 0) model_file[modelcount++]=line; } stream->Release(); cout << "Number of model files in map: " << modelcount << endl; for(int i=0; i<modelcount; i++) { cout << "Preloading file #" << i << ":" << model_file[i] << endl << endl; preload_model[i]=NULL; preload_model[i]=Model::Load(model_file[i]); // You can check if model was properly loaded here (should not be NULL) preload_model[i]->Hide(); // Preload model should not be seen // Draw progress bar context->SetColor(0.0, 0.0, 0.0); context->Clear(); context->SetColor(1.0, 0.0, 0.0); float barwidth = (float)(context->GetWidth())-20.0; barwidth *= (float)(i+1); barwidth /= (float)modelcount; context->DrawRect(10, 10, (int)barwidth, 20); // Remove the below delay line. It's put in just to show progress bar effect Sleep(500); context->Sync(); } // You can then load the map here and it should be much faster // since you already loaded the models in it above // Map::Load(mapfile); // Game code here return 0; }
-
I'm working on a little project where I need to make some simple image processing such as rotation, brightness, contrast and so on. FreeImage is a nice lib for those things and I noticed that it's already included in the FITextueLoader Plugin so that's nice. Is there a way to access the pixel-data in Pixmap so it an be used with the FreeImage functions? Actually something like this: FIBITMAP* Pixmap2FreeImage( std::shared_ptr<Pixmap> pixmap ) { FIBITMAP* bmp = //??? pixmap->???? // ???? return bmp; } std::shared_ptr<UltraEngine::Pixmap> FreeImage2Pixmap(FIBITMAP* source) { std::shared_ptr<UltraEngine::Pixmap> pixmap = std::make_shared<UltraEngine::Pixmap>( ); // ??? return pixmap; }
- 1 reply
-
- c++
- ultra app kit
-
(and 1 more)
Tagged with:
-
My program is working fine in Windows when I use '\n' in a c++ string to get a newline in the text of a label. I have now compiled it on Linux and the newline character is displayed as a character block as if the character cannot be displayed. Is there a way to get Linux to execute the same way as Windows. Here is sample code (a slightly modified version of the label sample code)? #include "UltraEngine.h" using namespace UltraEngine; int main(int argc, const char* argv[]) { //Get the displays auto displays = GetDisplays(); //Create a window auto window = CreateWindow("Ultra Engine", 0, 0, 800, 600, displays[0]); //Create User Interface auto ui = CreateInterface(window); //Create widget auto label1 = CreateLabel("Label", 20, 20, 120, 30, ui->root); auto label2 = CreateLabel("Border\nLabel", 20, 50, 120, 60, ui->root, LABEL_BORDER | LABEL_CENTER | LABEL_MIDDLE); while (window->Closed() == false) { WaitEvent(); } return 0; }
-
Hi, I have encountered a problem using threads with the help of classes. I have been using sample codes so far as well as browsing the forum, but unfortunately I cannot find an answer to this problem. Following the guide about threads I got something like this Unfortunately I don't understand why a function of void type not belonging to a class passes in the compilation without any problem, but in the other direction it doesn't. I apologize for bothering you with probably simple problems and for my poor English Thanks
-
Built to power a new generation of game development tools, Ultra App Kit provides an easy-to-use C++ programming SDK for creating desktop GUI applications. Unlike other alternatives like Dear ImGui, the Ultra App Kit GUI renders in retained mode rather than immediate mode, and is specifically designed for desktop GUI applications. This makes applications snappy and responsive when resizing or refreshing a window. DPI scaling is baked into the design for resolution-independent graphics on any screen. The GUI can be combined with an embedded OpenGL viewport, or combined with a 3D game engine to make custom editors and game development tools. Check out the video tutorials and read the documentation to learn more. Ultra App Kit can be purchased in our store. API Design C++ shared pointers everywhere Extensible widgets system Extensive documentation with examples for each command UI Features Resolution independent for any DPI scale Load SVG vector images Set widget icons Change mouse cursor Custom color schemes stored in JSON files Supported Widgets Label Button (push, checkbox, radio, and toggle styles) ProgressBar TextField TextArea ComboBox ListBox Slider (scrollbar, trackbar, and stepper styles) Draggable multi-select TreeView Create your own custom widgets Additional Features File I/O File system watcher Memory allocation and management Image loading, saving, processing Package system for loading files from compressed / encrypted archives Plugin system Thread management String manipulation (split, search, conversion, etc.) Message boxes and file / folder requester
-
This will create a "spinner" widget like in 3ds max. You can use SetRange() to control the min and max values. Download: Spinner.h Example: #include "UltraEngine.h" #include "Spinner.h" using namespace UltraEngine; int main(int argc, const char* argv[]) { //Get displays auto displays = GetDisplays(); //Create window auto mainwindow = CreateWindow("Spinner", 0, 0, 800, 600, displays[0]); //Create user interface auto ui = CreateInterface(mainwindow); iVec2 sz = ui->root->ClientSize(); //Create spinners auto ispinner = CreateSpinner(20, 20, 100, 30, ui->root, SPINNER_INTEGER); auto fspinner = CreateSpinner(20, 60, 100, 30, ui->root, SPINNER_FLOAT); auto nspinner = CreateSpinner(20, 100, 100, 30, ui->root, SPINNER_FLOAT | SPINNER_NORMALIZE); //Show the window mainwindow->Show(); mainwindow->Activate(); //Easier to work with: ui->SetScale(2); while (true) { const Event event = WaitEvent(); switch (event.id) { case EVENT_WIDGETACTION: if (event.source == ispinner) Print(event.data); if (event.source == fspinner or event.source == nspinner) { Print(event.source->As<Widget>()->GetProgress()); } break; case EVENT_WINDOWCLOSE: if (event.source == mainwindow) return 0; break; } } return 0; }
-
I worked the half day on that problem and I think I give the information to everyone who also has this problem with fog. In the Editor fog looks a little bit like fog. Without implementig fog in C++ source, no fog is visible in C++. Loading a map after craeting a camera only a touch of a color is visible in the very far, and fog is not working as it should. I mean with map not a terrain, I loaded a map with only a plattform and some prefabs. I have not tested with terrain. So load a map before creating a camera. Nothing works correct if you not do that.