Search the Community
Showing results for tags 'strategy'.
-
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