Getting started¶
In this tutorial, we’ll start with a project template and create an enemy that we can kick around and get chased by.
Kicking a kobold
The following sections will expand this into a complete game.
Installation¶
Follow the installation guide to install LÖVE and set up the project template.
Creating an enemy¶
To make the game more engaging, let’s introduce an enemy: the Kobold.
Navigate to the
/modules/MyGame/actors/
directory.Create a new file named
kobold.lua
.Add the following code to define the Kobold actor:
prism.registerActor("Kobold", function()
return prism.Actor.fromComponents {
prism.components.Position(),
prism.components.Drawable("k", prism.Color4.RED),
}
end)
Note
See Object registration for an overview on how to load objects in prism.
Let’s run the game again, and press ~
. This opens Geometer, the editor.
Click on the k on the right hand side and use the pen tool to draw a
kobold in. Press the green button to resume the game.
You might notice that you can walk right through the kobold. We fix that by giving it a
Collider
:
prism.components.Collider()
Note
See Collision for more information on the collision system.
If we restart the game and spawn in another kobold, we shouldn’t be able to walk
through kobolds anymore. We’re also going to give the kobold a few more core components: a
Senses
, SightComponent
, and MoverComponent
, so it can see and move:
prism.components.Senses(),
prism.components.Sight{ range = 12, fov = true },
prism.components.Mover{ "walk" }
The kobold controller¶
Now that the kobold exists in the world, you might notice something—it’s
not moving! To give it behavior, we need to implement a Controller
.
A Controller
(or one of its derivatives) defines the Controller:act()
function, which takes the Level
and the Actor
as arguments and
returns a valid action.
Caution
The act
function should not modify the level directly–it should only use it to validate actions.
Navigate to
modules/MyGame/components/
.Create a new file named
koboldcontroller.lua
.Add the following code:
--- @class KoboldController : Controller
--- @overload fun(): KoboldController
local KoboldController = prism.components.Controller:extend("KoboldController")
KoboldController.name = "KoboldController"
function KoboldController:act(level, actor)
local destination = actor:getPosition() + prism.Vector2.RIGHT
local move = prism.actions.Move(actor, destination)
if level:canPerform(move) then
return move
end
return prism.actions.Wait(actor)
end
return KoboldController
Tip
Always provide a default action to take in a controller.
Back in kobold.lua
, give it our new controller component:
prism.components.KoboldController()
Our kobold should move right until they hit a wall now, but this behaviour doesn’t make for a great game. Let’s make them follow the player around.
Complete kobold.lua
prism.registerActor("Kobold", function()
return prism.Actor.fromComponents {
prism.components.Name("Kobold"),
prism.components.Position(),
prism.components.Collider(),
prism.components.Drawable("k", prism.Color4.RED),
prism.components.Senses(),
prism.components.Sight{ range = 12, fov = true },
prism.components.Mover{ "walk" },
prism.components.KoboldController()
}
end)
Pathfinding¶
To make our kobold follow the player, we need to do a few things:
See if the player is within range of the kobold.
Find a valid path to the player.
Move the kobold along that path.
We can find the player by grabbing the Senses
from the kobold and
seeing if it contains the player. We should also ensure the kobold has the component in the first place.
local senses = actor:get(prism.components.Senses)
if not senses then return prism.actions.Wait() end -- we can't see!
local player = senses:query(prism.components.PlayerController):first()
if not player then return prism.actions.Wait() end
Note
See Queries for more information on querying.
We can get a path to the player by using the Level:findPath()
method, passing the
positions and the kobold’s collision mask.
local mover = actor:get(prism.components.Mover)
if not mover then return prism.actions.Wait() end -- we can't move!
local path = level:findPath(actor:getPosition(), player:getPosition(), actor, mover.mask, 1)
Then we check if there’s a path and move the kobold along it, using Path:pop()
to get the first
position.
if path then
local move = prism.actions.Move(actor, path:pop())
if level:canPerform(move) then
return move
end
end
Jump back into the game and you should find kobolds chasing after you.
Complete koboldcontroller.lua
--- @class KoboldController : Controller
--- @overload fun(): KoboldController
local KoboldController = prism.components.Controller:extend("KoboldController")
KoboldController.name = "KoboldController"
function KoboldController:act(level, actor)
local senses = actor:get(prism.components.Senses)
if not senses then return prism.actions.Wait() end -- we can't see!
local player = senses:query(prism.components.PlayerController):first()
if not player then return prism.actions.Wait() end
local mover = actor:get(prism.components.Mover)
if not mover then return prism.actions.Wait() end
local path = level:findPath(actor:getPosition(), player:getPosition(), actor, mover.mask, 1)
if path then
local move = prism.actions.Move(actor, path:pop())
if level:canPerform(move) then
return move
end
end
return prism.actions.Wait(actor)
end
return KoboldController
Kicking kobolds¶
In this section we’ll give you something to do to these kobolds: kick them!
We’ll need to create our first action. Head over to /modules/MyGame/actions
and add kick.lua.
Let’s first create a target for our kick. Put this at the top of kick.lua:
local KickTarget = prism.Target()
:with(prism.components.Collider)
:range(1)
:sensed()
With this target we’re saying you can only kick actors at range one with a collider component. Then we can define the kick action, including our target. We will also require that any actor trying to perform the kick action have a controller.
---@class KickAction : Action
local Kick = prism.Action:extend("KickAction")
Kick.name = "Kick"
Kick.targets = { KickTarget }
Kick.requiredComponents = {
prism.components.Controller
}
return Kick
For the logic, we’ll define methods that validate and perform the kick. We don’t have any
special conditions for kicking, so from Action:canPerform()
we’ll just return true.
For the kick itself, we get the direction from the player to the target (kobold), and check passability
for three tiles in the direction before finally moving them. We also give the kobold flying movement by
checking passability with a custom collision mask.
function Kick:canPerform(level)
return true
end
local mask = prism.Collision.createBitmaskFromMovetypes{ "fly" }
--- @param level Level
--- @param kicked Actor
function Kick:perform(level, kicked)
local direction = (kicked:getPosition() - self.owner:getPosition())
for _ = 1, 3 do
nextpos = kicked:getPosition() + direction
if not level:getCellPassable(nextpos.x, nextpos.y, mask) then break end
if not level:hasActor(kicked) then break end
level:moveActor(kicked, nextpos)
end
end
Complete kick.lua
local KickTarget = prism.Target()
:with(prism.components.Collider)
:range(1)
:sensed()
---@class KickAction : Action
local Kick = prism.Action:extend("KickAction")
Kick.name = "Kick"
Kick.targets = { KickTarget }
Kick.requiredComponents = {
prism.components.Controller
}
function Kick:canPerform(level)
return true
end
--- @param level Level
--- @param kicked Actor
function Kick:perform(level, kicked)
local direction = (kicked:getPosition() - self.owner:getPosition())
local mask = prism.Collision.createBitmaskFromMovetypes{ "fly" }
for _ = 1, 3 do
local nextpos = kicked:getPosition() + direction
if not level:getCellPassable(nextpos.x, nextpos.y, mask) then break end
if not level:hasActor(kicked) then break end
level:moveActor(kicked, nextpos)
end
end
return Kick
Kicking kobolds, for real this time¶
We’ve added the kick action, but we don’t use it anywhere. Let’s fix that by performing the kick
when we bump into a kobold. Head over to gamestates/MyGamelevelstate.lua
and find where the move action
is called. If the player doesn’t move, we want to check if there’s a valid actor to kick in front of us,
and then perform the kick action on them:
if self.level:canPerform(move) then
...
end
local target = self.level:query() -- grab a query object
:at(destination:decompose()) -- restrict the query to the destination
:first() -- grab one of the kickable things, or nil
local kick = prism.actions.Kick(owner, target)
if self.level:canPerform(kick) then
decision:setAction(kick)
end
Note
Level:canPerform()
will validate all targets in the action.
That’s a wrap¶
That’s all for part one. In conclusion, we’ve accomplished the following:
Added a kobold enemy with basic pathfinding.
Implemented a kick action to shove kobolds around.
Integrated the kick by performing it when bumping into a valid target.
You can find the code for this part at https://github.com/prismrl/prism-tutorial on the part1
branch. In the
next section, we’ll do some work with components and systems to flesh out the combat system.