Skip to main content

Bots

Open this Example

Bots example

How can I make a bot that shoots projectiles at the nearest player?

This is an example of a little multiplayer pirate ship game, where the game can be played against humans or bots.

Bots vs Mobs

Before you start, make sure you understand the difference between a bot and a mob:

  • A bot is a hero controlled by a computer, created using the SpawnBot function. The hero could have been controlled by a human player, but the computer is controlling it instead. The bot normally controls its hero by simulating inputs (button clicks, mouse movement, etc.).

  • A mob is an enemy controlled by the game. It can never be controlled by a human player. In fact, it is not a player at all, it is just an entity in the game world.

It is much more complex to implement a bot than a mob, so make sure that a bot is really what you want to implement, and not a mob. See Mobs for a simpler way to make mobs that shoot at players.

Step 1: Spawn some bots

First, use SpawnBot to spawn some bots in your game world. You can do this in your Main function.

pub const BotNames = ["Rocko", "Jack", "Henry", "Abigail", "Ringarde"]
pub game fn World.Main() {
// ...

for botName in BotNames {
SpawnBot(botName)
}
}

In this example we've given our bots some names, but that is optional.

tip

In a real game you might want to wait until the player clicks a Play vs AI button before spawning the bots, but for this example we will just spawn them immediately.

Step 2: Give a BotSystem to your hero

When a Hero is under the control of a bot, it needs to have a BotSystem to simulate inputs for it. We will define what a BotSystem does in the next step.

pub fn unit.Hero([owner]) {
// ...

if !IsHuman {
BotSystem
}
}

Step 3: Define the BotSystem

A simple BotSystem simply runs every few ticks, finds the nearest player, and simulates a mouse click to shoot at them. We're also making it pick a random target to move towards every few ticks, so that it doesn't just stand still.

fn unit.BotSystem([owner]) {
use body=unit

on Tick(3s * Random) {
let enemy = QueryNearest(filter=Category:Hero, against=Alliance:Enemy)
if !enemy { continue }
Pointer = enemy.Pos
ButtonDown(Click)
ButtonUp(Click)
}

with Tick(3s * Random) {
let target = @(SignedRandom*BoundaryHalfWidth, SignedRandom*BoundaryHalfHeight)
Joystick = (target - Pos).Direction
await Tick(2s)
Joystick = @(0, 0)
}
}
  • QueryNearest is used to find the nearest enemy player. As the nearest Hero is always going to be itself, notice how we use against=Alliance:Enemy to only find other heroes.
  • Pointer is used to aim at the enemy player's position.
  • ButtonDown(Click) and ButtonUp(Click) simulate a mouse click to shoot at the enemy. Both the ButtonDown and ButtonUp are needed to simulate a full click, without ButtonUp, the bot would just hold down the mouse button and it would never shoot again.

Full Code Listing

pub tangible category Category:Hero
pub tangible category Category:Projectile

pub const BoundaryHalfWidth = 16
pub const BoundaryHalfHeight = 9

pub const BotNames = ["Rocko", "Jack", "Henry", "Abigail", "Ringarde"]

pub game fn World.Main(maxHumanPlayers=6) {
Camera(@(0, 0), radius=@(BoundaryHalfWidth, BoundaryHalfHeight))
ImageSprite(body=@(0,0), image=@water.png, radius=3, repeatX=100, repeatY=100, layer=-100)

BottomContent {
InsetPanel {
"Arrow keys to move, click to fire"
}
}

SpawnEachPlayer owner {
Spawn unit {
Hero
}
}

for botName in BotNames {
SpawnBot(botName)
}
}

pub fn unit.Hero([owner]) {
use body=this, radius=0.5, extent=0.5, shape=Capsule(radius=, extent=)
Body(pos=@(SignedRandom*BoundaryHalfWidth, SignedRandom*BoundaryHalfHeight))
ImageSprite(image=PickRandom(@ship-*.png), radius=(radius+extent), angleOffset=-0.25rev)
PolygonCollider(category=Category:Hero)
TextSprite(PlayerName, color=#3338, radius=0.25, screenOffset=@(0, 0.1 + radius), vAlign=VAlign:Top, noRotation=true)
DecaySpeed(0.01)
DecayTurnRate(0.01)

on BeforePhysics {
if Joystick == @(0, 0) { continue }

if Velocity.Dot(Joystick.Direction) < 5 {
Velocity += 0.1 * Joystick
Heading = Velocity.Angle
}
}

on ButtonDown(Click) {
Spawn projectile {
Cannonball(heading=Angle(Pointer - Pos), parent=unit)
}
await Tick(0.5s)
}

if !IsHuman {
BotSystem
}
}

fn unit.BotSystem([owner]) {
use body=unit
on Tick(3s * Random) {
let enemy = QueryNearest(filter=Category:Hero, against=Alliance:Enemy)
if !enemy { continue }

Pointer = enemy.Pos
ButtonDown(Click)
ButtonUp(Click)
}

with Tick(3s * Random) {
let target = @(SignedRandom*BoundaryHalfWidth, SignedRandom*BoundaryHalfHeight)
Joystick = (target - Pos).Direction
await Tick(2s)
Joystick = @(0, 0)
}
}

pub fn projectile.Cannonball(heading, parent, [owner]) {
use body=this, radius=0.125, shape=Circle, color=#555, layer=10
Body(pos=parent.Pos, velocity=7*heading.Direction)
PolygonSprite
PolygonCollider(category=Category:Projectile, parent=)
DecaySpeed(0.005)

on Paint {
Streak(color=#fff, fade=1, dissipate=1s, layer=layer-1)
}

once Tick(1.5s) { Expire }
on BeforeCollide that { Expire }
once BeforeDespawn {
repeat 5 { Spark(splatter=1, dissipate=0.25s) }
}
}
Open in Editor