Jump to content

Search the Community

Showing results for tags 'strategy'.

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Blogs

  • Development Blog
  • Canardian's Blog
  • Tyler's Blog
  • macklebee's Blog
  • B-Byrnes' Blog
  • Marleys Ghost's Blog
  • omid3098's Blog
  • Richardsimpo123456's Blog
  • DJDD's Blog
  • Rick's Blog
  • --
  • Economically Disadvantaged
  • klepto2's Blog
  • Old Blog
  • Chris Paulson's Blog
  • Davaris' Blog
  • nil
  • Rekindled Phoenix's Blog
  • Flexman's Blog
  • Kattemaksu Online
  • Niosop's Blog
  • 1299 RTS Game Project
  • Blitzbat's Blog
  • gordonramp's Blog
  • Andy Gilbert's Blog
  • Marcus' Blog
  • JornAggror Blog
  • diedir's Blog
  • wayneg's Blog
  • Masterxilo's Blog
  • peubuy's blog
  • OD Arts -Leadwerks Blog
  • Leadwerks News
  • Dave Lee's Blog
  • The progression......
  • The Progression
  • Pancake's Blog
  • Test Blog
  • Pure³d's Blog
  • dreamhead Blog
  • RVL's Blog
  • DB's Blog
  • Toxin Games Development Blog
  • CombatHelo Blog (RSS Import)
  • Foolish's Blog
  • ZioRed's Corner
  • BLaBZ Blog
  • Toxin Games Twitter
  • Shard - Third Initiative
  • Shard's Blog
  • Soamp's Blog
  • Soamp's Blog
  • Laurens' Blog
  • JT`s warehouse district
  • Kennar's Blog
  • KiteFuchs' Blog
  • KiteFuchs' Blog
  • Eagle's Blog
  • Rachel's Dev-Blog
  • zaphos' Blog
  • please delete this blog
  • Afke's Blog
  • Richard Simpson
  • knowledgegranted's Blog
  • EdzUp(GD)'s Blog
  • aGameLife's Kelasel MMORPG Blog
  • Beyond Civilization Blog Ext
  • tournamentdan's Blog
  • Beyond Civilization Blog
  • smashthewindow
  • AnniXa's Blog
  • AnniXa's Blog
  • Elemental Development
  • Aria's Blog
  • Strogg76's Blog
  • Clackdor's Blog
  • Leadwerks Project Status
  • simpleprogrammer's Blog
  • Pathfinding in LE.2.x
  • Naughty Alien's Blog
  • smashthewindow's Blog
  • Ali Salehi's Blog
  • Jardar's Blog
  • DigitalHax Blog
  • Chris Vossen's Development Blog
  • Scarlet Thread Studios' Blog
  • Kronos' Blog
  • Benton's Blog
  • Screen Size Utility
  • ChrisV's Blog
  • tjheldna's Blog
  • shadmar's Blog
  • 3D Masons, LLC
  • Andy Gilbert's Enviro Models
  • ParaToxic's Blog
  • NarkLord's Blog
  • ChrisMAN's Blog
  • ChrisMAN's Blog
  • CGMan's Blog
  • The Game
  • Chris Tutorials
  • Leadwerks 3 Experience
  • An Alien Saga
  • klepto2 & Leadwerks 3
  • Inside the mind of eternal insomniac
  • josk's Blog
  • xtreampb's Blog
  • Rendering puzzles
  • Shader Development in Leadwerks 3.1
  • 3D Coat : Column game making of
  • Einlander's Blog
  • Ginger George's Blog
  • I have a problem
  • Michael_J's Blog
  • nasamydifol's Blog
  • Digman's Blog
  • noesisGUI
  • Remaining Days
  • test_external_blog
  • SavageDogg38's Blog
  • Built from Ruins
  • The Hunt For Food Blog
  • Crazy Minnow Studio
  • CrazyMinnowStudio
  • Guppy's Blog
  • Evayr's Blog
  • Playing Online
  • DerRidda's Blog
  • Karl's Blog
  • whiterabbit's Blog
  • Tinyboss Games
  • abendkleider's Blog
  • sacguccireplica's Blog
  • Arena
  • Imchasinyou's Blog
  • EVE LBS Studio
  • xtom's Blog
  • Lua is better than you think.
  • mdgunn's Blog
  • Crime Closer
  • lxFirebal69xl's Blog
  • Pump-Action Captain
  • Igor's Blog
  • Dead Anyway
  • Lockdown, going forward.
  • Runenrise and more
  • reepblue's Blog
  • LUA Musings
  • severjack's Blog
  • mikeporter's Blog
  • miko93's Blog
  • Megalocerous' Blog
  • lxFirebal69xl's Blog
  • aiaf's Blog
  • Lostghbear's Blog
  • Wedmer's Blog
  • echo $BLOG_NAME > blog_title.tmpl
  • johnadam111's Blog
  • burgelkat's Blog
  • Charrua's Blog
  • peterpaul's Blog
  • joshmathews' Blog
  • Martin Kearl's Blog
  • Brutile's blog
  • tipforeveryone's Blog
  • Express Lab Games Blog
  • Glushchenko Blog
  • Blueapples' Blog
  • DooMAGE's Blog
  • Structura devblog
  • martyj's Blog
  • Dragonfreak's Blog
  • Brutile's Blog
  • assigmenthelp
  • Case Study Assignment Help
  • How to deal with bad grade ?
  • How to deal with bad grade ?
  • GameDev Blog
  • Marcousik's Creations Blog
  • devcjohnson's Blog
  • devcjohnson's Blog
  • THE WHAT? Blog
  • GUI Editor
  • Phodex Games Blog
  • GUI Tutorial
  • Dwarf Beard
  • The Seventh World
  • The Demurian Scribe
  • The Blog of Yue
  • Snowboarding Development Blog
  • Leadwerks VS Source 2
  • Work in Progress - Scifi PBR Media
  • [C++] First Player game start
  • Ocean: Rendering in Leadwerks 4
  • noob_shaders
  • Ultra App Kit (Advanced Custom Widgets)
  • Poking around
  • Slippy's Corner
  • UltraEngine - Experiences, add-ons and other Stuff
  • Blender tutorials
  • Game Ready Maps
  • Thirsty Panther
  • Ultimate Action Game Controller
  • Ultra Tutorials
  • SCP Dev Blog
  • Get 40% Off with Temu Coupon [acu639380] for All Users This Month
  • The Seamless Finish: How Coalescing Agents Perfect Coatings and Paints Globally

Forums

  • Software
    • General Discussion
    • Programming
    • Game Artwork
    • Showcase
    • Suggestion Box
    • Bug Reports
  • Addons

Categories

  • Streams and Events
  • Games
  • Work in Progress

Categories

  • Components
  • Environments
  • Heightmaps
  • Materials
    • Abstract
    • Asphalt
    • Brick
    • Concrete
    • Debris
    • Effects
    • Fabric
    • Ground
    • Marble
    • Metal
    • Organic
    • Plaster
    • Rock
    • Roof
    • Sand
    • SciFi
    • Snow
    • Surface Imperfections
    • Tile
    • Wood
  • Models
    • Animals and Creatures
    • Architecture
    • Characters
    • Food
    • Machinery
    • Plants and Vegetation
    • Props
    • Rocks
    • Vehicles
    • Tools
    • Weapons
  • Plugins
  • Skyboxes
  • Sound
    • Music
  • Tools

Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


Location

Found 2 results

  1. 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 and flag model 2. Create Unit component with bot behavior. 3. Add control over player units. 4. Making prefabs and new map In this tutorial used 1.0.0 Engine version from Steam Beta branch (Dev in Standalone version). Later versions, once they out, should be compitable code-wise. Just update shader via project sync in Project Manager in the Editor Asset import Load 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 dowload 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 rename those animations to use them properly later - open 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 called 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 choosed "Standing React Large From Right" animation. Next do same stuff as it was with Paladin - material, animations etc. In Transform make scale 0.85 so it would match Paladin size. Collider settings: Another model that we need is a flag which would represent a place that player units will try to reach It 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 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 and Unit.lua - can be done with + sign in the Editor 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.lua: Unit = {} Unit.name = "Unit" Unit.team = 0 -- 0 neutral, 1 player team, 2 enemy Unit.isPlayer = false -- so it could be added for entity with FPS Player component Unit.isFullPlayerControl = false Unit.health = 100 Unit.maxHealth = 100 -- used for AI navigation Unit.navMesh = nil -- NavAgent used to create to plot navigation paths in NavMesh Unit.agent = nil -- how far AI see its enemies in meters Unit.perceptionRadius = 10 -- how long to pursue when out of radius Unit.chaseMaxDistance = Unit.perceptionRadius * 2 -- is target a priority Unit.isForcedTarget = false -- target to follow and attack if possible Unit.targetWeak = nil -- to avoid fighting Unit.isForcedMovement = false -- which distance to point should be to reach it Unit.targetPointDistance = 0.5 -- place to reach Unit.targetPoint = nil -- is attack animation playing Unit.isAttacking = false -- when attack started Unit.meleeAttackTime = 0 -- do damage in meleeAttackTiming after attack start Unit.attackFrame = 5 Unit.attackRange = 2.0 Unit.attackDamage = 30 -- pain/git hit state Unit.isInPain = false -- can't start new pain animation immediately to avoid infinite stugger Unit.painCooldown = 300 -- when pain animation started Unit.painCooldownTime = 0 -- how fast unit is Unit.speed = 3.0 -- when to try scan again Unit.nextScanForTargetTime = 0 -- health bar above unit Unit.healthBar = nil Unit.healthBarBackground = nil Unit.isSelected = false -- to keep camera pointer for unit health bars Unit.camera = nil -- to be able to remove entity inside of component later Unit.sceneWeak = nil -- time in ms before delete model after a death, 0 of disabled Unit.decayTime = 10000 Unit.removeEntityTimer = nil -- animations Unit.attackName = "Attack" Unit.idleName = "Idle" Unit.painName = "Pain" Unit.deathName = "Death" Unit.runName = "Run" function Unit:Start() local entity = self.entity local model = Model(entity) entity:AddTag("Unit") -- for custom save/load system entity:AddTag("Save") if (self.isFullPlayerControl == false) then if (self.navMesh ~= nil) then -- 0.5 m radius because of Beast long model, 0.5 would better otherwise, 2 m height self.agent = CreateNavAgent(self.navMesh, 0.5, 2) self.agent:SetMaxSpeed(self.speed) self.agent:SetPosition(entity:GetPosition(true)) self.agent:SetRotation(entity:GetRotation(true).y) self.entity:SetPosition(0, 0, 0) -- becase models rotated by back self.entity:SetRotation(0, 180, 0) self.entity:Attach(self.agent) end entity:SetCollisionType(COLLISION_PLAYER) entity:SetMass(0) entity:SetPhysicsMode(PHYSICS_RIGIDBODY) end if (model ~= nil) then local seq = model:FindAnimation(self.attackName) if (seq ~= -1) then local count = model:CountAnimationFrames(seq) -- to disable attack state at end of attack animation model.skeleton:AddHook(seq, count - 1, Unit.EndAttackHook, self) -- to deal damage to target at range at specific animation frame model.skeleton:AddHook(seq, self.attackFrame, Unit.AttackHook, self) end seq = model:FindAnimation(self.painName) if (seq ~= -1) then local count = model:CountAnimationFrames(seq) -- to disable pain state at end of pain animation model.skeleton:AddHook(seq, count - 1, Unit.EndPainHook, self) end if (self.health <= 0) then seq = model:FindAnimation(self.deathName) local count = model:CountAnimationFrames(seq) model:Animate(self.deathName, 1.0, 250, ANIMATION_ONCE, count - 1) end end local world = entity:GetWorld() local cameras = world:GetTaggedEntities("Camera") for i = 1, #cameras do if (Camera(cameras[i]) ~= nil) then self.camera = Camera(cameras[i]) break end end if (self.camera == nil) then local entities = world:GetEntities() for i = 1, #entities do if (Camera(entities[i]) ~= nil) then self.camera = Camera(entities[i]) break end end end if (self.isFullPlayerControl == false) then local healthBarHeight = 5 self.healthBar = CreateTile(self.camera, self.maxHealth, healthBarHeight) if (self.team == 1) then self.healthBar:SetColor(0, 1, 0) else self.healthBar:SetColor(1, 0, 0) end self.healthBar:SetPosition(0, 0) self.healthBarBackground = CreateTile(self.camera, self.maxHealth, healthBarHeight) self.healthBarBackground:SetColor(0.1, 0.1, 0.1) self.healthBar:SetScale(self.health / self.maxHealth, 1) -- to put it behind health bar self.healthBarBackground:SetOrder(0) self.healthBar:SetOrder(1) end end function Unit:Load(properties, binstream, scene, flags, extra) self.sceneWeak = scene self.navMesh = nil if #scene.navmeshes > 0 then self.navMesh = scene.navmeshes[1] end if type(properties.isFullPlayerControl) == "boolean" then self.isFullPlayerControl = properties.isFullPlayerControl end if type(properties.isPlayer) == "boolean" then self.isPlayer = properties.isPlayer end if type(properties.isSelected) == "boolean" then self.isSelected = properties.isSelected end if type(properties.team) == "number" then self.team = properties.team end if type(properties.health) == "number" then self.health = properties.health end if type(properties.maxHealth) == "number" then self.maxHealth = properties.maxHealth end if type(properties.attackDamage) == "number" then self.attackDamage = properties.attackDamage end if type(properties.attackRange) == "number" then self.attackRange = properties.attackRange end if type(properties.attackFrame) == "number" then self.attackFrame = properties.attackFrame end if type(properties.painCooldown) == "number" then self.painCooldown = properties.painCooldown end if type(properties.enabled) == "boolean" then self.enabled = properties.enabled end if type(properties.painCooldown) == "number" then self.painCooldown = properties.painCooldown end if type(properties.decayTime) == "number" then self.decayTime = properties.decayTime end if type(properties.target) == "string" then self.targetWeak = scene:GetEntity(properties.target) end if type(properties.isForcedMovement) == "boolean" then self.isForcedMovement = properties.isForcedMovement end if type(properties.attackName) == "string" then self.attackName = properties.attackName end if type(properties.idleName) == "string" then self.idleName = properties.idleName end if type(properties.deathName) == "string" then self.deathName = properties.deathName end if type(properties.painName) == "string" then self.painName = properties.painName end if type(properties.runName) == "string" then self.runName = properties.runName end return true end function Unit:Save(properties, binstream, scene, flags, extra) properties.isFullPlayerControl = self.isFullPlayerControl properties.isPlayer = self.isPlayer properties.isSelected = self.isSelected properties.team = self.team properties.health = self.health if (self.targetWeak ~= nil) then properties.target = self.targetWeak:GetUuid() end properties.health = self.health properties.isForcedMovement = self.isForcedMovement end function Unit:Damage(amount, attacker) if not self:isAlive() then return end self.health = self.health - amount local world = self.entity:GetWorld() if world == nil then return end local now = world:GetTime() if (self.health <= 0) then self:Kill(attacker) elseif (not self.isInPain and (now - self.painCooldownTime) > self.painCooldown) then self.isInPain = true self.isAttacking = false local model = Model(self.entity) if (model ~= nil) then model:StopAnimation() model:Animate(self.painName, 1.0, 100, ANIMATION_ONCE) end if (self.agent ~= nil) then self.agent:Stop() end end if (self.healthBar) then -- reducing health bar tile width self.healthBar:SetScale(self.health / self.maxHealth, 1) end -- attack an atacker if (self.isForcedMovement == false and self.isForcedTarget == false) then self:attack(attacker) end end function Unit:Kill(attacker) --need to remove pointer to other entity so it could be deleted self.targetWeak = nil local entity = self.entity if (entity == nil) then return end local model = Model(entity) if (model) then model:StopAnimation() model:Animate(self.deathName, 1.0, 250, ANIMATION_ONCE) end if (self.agent) then -- This method will cancel movement to a destination, if it is active, and the agent will smoothly come to a halt. self.agent:Stop() end -- to remove nav agent entity:Detach() self.agent = nil -- to prevent it being obstacle entity:SetCollisionType(COLLISION_NONE) -- to prevent selection entity:SetPickMode(PICK_NONE) self.isAttacking = false self.healthBar = nil self.healthBarBackground = nil if (self.decayTime > 0) then self.removeEntityTimer = CreateTimer(self.decayTime) ListenEvent(EVENT_TIMERTICK, self.removeEntityTimer, Unit.RemoveEntityCallback, self) -- not saving if supposed to be deleted anyway entity:RemoveTag("Save") end end function Unit:isAlive() return self.health > 0 and self.entity ~= nil end function Unit.RemoveEntityCallback(ev, extra) local unit = Component(extra) unit.removeEntityTimer:Stop() unit.removeEntityTimer = nil unit.sceneWeak:RemoveEntity(unit.entity) unit.sceneWeak = nil return false end function Unit:scanForTarget() local entity = self.entity local world = entity:GetWorld() if (world ~= nil) then -- 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 it yet in Release mode, but it's better to have it optimized Debug as well anyway. local now = world:GetTime() if (now < self.nextScanForTargetTime) then return end self.nextScanForTargetTime = now + Random(100, 200) local entityPosition = entity:GetPosition(true) --simple copy would copy pointer to new var, so this case it's better recreate Vec3 here local positionLower = Vec3(entityPosition.x, entityPosition.y, entityPosition.z) positionLower.x = positionLower.x - self.perceptionRadius positionLower.z = positionLower.z - self.perceptionRadius positionLower.y = positionLower.y - self.perceptionRadius local positionUpper = Vec3(entityPosition.x, entityPosition.y, entityPosition.z) positionUpper.x = positionUpper.x + self.perceptionRadius positionUpper.z = positionUpper.z + self.perceptionRadius positionUpper.y = positionUpper.y + self.perceptionRadius -- will use it to determinate which target is closest local currentTargetDistance = -1 -- GetEntitiesInArea takes positions of an opposite corners of a cube as params local entitiesInArea = world:GetEntitiesInArea(positionLower, positionUpper) for k, foundEntity in pairs(entitiesInArea) do local foundUnit = foundEntity:GetComponent("Unit") -- targets are only alive enemy units if not (foundUnit == nil or not foundUnit:isAlive() or not foundUnit:isEnemy(self.team) or foundUnit.entity == nil) then local dist = foundEntity:GetDistance(entity) if not (dist > self.perceptionRadius) then -- check if no obstacles like walls between units local pick = world:Pick(entity:GetBounds(BOUNDS_RECURSIVE).center, foundEntity:GetBounds(BOUNDS_RECURSIVE).center, self.perceptionRadius, true, Unit.RayFilter, self) if (dist < 0 or currentTargetDistance < dist) then self.targetWeak = foundEntity currentTargetDistance = dist end end end end end end function Unit:Update() if self.entity == nil or not self:isAlive() then return end local world = self.entity:GetWorld() local model = Model(self.entity) if world == nil or model == nil then return end if self.isFullPlayerControl then return end -- making health bar follow the unit local window = ActiveWindow() if (window ~= nil and self.healthBar ~= nil and self.healthBarBackground ~= nil) then local framebuffer = window:GetFramebuffer() local position = self.entity:GetBounds().center position.y = position.y + self.entity:GetBounds().size.y / 2 -- take top position of unit if (self.camera ~= nil) then -- ransorming 3D position into 2D local unitUiPosition = self.camera:Project(position, framebuffer) unitUiPosition.x = unitUiPosition.x - self.healthBarBackground.size.x / 2 self.healthBar:SetPosition(unitUiPosition.x, unitUiPosition.y, 0.99901) self.healthBarBackground:SetPosition(unitUiPosition.x, unitUiPosition.y, 0.99902) local doShow = self.isSelected or (self.health ~= self.maxHealth and not self.isPlayer) self.healthBar:SetHidden(not doShow) self.healthBarBackground:SetHidden(not doShow) end end -- can't attack or move while pain animation if (self.isInPain == true) then return end local isMoving = false -- ignore enemies and move if (self.isForcedMovement == true and self:goTo()) then return end -- atacking part if (not isMoving) then local target = self.targetWeak -- Stop attacking if target is dead if (target ~= nil) then local distanceToTarget = self.entity:GetDistance(target) local doResetTarget = false if (distanceToTarget > self.chaseMaxDistance and ~self.isForcedTarget) then doResetTarget = true else for k, targetComponent in pairs(target.components) do local targetUnit = Component(targetComponent) if (targetUnit and not targetUnit:isAlive()) then doResetTarget = true self.isForcedTarget = false end break end end if (doResetTarget) then target = nil self.targetWeak = nil if (self.agent ~= nil) then self.agent:Stop() end end end if (self.isAttacking and target ~= nil) then -- rotating unit to target local a = ATan(self.entity.matrix.t.x - target.matrix.t.x, self.entity.matrix.t.z - target.matrix.t.z) if (self.agent) then self.agent:SetRotation(a + 180) end end if (target == nil) then self:scanForTarget() end if (target ~= nil) then local distanceToTarget = self.entity:GetDistance(target) -- run to target if out of range if (distanceToTarget > self.attackRange) then if (self.agent ~= nil) then self.agent:Navigate(target:GetPosition(true)) end self.isAttacking = false model:Animate(self.runName, 1.0, 250, ANIMATION_LOOP) else if (self.agent) then self.agent:Stop() end -- start attack if did not yet if (self.isAttacking == false) then self.meleeAttackTime = world:GetTime() model:Animate(self.attackName, 1.0, 100, ANIMATION_ONCE) self.isAttacking = true end end return end end if (self.targetPoint ~= nil and self:goTo()) then return end if (self.isAttacking == false) then model:Animate(self.idleName, 1.0, 250, ANIMATION_LOOP) if (self.agent ~= nil) then self.agent:Stop() end end end function Unit.RayFilter(entity, extra) local thisUnit = Component(extra) local pickedUnit = entity:GetComponent("Unit") --skip if it's same team return pickedUnit == nil or pickedUnit ~= nil and pickedUnit.team ~= thisUnit.team end function Unit.AttackHook(skeleton, extra) local unit = Component(extra) if (unit == nil) then return end local entity = unit.entity local target = unit.targetWeak if (target ~= nil) then local pos = entity:GetPosition(true) local dest = target:GetPosition(true) + target:GetVelocity(true) --attack in target in range if (pos:DistanceToPoint(dest) < unit.attackRange) then for k, targetComponent in pairs(target.components) do if targetComponent.Damage and type(targetComponent.Damage) == "function" then targetComponent:Damage(unit.attackDamage, entity) end end end end end function Unit.EndAttackHook(skeleton, extra) local unit = Component(extra) unit.attacking = false end function Unit.EndPainHook(skeleton, extra) local unit = Component(extra) if (unit ~= nil) then unit.isInPain = false if (unit:isAlive() and unit.entity:GetWorld()) then unit.painCooldownTime = unit.entity:GetWorld():GetTime() end end end function Unit:isEnemy(otherUnitTeam) return self.team == 1 and otherUnitTeam == 2 or self.team == 2 and otherUnitTeam == 1 end function Unit:goToEntity(targetPointEntity, isForced) if (targetPointEntity ~= nil) then self.isForcedMovement = isForced self.targetPoint = targetPointEntity self:goTo() end end function Unit:goToPosition(positionToGo, isForced) if (self.entity ~= nil) then self.isForcedMovement = isForced self.targetPoint = CreatePivot(self.entity:GetWorld()) self.targetPoint:SetPosition(positionToGo) self:goTo() end end function Unit:goTo() local doMove = false local model = Model(self.entity) if (self.targetPoint ~= nil and self.agent ~= nil) then doMove = self.agent:Navigate(self.targetPoint:GetPosition(true), 100, 2.0) --doMove is false if unit can't reach target point if (doMove) then --checking distance to target point on nav mesh local distanceToTarget = self.entity:GetDistance(self.agent:GetDestination(true)) local resultMaxDistance = self.targetPointDistance if (self.isForcedMovement) then --so that the player's units don't push each other trying to reach the point resultMaxDistance = resultMaxDistance * 2 end --stop moving to this point if close enough if (distanceToTarget < resultMaxDistance) then local wayPoint = self.targetPoint:GetComponent("WayPoint") if (wayPoint ~= nil and wayPoint.nextPoint ~= nil) then --move to next one if exist self.targetPoint = wayPoint.nextPoint doMove = true else self.targetPoint = nil doMove = false end else doMove = true end end if (doMove and model) then model:Animate(self.runName, 1.0, 250, ANIMATION_LOOP) end end return doMove end function Unit:attack(entityToAttack, isForced) self.targetWeak = nil if (entityToAttack == nil or entityToAttack:GetComponent("Unit") == nil or entityToAttack:GetComponent("Unit").team == self.team) then return end self.targetPoint = nil self.isForcedMovement = false self.isForcedTarget = isForced self.targetWeak = entityToAttack end function Unit:select(doSelect) self.isSelected = doSelect end RegisterComponent("Unit", Unit) return Unit For Unit class you need WayPoint component from previous tutorial. Also can be download here: https://www.ultraengine.com/community/files/file/3491-lua-waypoint/ 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.lua StrategyController = {} StrategyController.name = "StrategyController" StrategyController.selectedUnits = {} -- Control key state StrategyController.isControlDown = false StrategyController.playerTeam = 1 StrategyController.camera = nil StrategyController.unitSelectionBox = nil -- first mouse position when Mouse Left was pressed StrategyController.unitSelectionBoxPoint1 = iVec2(0, 0) -- height of selection box StrategyController.selectHeight = 4 -- mouse left button state StrategyController.isMouseLeftDown = false function StrategyController:Start() self.entity:AddTag("StrategyController") self.entity:AddTag("Save") -- For custom save/load system -- Listen() needed for calling ProcessEvent() in component when event happen self:Listen(EVENT_MOUSEDOWN, nil) self:Listen(EVENT_MOUSEUP, nil) self:Listen(EVENT_MOUSEMOVE, nil) self:Listen(EVENT_KEYUP, nil) self:Listen(EVENT_KEYDOWN, nil) -- optimal would be setting component to a camera if Camera(self.entity) ~= nil then self.camera = Camera(self.entity) else -- Otherwise, find the camera by tag for _, cameraEntity in ipairs(self.entity:GetWorld():GetTaggedEntities("Camera")) do self.camera = Camera(cameraEntity) break end end -- Create a 1x1 sprite for pixel-accurate scaling self.unitSelectionBox = CreateTile(self.camera, 1, 1) -- Set the sprite's color to transparent green self.unitSelectionBox:SetColor(0, 0.4, 0.2, 0.5) self.unitSelectionBox:SetPosition(0, 0, 0.00001) self.unitSelectionBox:SetHidden(true) -- Create a material for transparency local material = CreateMaterial() material:SetShadow(false) material:SetTransparent(true) -- material:SetPickMode(false) -- Use an unlit shader family material:SetShaderFamily(LoadShaderFamily("Shaders/Unlit.fam")) -- Assign the material to the sprite self.unitSelectionBox:SetMaterial(material) end function StrategyController.RayFilter(entity, extra) local pickedUnit = nil if (entity ~= nil) then pickedUnit = entity:GetComponent("Unit") end --skip if it's unit return pickedUnit == nil end function StrategyController:Load(properties, binstream, scene, flags, extra) if type(properties.playerTeam) == "number" then self.playerTeam = properties.playerTeam end for k in pairs (self.selectedUnits) do self.selectedUnits[k] = nil end if type(properties.selectedUnits) == "array" then for i = 1, #properties.selectedUnits do local unit = scene:GetEntity(properties.selectedUnits[i]); if (unit ~= nil) then self.selectedUnits[#self.selectedUnits+1] = unit; end end end return true end function StrategyController:Save(properties, binstream, scene, flags, extra) properties.playerTeam = self.playerTeam properties.selectedUnits = {}; for i=1, #self.selectedUnits do properties.selectedUnits[i] = self.selectedUnits[i]:GetUuid() end return true end function StrategyController:ProcessEvent(e) local window = ActiveWindow() if not window then return true end local mousePosition = window:GetMousePosition() if e.id == EVENT_MOUSEDOWN then if not self.camera then return end if e.data == MOUSE_LEFT then self.unitSelectionBoxPoint1 = iVec2(mousePosition.x, mousePosition.y) self.isMouseLeftDown = true -- Move or attack on Right Click elseif e.data == MOUSE_RIGHT then -- Get entity under cursor local pick = self.camera:Pick(window:GetFramebuffer(), mousePosition.x, mousePosition.y, 0, true) if pick.success and pick.entity then local unit = pick.entity:GetComponent("Unit") if unit and unit:isAlive() and unit.team ~= self.playerTeam then -- Attack the selected entity for _, entityWeak in ipairs(self.selectedUnits) do local entityUnit = entityWeak if entityUnit and entityUnit:GetComponent("Unit") then entityUnit:GetComponent("Unit"):attack(pick.entity, true) end end --checking if we have selected units elseif (#self.selectedUnits ~= 0) then local flag = LoadPrefab(self.camera:GetWorld(), "Prefabs/FlagWayPoint.pfb") if (flag ~= nil) then flag:SetPosition(pick.position) end -- Move to the selected position for _, entityWeak in ipairs(self.selectedUnits) do local entityUnit = entityWeak if entityUnit and entityUnit:GetComponent("Unit") then if (flag ~= nil) then entityUnit:GetComponent("Unit"):goToEntity(flag, true) else entityUnit:GetComponent("Unit"):goToPosition(pick.position, true) end end end end end end elseif e.id == EVENT_MOUSEUP then if not self.camera then return end -- Unit selection on Left Click if e.data == MOUSE_LEFT then if not self:selectUnitsByBox(self.camera, window:GetFramebuffer(), iVec2(mousePosition.x, mousePosition.y)) then local pick = self.camera:Pick(window:GetFramebuffer(), mousePosition.x, mousePosition.y, 0, true) if pick.success and pick.entity then local unit = pick.entity:GetComponent("Unit") if unit and unit.isPlayer and unit:isAlive() then if not self.isControlDown then self:deselectAllUnits() end table.insert(self.selectedUnits, pick.entity) unit:select(true) else self:deselectAllUnits() end else self:deselectAllUnits() end end self.isMouseLeftDown = false end elseif e.id == EVENT_KEYUP then if e.data == KEY_CONTROL then self.isControlDown = false end elseif e.id == EVENT_KEYDOWN then if e.data == KEY_CONTROL then self.isControlDown = true end end return true end function StrategyController:deselectAllUnits() for _, entityWeak in ipairs(self.selectedUnits) do local entityUnit = entityWeak if entityUnit and entityUnit:GetComponent("Unit") then entityUnit:GetComponent("Unit"):select(false) end end self.selectedUnits = {} end function StrategyController:Update() if not self.isMouseLeftDown then self.unitSelectionBox:SetHidden(true) else local window = ActiveWindow() if window then local mousePosition = window:GetMousePosition() local unitSelectionBoxPoint2 = iVec2(mousePosition.x, mousePosition.y) local upLeft = iVec2(math.min(self.unitSelectionBoxPoint1.x, unitSelectionBoxPoint2.x), math.min(self.unitSelectionBoxPoint1.y, unitSelectionBoxPoint2.y)) local downRight = iVec2(math.max(self.unitSelectionBoxPoint1.x, unitSelectionBoxPoint2.x), math.max(self.unitSelectionBoxPoint1.y, unitSelectionBoxPoint2.y)) -- Don't show the selection box if it's too small (could be a single click) if (downRight.x - upLeft.x < 4) or (downRight.y - upLeft.y < 4) then self.unitSelectionBox:SetHidden(true) return end -- Set the position of the selection box self.unitSelectionBox:SetPosition(upLeft.x, upLeft.y) -- Calculate the width and height of the selection box local width = downRight.x - upLeft.x local height = downRight.y - upLeft.y -- Change the sprite size via scale (size is read-only) self.unitSelectionBox:SetScale(width, height) -- Make the selection box visible self.unitSelectionBox:SetHidden(false) end end end function StrategyController:selectUnitsByBox(camera, framebuffer, unitSelectionBoxPoint2) if not self.unitSelectionBox or self.unitSelectionBox:GetHidden() or not camera or not framebuffer then return false end -- Calculate the top-left and bottom-right corners of the selection box local upLeft = iVec2(Min(self.unitSelectionBoxPoint1.x, unitSelectionBoxPoint2.x), Min(self.unitSelectionBoxPoint1.y, unitSelectionBoxPoint2.y)) local downRight = iVec2(Max(self.unitSelectionBoxPoint1.x, unitSelectionBoxPoint2.x), Max(self.unitSelectionBoxPoint1.y, unitSelectionBoxPoint2.y)) -- Perform raycasting at the corners of the selection box local pick1 = camera:Pick(framebuffer, upLeft.x, upLeft.y, 0, true, StrategyController.RayFilter, nil) local pick2 = camera:Pick(framebuffer, downRight.x, downRight.y, 0, true, StrategyController.RayFilter, nil) if not pick1.success or not pick2.success then return false end -- Deselect all currently selected units self:deselectAllUnits() -- Calculate the lower and upper bounds of the selection area local positionLower = Vec3( math.min(pick1.position.x, pick2.position.x), math.min(pick1.position.y, pick2.position.y), math.min(pick1.position.z, pick2.position.z) ) local positionUpper = Vec3( math.max(pick1.position.x, pick2.position.x), math.max(pick1.position.y, pick2.position.y) + self.selectHeight, math.max(pick1.position.z, pick2.position.z) ) -- Find entities within the selection area for _, foundEntity in ipairs(camera:GetWorld():GetEntitiesInArea(positionLower, positionUpper)) do local foundUnit = foundEntity:GetComponent("Unit") -- Only select alive, player-controlled, and enemy units if foundUnit and foundUnit:isAlive() and foundUnit.isPlayer and foundUnit.team == self.playerTeam then table.insert(self.selectedUnits, foundUnit.entity) foundUnit:select(true) end end return true end RegisterComponent("StrategyController", StrategyController) return StrategyController 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 changing something without having to do it at every map, just open prefab .pfb file and do a change there. Now create Units subfolder in Prefabs folder for two units. Add to scene Paladin model. Add Paladin component, click on "Is Player Unit" checkbix 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. 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 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.lua update GameMenuButtonCallback function to avoid cursor being hidden: local function GameMenuButtonCallback(Event, Extra) if (KEY_ESCAPE == Event.data and Extra) then local game = Extra local isHidden = game.menuPanel:GetHidden() game.menuPanel:SetHidden(not isHidden) if (game.player ~= nil) then if isHidden then window:SetCursor(CURSOR_DEFAULT) else window:SetCursor(CURSOR_NONE) end -- to stop cursor reset to center when menu on game.player.doResetMousePosition = not isHidden end end return false end Game.lua Also replace in main.lua "start.ultra" with "strategy.ultra" In result should be something like that: Final version here: https://github.com/Dreikblack/LuaTutorialProject/tree/3-making-strategy-game
  2. 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 0.9.9 engine version. 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 Unit component in AI folder via Editor or manually 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 Leadwerks; //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 Leadwerks : 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<Tile> healthBar; shared_ptr<Tile> 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 "Leadwerks.h" #include "Unit.h" #include "../Logic/WayPoint.h" using namespace Leadwerks; 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()); } } 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; if (!isFullPlayerControl) { int healthBarHeight = 5; healthBar = CreateTile(camera, maxHealth, healthBarHeight); if (team == 1) { healthBar->SetColor(0, 1, 0); } else { healthBar->SetColor(1, 0, 0); } //to put it before health bar healthBar->SetOrder(1); healthBarBackground = CreateTile(camera, maxHealth, healthBarHeight); healthBarBackground->SetColor(0.1f, 0.1f, 0.1f); healthBarBackground->SetOrder(0); } 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 tile width healthBar->SetScale((float)health / (float)maxHealth, 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); unitUiPosition.x -= healthBarBackground->size.x / 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 "Leadwerks.h" using namespace Leadwerks; 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<Tile> 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 "Leadwerks.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 = CreateTile(cameraWeak.lock(), 1, 1); //transparent green color unitSelectionBox->SetColor(0, 0.4f, 0.2, 0.5f); 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, upLeft.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); 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. 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; } GameAndMenu.zip In result should be something like that: Final version here: https://github.com/Dreikblack/CppTutorialProject/tree/3-making-strategy-game GameAndMenu.zip StrategyController.zip
×
×
  • Create New...