Search the Community
Showing results for tags 'LUA'.
-
In this tutorial will be used Tile, Camera:Pick() and Entity:GetDistance FPSPlayer component of FPS Template will be used as a base to modify Algorithm: 1. Create a Tile with text prompt 2. In Update function of component check if player look at usable object at close distance 3. if the check is passed then show prompt at center of screen Result will looks like that: Let's implement this Algorithm: 1. Add new component variable, for example after: FPSPlayer.movement = Vec3(0) New code line: FPSPlayer.usePromtTile = nil Now in FPSPlayer:Load() function after: if not self.camera then self.camera = CreateCamera(world) self.camera:Listen() Add: local font = LoadFont("Fonts/arial.ttf") self.usePromtTile = CreateTile(self.camera, font, "Press E", 20, TEXT_CENTER, 1.5) This overload used (more short one for text currently not available when tutorial is written): CreateTile(Camera camera, Font font, string text, number fontsize, number alignment, number linespacing) Tile is something like mini GUI widget which can be used to show text or rectangles without creating Interface. 2 and 3. Inside of FPSPlayer:Update() in "if window then" scope before its end (after 392th line) add: --to decide later if we want to show or hide prompt tile local doHideTile = true --cx and cy are screen center coordinates which were created above local pickInfo = self.camera:Pick(framebuffer, cx, cy, 0, true) if pickInfo.success and pickInfo.entity and pickInfo.entity:GetDistance(self.entity) < 2.0 then --iterate all components of picked entity for _, component in ipairs(pickInfo.entity.components) do --find out if component of picked entity have Use function and it's enabled if component.Use and type(component.Use) == "function" and component:GetEnabled() then --move tile to center of screen self.usePromtTile:SetPosition(cx, cy) doHideTile = false --stop iterating once we found usable object break end end end self.usePromtTile:SetHidden(doHideTile) Result file: FPSPlayer.lua
-
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
-
Lua Ultra Beginner's Guide #2 - making and using components
Dreikblack posted a blog entry in Ultra Tutorials
In this tutorial we will make a newcomponent, which will be moving an entity to way points and movement start will be activated by trigger zone Let's start with making WayPoint component: In the Ultra Editor click plus button in Project tab: Now open Visual Studio Code. Open WayPoint.json "properties" is a list of component's fields avaible for edit in the Editor "name" - actual name of property that will be used in code later "label" - just a name to display in the Editor "value" - initial value that property will have after map load by default. Can be changed in the Editor for specific entity. "options" - allows to choose int value in Combo Box in the Editor. First option - 0 value Default value here also defines type of this property. New component made via editor have all possible types. Replace WayPoint.json content with: { "component": { "properties": [ { "name": "nextPoint", "label": "Next point", "value": null }, { "name": "doStayOnPoint", "label": "Do stay on point", "value": false } ] } } nextPoint - another WayPoint, where platform will move to once reach this one doStayOnPoint - wait for command before moving to next WayPoint Take a note that the Editor sees only json which could be same for LUA and C++ projects which allows to work on same map even if people have different engine versions (Pro/Standard) or make a level before programming components. Replace WayPoint.lua content with: WayPoint = {} -- name should always match class name for correct component work WayPoint.name = "WayPoint" WayPoint.nextPoint = nil -- wait for command before moving to next WayPoint WayPoint.doStayOnPoint = false -- Start is called when Load() of all components was called already function WayPoint:Start() end -- will be called on map load function WayPoint:Load(properties, binstream, scene, flags, extra) -- internally entity saves in the Editor as String unique id -- can be empty if this way point is final if type(properties.nextPoint) == "string" then for _, entity in ipairs(scene.entities) do if properties.nextPoint == entity:GetUuid() then self.nextPoint = entity break end end -- self.nextPoint = scene:GetEntity(properties.nextPoint) if type(properties.doStayOnPoint) == "boolean" then self.doStayOnPoint = properties.doStayOnPoint end end return true end -- Can be used to save current component state on map save function WayPoint:Save(properties, binstream, scene, flags, extra) if self.nextPoint ~= nil then properties.nextPoint = self.nextPoint:GetUuid() properties.doStayOnPoint = self.doStayOnPoint; end return true end -- Can be used to get copy of this component function WayPoint:Copy() local t = {} local k local v for k, v in pairs(self) do t[k] = v end return t end -- needed for correct work, when loaded from a map RegisterComponent("WayPoint", WayPoint) return WayPoint Let's create our floating object component and call it WayMover WayMover.json: { "component": { "properties": [ { "name": "moveSpeed", "label": "Move Speed", "value": 4.0 }, { "name": "nextPoint", "label": "Next point", "value": null }, { "name": "doDeleteAfterMovement", "label": "Del after move", "value": false } ], "inputs": [ { "name": "DoMove" } ], "outputs": [ { "name": "EndMove" } ] } } moveSpeed - how fast entity will move to way point doDeleteAfterMovement - auto remove entity when it's reach final waypoint. Can be used for door, that goes into walls or floor inputs - it's commands for components, that usually triggered by another components via flowgrough outputs - commands that component sends to other components inputs via FireOutputs("EndMove"); in the component code WayMover.lua: WayMover = {} -- name should always match class name for correct component work WayMover.name = "WayMover" WayMover.moveSpeed = 4.0 WayMover.isMoving = false WayMover.nextPoint = nil WayMover.scene = nil function WayMover:Copy() local t = {} local k local v for k, v in pairs(self) do t[k] = v end return t end -- will be called on map load function WayMover:Load(properties, binstream, scene, flags, extra) if type(properties.moveSpeed) == "number" then self.moveSpeed = properties.moveSpeed end if type(properties.isMoving) == "boolean" then self.isMoving = properties.isMoving end if type(properties.doDeleteAfterMovement) == "boolean" then self.doDeleteAfterMovement = properties.doDeleteAfterMovement end if type(properties.nextPoint) == "string" then for _, entity in ipairs(scene.entities) do if properties.nextPoint == entity:GetUuid() then self.nextPoint = entity break end end -- self.nextPoint = scene:GetEntity(properties.nextPoint) -- need scene for removing entity on doDeleteAfterMovement condition self.scene = scene return true end end -- Can be used to save current component state on map save function WayMover:Save(properties, binstream, scene, flags, extra) if self.nextPoint ~= nil then properties.nextPoint = self.nextPoint:GetUuid() properties.doStayOnPoint = self.doStayOnPoint; end properties.moveSpeed = self.moveSpeed; properties.isMoving = self.isMoving; properties.doDeleteAfterMovement = self.doDeleteAfterMovement; return true end function WayMover:DoMove() self.isMoving = true end function WayMover:MoveEnd() local doStay = false if (self.nextPoint ~= nil) then doStay = self.nextPoint:GetComponent("WayPoint").doStayOnPoint self.nextPoint = self.nextPoint:GetComponent("WayPoint").nextPoint end if (doStay or self.nextPoint == nil) then self.isMoving = false; self:FireOutputs("EndMove") -- deleting entity if need to, after reaching final way point if (not doStay and self.nextPoint == nil and self.doDeleteAfterMovement and self.scene ~= nil) then --commented out this code for now until bind for RemoveEntity will added --self.scene:RemoveEntity(self.entity) end end end function WayMover:Update() if (not self.isMoving) then return; end local wayPoint = self.nextPoint if (self.entity == nil or wayPoint == nil) then return end --60 HZ game loop, change to own value if different to keep same final speed local speed = self.moveSpeed / 60.0 local targetPosition = wayPoint:GetPosition(true) --moving to point with same speed directly to point no matter which axis local pos = self.entity:GetPosition(true); local distanceX = Abs(targetPosition.x - pos.x); local distanceY = Abs(targetPosition.y - pos.y); local distanceZ = Abs(targetPosition.z - pos.z); local biggestDelta = distanceZ; if (distanceX > distanceY and distanceX > distanceZ) then biggestDelta = distanceX; elseif (distanceY > distanceX and distanceY > distanceZ) then biggestDelta = distanceY; end local moveX = MoveTowards(pos.x, targetPosition.x, speed * (distanceX / biggestDelta)); local moveY = MoveTowards(pos.y, targetPosition.y, speed * (distanceY / biggestDelta)); local moveZ = MoveTowards(pos.z, targetPosition.z, speed * (distanceZ / biggestDelta)); self.entity:SetPosition(moveX, moveY, moveZ) if (self.entity:GetPosition(true) == targetPosition) then self:MoveEnd() end end -- needed for correct work, when loaded from a map RegisterComponent("WayMover", WayMover) return WayMover Now we can use just made component in practice. One of things that can be made is door or secret wall activated by player actions and this door will move a little bit inward and then to the side inside of wall. After that invisible now door will be removed. Create a walls with a empty place between them. Create couple of Empty/pivots and attach WayPoints to them. First WayPoint place a same place where door will be, but offset a bit deep into. In Scene tab grab and drag 2nd WayPoint to Nex Point field of 1st WayPoint. Place 2nd WayPoint insde of the wall. Create a door between walls. Attach WayMover component to it. Grab and drag 1st WayPoint to door's WayMover Next Point field. Enable "Del after Move" in WayMover component Create a box before door, make its collision type a trigger: Add Collision Trigger component to it. Open Flowgraph (2nd button at left side of the Editor). Drag and Drop trigger and door to it from Scene tab. In different order, but same result in video format: Result should looks something like that in game: In fast debug mode it might crash at this moment for unkown reason. Use Full Debug or Run mode. Project files: LuaExample2.zip Repository: https://github.com/Dreikblack/LuaTutorialProject/tree/2-making-and-using-components -
Lua Beginner's Guide - first map, Main Menu, Loading Screen and GUI
Dreikblack posted a blog entry in Ultra Tutorials
Prerequisites https://www.ultraengine.com/learn/luasetup?lang=lua You can use Engine's IDE for scripting, but VS Code in most cases is better. This Lua extension is quite useful for VS Code, if you don't mind Tencent. Map Create a simple map call start.ultra with a brush as floor and a Empty aka pivot with FPSPlayer component How to do it: Now you can press "F5" in the editor, or select "Game" then "run" to run your game to just made map: You can add skybox in Edit > World Settings (at top buttons in the Editor) by choosing skybox.dds from Materials\Environment\Default Main menu, loading screen and GUI Create a simple menu with couple brushes and camera and name this map menu.ultra Now open main.lua. For that open Project tab in top right corner, select root project folder and double click on Open the VSCode.bat file. If this fails make sure you have setup Visual Studio Code correctly https://www.ultraengine.com/learn/luasetup?lang=lua Select all in file and delete it from VS Code. Now paste there folllow code with just a Loading screen, almost without anything else and save: -- local before variable means it will be avaiable only in current scope (inside script, funtion, cycle or condition body) -- loading screen vars local loadingWorld = nil local loadingCamera = nil local loadingText = nil local loadingBackground = nil -- for using in main loop local currentWorld = nil local currentUi = nil local displays = GetDisplays() -- You can WINDOW_FULLSCREEN to styles to make game fullscreen, 3rd and 4th are resolution size window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[1], WINDOW_CENTER | WINDOW_TITLEBAR) -- Create a framebuffer, needed for rendering framebuffer = CreateFramebuffer(window) font = LoadFont("Fonts/arial.ttf"); loadingWorld = CreateWorld(); local centerX = framebuffer:GetSize().x * 0.5 local centerY = framebuffer:GetSize().y * 0.5 local labelHeight = framebuffer:GetSize().y * 0.2 loadingBackground = CreateSprite(loadingWorld, framebuffer.size.x, framebuffer.size.y) loadingBackground:SetColor(0.2, 0.2, 0.2) loadingBackground:SetRenderLayers(2) loadingBackground:SetPosition(centerX, centerY, 0) loadingText = CreateSprite(loadingWorld, font, "LOADING", labelHeight, TEXT_CENTER | TEXT_MIDDLE) loadingText:SetPosition(centerX, centerY + labelHeight * 0.5, 0) -- 0 layer - no render, 1 - default render, we will use 2 for UI and sprites loadingText:SetRenderLayers(2) -- Creating camera for sprites, which needs to be orthographic (2D) for UI and sprites if they used as UI loadingCamera = CreateCamera(loadingWorld, PROJECTION_ORTHOGRAPHIC); loadingCamera:SetPosition(centerX, centerY, 0); -- camera render layer should match with stuff that you want to be visible for this camera. RenderLayers is a bit mask, so you can combine few layers, but probably you don't need it in most cases loadingCamera:SetRenderLayers(2) currentWorld = loadingWorld -- simple minimum game loop while window:Closed() == false do -- Garbage collection step collectgarbage() -- getting all events from queue - input, UI etc. while (PeekEvent()) do local event = WaitEvent() -- You need to do it for UI in 3D scene if (currentUi) then currentUi:ProcessEvent(event) end end if (currentWorld) then -- Update game logic (positions, components etc.). By default 60 HZ and not depends on framerate if you have 60+ FPS currentWorld:Update() -- 2nd param is VSync (true by default), 3rd is fps limit. Can by changed dynamically. currentWorld:Render(framebuffer) end end Now if we start a game we will see this loading screen: And once we have a screen we can create a menu to load in. Create MainMenu.lua in Source folder. To do it in Visual Studio Code select "File" then select "New file". Type in "MainMenu.lua" and hit return. A dialog box will open asking where you want to save this file. It should open in the source folder by default but if it dosen't navigate to the source folder and hit return. Paste in the following content for this file and save: local function NewGameButtonCallback(Event, Extra) --this overload currently is not working --EmitEvent(EVENT_GAME_START, nil, 0, 0, 0, 0, 0, nil, "start.ultra") EmitEvent(EVENT_GAME_START) return true end local function ExitButtonCallback(Event, Extra) --to close application os.exit() return true end local function InitMainMenu(mainMenu) mainMenu.world = CreateWorld() mainMenu.scene = LoadMap(mainMenu.world, "Maps/menu.ultra") local frameSize = framebuffer:GetSize() --Create camera for GUI mainMenu.uiCamera = CreateCamera(mainMenu.world, PROJECTION_ORTHOGRAPHIC) mainMenu.uiCamera:SetPosition(frameSize.x * 0.5, frameSize.y * 0.5, 0) mainMenu.uiCamera:SetRenderLayers(2) --for correct rendering above 3D scene mainMenu.uiCamera:SetClearMode(CLEAR_DEPTH) --Create user interface mainMenu.ui = CreateInterface(mainMenu.uiCamera, font, frameSize) mainMenu.ui:SetRenderLayers(2) --to make backgrount transparent mainMenu.ui.background:SetColor(0.0, 0.0, 0.0, 0.0) --Menu buttons local newGameButton = CreateButton("New game", frameSize.x / 2 - 100, 125, 200, 50, mainMenu.ui.background) ListenEvent(EVENT_WIDGETACTION, newGameButton, NewGameButtonCallback) local exitButton = CreateButton("Exit", frameSize.x / 2 - 100, 200, 200, 50, mainMenu.ui.background) ListenEvent(EVENT_WIDGETACTION, exitButton, ExitButtonCallback) end --functions should be declared after another function that this fucntion uses function CreateMainMenu() local mainMenu = {} InitMainMenu(mainMenu) return mainMenu end Let's get back to main.lua. We need to import main menu script and custom event ids to top: import "Source/MainMenu.lua" --custom events ids EVENT_GAME_START = 1001 EVENT_MAIN_MENU = 1002 Now add after "local currentUi = nil" menu var and callback fuctions for events: framebuffer = nil local menu = nil local function StartGameEventCallback(Event, Extra) --nothing just for now return true; end --function should be declared after vars that this function uses local function MainMenuEventCallback(Event, Extra) menu = CreateMainMenu(); --switching current render and update targets for loop currentWorld = menu.world; currentUi = menu.ui; return true; end Put this above main loop: --to show Loading screen before Main Menu loadingWorld:Render(framebuffer); --ListenEvent are needed to do something in callback function when specific even from specfic source (or not, if 2nd param is nil) emitted ListenEvent(EVENT_GAME_START, nil, StartGameEventCallback); ListenEvent(EVENT_MAIN_MENU, nil, MainMenuEventCallback); --let's try it out! EmitEvent(EVENT_MAIN_MENU) Just in case if something one wrong, current main.lua main.lua We need to modify FPSPlayer component just a little bit. Via Editor you can find it by selecting "Project" ( Top right of the editor screen), select "Source", select "Components", select "Player". Double click "FPSPlayer.lua". Add to its top to other vars a new one (24th line to be precisely) FPSPlayer.doResetMousePosition = true And new condition for mouse position reset in FPSPlayer.lua in Update() at 286 line or find thine line with Ctrl-F and past to search field "window:SetMousePosition(cx, cy)" if doResetMousePosition then window:SetMousePosition(cx, cy) end One big thing left - a game to play. Add Game.lua to project at Source folder: 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 isHidden then window:SetCursor(CURSOR_DEFAULT) else window:SetCursor(CURSOR_NONE) end if (game.player ~= nil) then --to stop cursor reset to center when menu on game.player.doResetMousePosition = not isHidden end end return false end local function MainMenuButtonCallback(Event, Extra) EmitEvent(EVENT_MAIN_MENU) return true end local function ExitButtonCallback(Event, Extra) os.exit() return true end local function InitGame(game, mapPath) game.world = CreateWorld() game.scene = LoadScene(game.world, mapPath) for k, entity in pairs(game.scene.entities) do local foundPlayer = entity:GetComponent("FPSPlayer") if (foundPlayer ~= nil) then game.player = foundPlayer break end end --Create user interface for game menu local frameSize = framebuffer:GetSize() game.uiCamera = CreateCamera(game.world, PROJECTION_ORTHOGRAPHIC) game.uiCamera:SetPosition(frameSize.x * 0.5, frameSize.y * 0.5, 0) game.uiCamera:SetRenderLayers(2) game.uiCamera:SetClearMode(CLEAR_DEPTH) game.ui = CreateInterface(game.uiCamera, font, frameSize) game.ui:SetRenderLayers(2) game.ui.background:SetColor(0.0, 0.0, 0.0, 0.0) --widgets are stays without extra pointers because parent widet, game.ui.background in this case, keep them --to remove widget you should do widget:SetParent(nil) game.menuPanel = CreatePanel(frameSize.x / 2 - 150, frameSize.y / 2 - 125 / 2, 300, 250, game.ui.background) local menuButton = CreateButton("Main menu", 50, 50, 200, 50, game.menuPanel) ListenEvent(EVENT_WIDGETACTION, menuButton, MainMenuButtonCallback) local exitButton = CreateButton("Exit", 50, 150, 200, 50, game.menuPanel) ListenEvent(EVENT_WIDGETACTION, exitButton, ExitButtonCallback) --we don't need game menu on screen while playing game.menuPanel:SetHidden(true) --and we will need it once hitting Esc button ListenEvent(EVENT_KEYUP, window, GameMenuButtonCallback, game) end --functions should be declared after another function that this fucntion uses function CreateGame(mapPath) local game = {} InitGame(game, mapPath) return game end Add new Game include and global var to main.lua: import "Source/Game.lua" local game = nil Update function callbacks at 24 line: local function StartGameEventCallback(Event, Extra) --destroying a main menu menu = nil --to show loading screen loadingWorld:Render(framebuffer) if Event.text ~= nil and Event.text ~= "" then --in lua .. used for string concatenation game = CreateGame("Maps/" .. Event.text); else game = CreateGame("Maps/start.ultra"); end --switching current render and update targets for loop currentWorld = game.world currentUi = game.ui return true; end --function should be declared after vars that this function uses local function MainMenuEventCallback(Event, Extra) --destroying a game instance if one existed game = nil --to show loading screen loadingWorld:Render(framebuffer) menu = CreateMainMenu() --switching current render and update targets for loop currentWorld = menu.world; currentUi = menu.ui; return true; end And in the end we have a game with own menu: All created and modified classes: LuaMenuGameLoad.zip Repository: https://github.com/Dreikblack/LuaTutorialProject/tree/menu-loading-screen-and-gui -
I don't see in the tutorial how to display a texture on the HUD. Let's say I want to display a red cross icon as a health icon and break the Geneva convention In Leadwerks 4 this was done in PostRender(). But how is it done now in Ultra Engine?
-
Hi It would be really helpful to get this coverted to LUA to assist in development of Steamworks/Multiplayer. It's been tricky to use only the basic documentation to get it done, without an example. https://www.leadwerks.com/learn/Steamworks_GetPacket?lang=cpp Example This example demonstrates lobbies, voice chat, and simple player movement. #include "UltraEngine.h" #include "Steamworks/Steamworks.h" #include "ComponentSystem.h" using namespace UltraEngine; class Player : public Object { public: static inline std::map<uint64_t, shared_ptr<Player> > players; shared_ptr<Entity> entity; WString name; uint64_t userid; static std::shared_ptr<Player> Get(shared_ptr<World> world, const uint64_t userid) { if (players[userid]) return players[userid]; auto player = std::make_shared<Player>(); player->entity = CreatePivot(world); auto model = CreateCylinder(world, 0.25, 1.8); model->SetPosition(0, 0.9, 0); model->SetParent(player->entity); model->SetCollider(nullptr); player->userid = userid; players[userid] = player; return player; } static void Remove(const uint64_t userid) { players[userid] = nullptr; } }; struct PlayerState { Vec3 position; float yaw; }; int main(int argc, const char* argv[]) { // Initialize Steam if (not Steamworks::Initialize()) { RuntimeError("Steamworks failed to initialize."); return 1; } // Get the displays auto displays = GetDisplays(); // Create a window auto window = CreateWindow("Ultra Engine", 0, 0, 1280 * displays[0]->scale, 720 * displays[0]->scale, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR); // Create a framebuffer auto framebuffer = CreateFramebuffer(window); // Create world auto world = CreateWorld(); world->SetGravity(0, -18, 0); // Create lobby auto lobbyid = Steamworks::CreateLobby(Steamworks::LOBBY_PUBLIC); Print("Lobby: " + String(lobbyid)); // Spawn local player auto player = Player::Get(world, Steamworks::GetUserId()); player->entity->AddComponent<FirstPersonControls>(); // Add lighting auto light = CreateDirectionalLight(world); light->SetRotation(55, 35, 0); // Add a floor auto floor = CreateBox(world, 50, 1, 50); floor->SetPosition(0, -0.5, 0); auto mtl = CreateMaterial(); mtl->SetTexture(LoadTexture("https://github.com/UltraEngine/Documentation/raw/master/Assets/Materials/Developer/griid_gray.dds")); floor->SetMaterial(mtl); // Main loop while (not window->KeyDown(KEY_ESCAPE) and not window->Closed()) { while (PeekEvent()) { const auto e = WaitEvent(); switch (e.id) { case Steamworks::EVENT_LOBBYINVITEACCEPTED: case Steamworks::EVENT_LOBBYDATACHANGED: case Steamworks::EVENT_LOBBYUSERJOIN: case Steamworks::EVENT_LOBBYUSERLEAVE: case Steamworks::EVENT_LOBBYUSERDISCONNECT: auto info = e.source->As<Steamworks::LobbyEventInfo>(); auto username = Steamworks::GetUserName(info->userid); switch (e.id) { case Steamworks::EVENT_LOBBYINVITEACCEPTED: Print("Invite accepted to lobby " + String(info->lobbyid)); lobbyid = info->lobbyid; if (not Steamworks::JoinLobby(info->lobbyid)) { lobbyid = 0; Print("Failed to join lobby"); } break; case Steamworks::EVENT_LOBBYDATACHANGED: Print("New lobby owner " + username); break; case Steamworks::EVENT_LOBBYUSERJOIN: Print("User " + username + " joined"); if (not Player::players[info->userid]) { // Spawn remote player Player::Get(world, info->userid); } break; case Steamworks::EVENT_LOBBYUSERLEAVE: Print("User " + username + " left"); // Remove remote player Player::Remove(info->userid); break; case Steamworks::EVENT_LOBBYUSERDISCONNECT: Print("User " + username + " disconnected"); // Remove remote player Player::Remove(info->userid); break; } break; } } // Receive player data PlayerState state; while (true) { auto pak = Steamworks::GetPacket(); if (not pak) break; if (pak->data->GetSize() == sizeof(PlayerState)) { auto player = Player::Get(world, pak->userid); if (player) { pak->data->Peek(0, (const char*)&state, pak->data->GetSize()); player->entity->SetPosition(state.position); player->entity->SetRotation(state.yaw); } } } //Receive text messages while (true) { auto pak = Steamworks::GetPacket(1); if (not pak) break; String s = pak->data->PeekString(0); Print(Steamworks::GetUserName(pak->userid) + ": " + WString(s)); } // Send player data auto userid = Steamworks::GetUserId(); auto player = Player::players[userid]; state.position = player->entity->position; state.yaw = player->entity->rotation.y; Steamworks::BroadcastPacket(lobbyid, &state, sizeof(PlayerState), 0, Steamworks::P2PSEND_UNRELIABLENODELAY); // Enable voice chat when the C key is pressed bool record = window->KeyDown(KEY_C); Steamworks::RecordVoice(record); String title = "Ultra Engine"; if (record) title += " (Microphone Enabled)"; window->SetText(title); // Update world world->Update(); // Render world world->Render(framebuffer); // Update Steamworks Steamworks::Update(); } // Close Steam Steamworks::Shutdown(); return 0; }
- 2 replies
-
- lua
- steamworks
-
(and 1 more)
Tagged with:
-
Oddly, I cannot create an interface attached to the camera anymore, I think this was working fine yesterday or day before. This code, silent crashes, in fast debug or full debug. Cam exists and so does the framebuffer. ConsolePadfont = LoadFont("Fonts/arial.ttf") --Create user interface ConsolePadui = CreateInterface(cam, ConsolePadfont, framebuffer.size)
-
LuaExample2.zip 1. Run in Fast debug mode 2. Hit start game btn in menu 3. Just after first frame of map apps closes without error notifications or new errors in debug console btw i have strange errors on map load in any debug mode like: Error: Failed to load scene "Path/ProjectName" Deleting prefab "Path/ProjectName" Loading prefab "Path/ProjectName" Error: Failed to load sound "Sound/Impact/bodypunch3.wav"
-
No error windows or in consoles, just app closing when GetDistance called. Need this working for tutorial local displays = GetDisplays(); --Create a window local window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[1], WINDOW_CENTER | WINDOW_TITLEBAR) --Create a framebuffer local framebuffer = CreateFramebuffer(window) --Create a world local world = CreateWorld() --Create a camera local camera = CreateCamera(world) local distanceToTarget = camera:GetDistance(Vec3(1,1,1)) Print(distanceToTarget) --Main loop while not window:Closed() and not window:KeyDown(KEY_ESCAPE) do world:Update() world:Render(framebuffer) end
-
Need asap it for lua variation of my 3rd tutorial -- Get the displays local displays = GetDisplays() -- Create a window local window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[1], WINDOW_CENTER + WINDOW_TITLEBAR) -- Create a world local world = CreateWorld() -- Create a framebuffer local framebuffer = CreateFramebuffer(window) -- Create light local light = CreateBoxLight(world) light:SetRange(-10, 10) light:SetRotation(15, 15, 0) light:SetColor(2) -- Create camera local camera = CreateCamera(world) camera:SetClearColor(0.125) camera:SetPosition(0, 0, -3) camera:SetFov(70) camera:Project(Vec3(0,0,0), framebuffer) -- Main loop while not window:Closed() and not window:KeyDown(KEY_ESCAPE) do -- Click on an object to change its color world:Update() world:Render(framebuffer) end
-
7 downloads
Component to move an entity to WayPoints: Move Speed - how much velocity entity will have while moving doDeleteAfterMovement - auto remove entity when it's reach final waypoint. Can be used for door, that goes into walls or floor Input "DoMove" - make entity move to next point Output "EndMove" - happens when entity stops after reaching final way point or if this way poiont has enabled doStayOnPoint -
-
11 downloads
Simple component which can be used to naviage bots or objects nextPoint - null if it's a final onem otherwise add another entity with this component to make a chain doStayOnPoint - can be checked by object that uses WayPoints to find out if it should stay after reaching this point and wait a command before moving to next one Put in Components\Logic folder -
If do something like that in lua script: EmitEvent(EVENT_WIDGETACTION, nil, 0, 0, 0, 0, 0, nil, "start.ultra")
-
This uses the Quake light animation presets to add some interesting color modulation to any entity. ColorChanger.zip The following presets are available: Normal Flicker Slow Strong Pulse Candle 1 Fast Strobe Gentle Pulse 1 Flicker 2 Candle 2 Candle 3 Slow Strobe Flourescent Flicker Slow Pulse Todo: The candle presents don't look right to me, but I'm not sure what they are supposed to look like. I remember them as being a gentle flicker. There is no interpolation between the nearest two values, it just grabs one single value, so it's not very smooth. I guess this is appropriate for the strobe effects, but not for others.
-
I've been spending the past few days trying to figure out how to add SDL2 to Leadwerks, as it has an additional Lua binding that would be nice to have. Not to mention the gamepad support (which is the main reason because just adding XInput only would give support for XBox 360 and Dualshock 3 controllers). Not having any experience with an engine like Leadwerks, application building, or libraries has severely impacted my ability to figure out what to do in my current situation. I know I need to include the library and expose it to Lua, but I don't know how to do that, and I don't know how to actually add it to the engine either. I've taken a look at some resources online, such as lazyfoo, and it hasn't really helped me understand. Any help or other resources that could be passed along to me would be greatly appreciated. Thanks!
- 4 replies
-
- programing
- lua
-
(and 2 more)
Tagged with:
-
Here the current Camera Dolly Component i created. It consists of 2 Components. The Camera Dolly Component itself. The Camera Dolly Event Component. Usage Extract and place it into your `Souce/Components` directory. Add a Camera to your Scene. Add a "Empty" Node to your scene that will contain the Camera Spline Path. Add "Empty" Nodes inside this CameraPath node that defines the path the camera will move along. (takes the rotation into account as well as long as no "Point Camera At" node is selected. Name each "Empty" Node path point "p1", "p2", "p3" and so forth. (subject to change) and order the nodes accordingly. At the end this should look something like this in your Scene list: In the Camera itself with the CameraDolly component added, select the CameraPath node and optionally a node where the camera should point at. If you want to do something as soon as the Camera reaches a point along the path, add the CameraDollyEvent Component to one of the path nodes and add it to the Flowgraph. Todo and known issues The order of nodes in the Editor is not always the same as returned by the API (see https://www.ultraengine.com/community/topic/65647-moving-entities-in-editor-looses-order-and-cannot-be-moved-to-top/ ) Its difficult to determine the rotation of points without visualisation. Its difficult to guess the path the camera takes along the spline without visualisation. Movement speed is not yet Update thread independent. (unlikely to be a real issue, but possible. still thanks to Josh for adding `World:GetSpeed()`). Rotation might not work (see https://www.ultraengine.com/community/topic/65628-lua-entitygetrotation-always-returns-vec3-000/ ) Download CameraDolly.zip
-
Tried: GetComponent("Mover") GetComponent(Mover) GetComponent<Mover>() This example also not working https://www.ultraengine.com/learn/Entity_GetComponent?lang=lua require "Components/Motion/Mover" --Get the displays local displays = GetDisplays() --Create a window local window = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[1], WINDOW_CENTER | WINDOW_TITLEBAR) --Create a framebuffer local framebuffer = CreateFramebuffer(window) --Create a world local world = CreateWorld() --Create a camera local camera = CreateCamera(world) camera:SetClearColor(0.125) camera:SetPosition(0, 0, -2) --Create a light local light = CreateBoxLight(world) light:SetRotation(45, 35, 0) light:SetColor(2) light:SetRange(-5, 5) --Create a model local box = CreateBox(world) box:SetColor(0,0,1) --Add a component for automatic motion local component = box:AddComponent(Mover) component.rotationspeed.y = -2 box.mover.rotationspeed.x = 2 local componentTest = box:GetComponent("Mover") if (componentTest ~= nil) then Print("Success") end while window:KeyDown(KEY_ESCAPE) == false and window:Closed() == false do --Update the world world:Update() --Render the world to the framebuffer world:Render(framebuffer) end
-
1. New game 2. Esc to call menu 3. Press Main menu 4. Continue game 5. If no app is froze yet do paragraph 2 again. Usually it happens at 2nd continue game but often happen at 1st too, Same approach works for me in main C++ project, game.lua globals.lua main.lua
-
Plays a sound continuously or intermittently. AmbientNoise.zip
-
- 2
-
-
This component will copy the rotation of another entity, with adjustable smoothing. MatchRotation.zip
-
Here's a new lib that i just finished that will allow game controller support for Lua fans! EDIT: Source: https://onedrive.live.com/redir?resid=1653f4923c37f970!278&authkey=!ABW8a3PdvzR_8cE&ithint=file%2czip Binaries (dll & so): https://onedrive.live.com/redir?resid=1653f4923c37f970!284&authkey=!AM0p2lTBH1mMSzk&ithint=file%2czip A lua example is provided, you can simply overwrite your existing App.lua with mine on a new test project. * Note: Lua sandbox must be disabled to use libs. Unfortunately i don't have linux installed so i couldn't compile it for that platform and I was able to compile it under Linux but i didn't get the chance to test it as i don't have the sdl2 binary libs. I included the full source should anyone want to re-compile it. Those that don't want to, simply copy LuaGamePad.dll file where your game executable is. You'll need to copy SDL2.DLL there as well, it's included. Tested with: - Logitech Rumblepad - Xbox 360 controller (XInput) Cheers!
-
Hi there team, time no see... I'm still working on the v4.0 Update for my Steam game. And one of the addons inside this update, was the option of drive an Opel Kadett car... And here is my question about... I have develop the mirror effect with a Camera using RenderTarget option on a Texture, and then the script: Script.TexturaProyecta=""--string "Textura" function Script:Start() local tex=Texture:Load("Materials/MisTexturas/Camaras_Texturas/"..self.TexturaProyecta..".tex") self.entity:SetRenderTarget(tex) end This works fine, but i've got a problem with the final image... Is there a way for make MIRRORED the image on the mirror? I've tried mirroring the texture, or using 2 cameras, but nothing works for me. I've got the same problem on another map: Many thanks in advance for your help.
-
Hello team, I'm going crazy with this bull****-idea... Is there any LUA command that allows us to obtain the name of the map we are on? I want to store the map name we are playing in a String variable, and then execute an IF to validate that value so that: - If I am in the map bull****.map a variable has "OK" text. - If I am in the map bullface.map the previous variable has "NotOK" text. (sight) I am unable to get the map name to save it in the global variable. And I don't know why... I have created a script where I enter manually the name... But that script (despite being assigned to a pivot in the map that is executed when the map is loaded) is not able to modify the value of the global variable defined in main.lua I have tried to use the mapfile variable used in main.lua of the project, but still don't know how to use it to get the name of the map we are on... This question may seem simple, but it's really driving me crazy.