-
Notifications
You must be signed in to change notification settings - Fork 0
Part 6 Populating the map
What we have created thus far almost looks like a game. The player can move around a procedurally generated map, and can only see as far as the weak light from their torch shines. But, our caves aren't yet very scary, because we know theres nothing inhabiting them. Lets change that!
In this article, we're going to add randomly generated and placed monsters to our game map, as well as give them a basic AI so they move around a bit. We'll want to be able to define leveled lists of monsters, and add them to the game easily as the player interacts with the game world. Lets start by defining some monsters.
One approach to adding monsters to our world would look very much like how we defined our player entity: we create a new entity, and manually create and attach components to it. That might look something like:
orcPosition := PositionComponent{5, 5}
orcAppearance := AppearanceComponent{
Glyph: ui.NewGlyph("o", "green", "gray"),
Layer: 1,
Name: "Orc",
Description: "A mean looking orc",
}
orcMovement := MovementComponent{}
orcBlocks := BlockingComponent{}
orc = ecsController.CreateEntity([]ecs.Component{orcPosition, orcAppearance, orcMovement, orcBlocks})
That would certainly create a single Orc in our game world, and we could then set its position to add it. If we wanted to add many orcs, we could just do the above code in a loop.
But, what if we want to add a large variety of monsters to our game? 10 or 15 different types, rats, orcs, goblins, bats, dragons, ghosts, zombies, etc? Well, the above approach would certainly work, but hopefully you can see that after a while, this would end up cluttering our code. Thankfully, Gogue has a cleaner way of defining lists of enemies, and building them into the game on demand.
Gogue comes with two mechanisms to help solve the above problem. First, the data loader class, which lets us define all of our game data in JSON files, and load them into data structures that we can then easily access whenever we need. When I say game data, I mean anything from lists of potential enemies, to map types, to items, to player abilities. If something can be defined as a Go map, it can be defined and loaded via a Gogue data loader.
The second mechanism is the entity loader. This takes the data from the data loader, and creates entities from it for use in our game. Using these two in concert, we can much more cleanly and easily define things that populate our game, and how our game acts. Lets jump into some code to see how these work.
First, lets start by defining a list of enemies we want in our game. I'm going to create a new folder in my game root called gamedata
, and inside that folder, I'm going to create a file called enemies.json
. This is just a standard JSON data file:
{
"level_1": {
"small_rat": {
"components": {
"position": {},
"appearance": {
"Name": "Small rat",
"Description": "A very small rat. It doesn't look very tough.",
"Glyph": {
"Char": "r",
"Color": "#8B4513"
},
"Layer": 1
},
"blocking": {},
"simple_ai": {}
}
},
"large_rat": {
"components": {
"position": {},
"appearance": {
"Name": "Large rat",
"Description": "A very large rat. It's big...but still doesn't look too tough.",
"Glyph": {
"Char": "R",
"Color": "#D2691E"
},
"Layer": 1
},
"blocking": {},
"simple_ai": {}
}
},
"cave_bat": {
"components": {
"position": {},
"appearance": {
"Name": "Cave bat",
"Description": "A small, gray, bat. It's fast, bot not a threat. By itself.",
"Glyph": {
"Char": "b",
"Color": "gray"
},
"Layer": 1
},
"blocking": {},
"simple_ai": {}
}
}
},
"level_2": {
"pissed_rat": {
"components": {
"position": {},
"appearance": {
"Name": "Small, pissed off, rat",
"Description": "A very small rat. It looks really pissed off.",
"Glyph": {
"Char": "r",
"Color": "red"
},
"Layer": 1
},
"blocking": {},
"simple_ai": {}
}
},
"kobold": {
"components": {
"position": {},
"appearance": {
"Name": "Kobold",
"Description": "A small humanoid creature, it is clothed in rags, and is carrying a rock. Looks like it knows how to use it, too.",
"Glyph": {
"Char": "k",
"Color": "brown"
},
"Layer": 1
},
"blocking": {},
"simple_ai": {}
}
}
}
}
Hopefully, there are no surprises in the syntax here, its just standard JSON. But, lets walk through whats going on here. First, we've defined a dictionary of dictionaries. The top level keys indicate the general level of the enemy, level_1
and level_2
(these could also indicate what level of the dungeon these enemies appear on). These indicators are just for organizational purposes. Next, inside the level_1
dictionary, I've defined three enemies, a rat, a large rat, and a bat. Now, within each one of these is the interesting bit: each enemy has a components
child dictionary, and in there, we've defined a bunch of components that should look familiar.
Remember that a component is just a container for data, or a flag, so in defining them outside of our game engine, we should treat them exactly the same. Lets look at the rat enemy. We say that it should be created with a PositionComponent
, as well as an AppearanceComponent
, BlockingComponent
, and a new component called simple_ai
(we'll go over that in a bit). For each component, we set the values it needs to apply to our game. The AppearanceComponent
requires a name, description, and a glyph (which itself requires a character and color). We could also set the X and Y properties of the PositionComponent
if we knew where we wanted this rat to show up, but we'll be setting that dynamically later, so leaving it blank for now is fine. BlockingComponent
is just a flag, as is simple_ai
, so we don't need to set any properties for those.
You may notice that I'm referring to the components here by their class names, but in the JSON file, I'm using a shorter name. If you recall, anytime we've added a new component, we've added it to our registerComponents
method, using an ECSController method called MapComponentClass
. The whole point of this registration is to give us an easy way to refer to our components in our data files. PositionComponent
is mapped as position
, AppearanceComponent
is mapped as appearance
etc. This enables Gogue to know which components we are referring to, without us having to use their full names.
Okay, so we've got a enemies data file, lets load it into Gogue. We'll start by defining some variables in our main.go file:
var (
.
.
.
// Data Loading
dataLoader *data.FileLoader
entityLoader *data.EntityLoader
enemies map[string]interface{}
)
We've created a dataLoader, an entityLoader, and defined an enemies list (which will keep track of our loaded enemies). Lets instantiate our two loaders in our init
function:
func init() {
.
.
.
// Data loading - load data from relevant data definition files
dataLoader, _ = data.NewFileLoader("gamedata")
entityLoader = data.NewEntityLoader(ecsController)
loadData()
}
We instantiate our data loader with the location of our game data, in this case just the gamedata
folder we created earlier. We anticipate that all our gamedata will live here. Next we create our entity loader, and pass it our ecsController. Finally, we call load_data
, which looks like this:
// loadData loads game data (enemies definitions, item definitions, map progression data, etc) via Gogues data file and
// entity loader. Any entities loaded are stored in string indexed maps, making it easy to pull out and create an entity
// via its defined name
func loadData() {
enemies, _ = dataLoader.LoadDataFromFile("enemies.json")
}
All this function does (for now), is call our data loaders LoadFromFile
method, with the name of our enemies file. This will marshal the JSON data into a Go data structure (in this case a map[str]interface{}). We can now access the enemy data we defined. Lets add a new function to place some random enemies from our level_1
list:
// placeEnemies places a handful of enemies at random around the level
func placeEnemies(numEnemies int) {
for i := 0; i < numEnemies; i++ {
var entityKeys []string
// Build an index for each entity, so we can randomly choose one
for key := range enemies["level_1"].(map[string]interface{}) {
entityKeys = append(entityKeys, key)
}
// Now, randomly pick an entity
entityKey := entityKeys[rng.Range(0, len(entityKeys))]
entity := enemies["level_1"].(map[string]interface{})[entityKey].(map[string]interface{})
// Create the entity based off the chosen item
loadedEntity := entityLoader.CreateSingleEntity(entity)
randomTile := gameMap.FloorTiles[rng.Range(0, len(gameMap.FloorTiles))]
ecsController.UpdateComponent(loadedEntity, PositionComponent{}.TypeOf(), PositionComponent{X: randomTile.X, Y: randomTile.Y})
}
}
The first part of this method is pretty straight forward: we pass it how many enemies we want to create, and then we iterate that many times, picking a random enemy to create on each pass (we do this by creating a list of the enemy keys, and choosing one at random). We could also randomize what level they chosen from, but for now, we're sticking with level_1
enemies.
Next, we take the map representing our chosen enemy, and pass it to our entity loader, via CreateSingleEntity
. This method parses the Go map representing the enemy, and creates a new entity based on what it finds, with all the components we defined attached, metadata and all. Its a bit of a black box, and we're going to keep it that way for now. All you need to know is that it returns the entity ID of the newly created entity. With that, we can use our ecsController to interact with it in the same way we can the player.
Finally, we select a random location from our floor tiles on on our game map, and set our new entities position accordingly.
And just like that, we have a bunch of new entities inhabiting our caves!
But wait, if you try to run this right now, you'll get an error. Something about a nil pointer reference or some nonsense like that. We forgot to do one thing: remember that simple_ai
component? Well, it doesn't exist yet, and the entity loader doesn't know what to do when it encounters a non-existent component. Lets go ahead and define that now in components.go:
// SimpeAIComponent is a basic AI. The entity will move randomy around the map.
type SimpleAiComponent struct {}
func (sc SimpleAiComponent) TypeOf() reflect.Type { return reflect.TypeOf(sc) }
This is just a flag component. Nothing new here. Lets register it as simple_ai
so the entity loader knows about it. Back in main.go, in our registerComponents
function:
ecsController.MapComponentClass("simple_ai", SimpleAiComponent{})
One more change we need to make, before we implement the actual "AI" is to ensure that they player cannot see entities that are not in their line of sight. We've previously only had one entity on screen, so this wasn't an issue, but if you ran your game right now, you would notice that there are random characters floating in the blackness outside your range of vision. Lets fix that by modifying SystemRender
s process
method:
func (sr SystemRender) Process() {
// Render all entities present in the global entity controller
for e := range sr.ecsController.GetEntities() {
if sr.ecsController.HasComponent(e, PositionComponent{}.TypeOf()) && sr.ecsController.HasComponent(e, AppearanceComponent{}.TypeOf()) {
pos := sr.ecsController.GetComponent(e, PositionComponent{}.TypeOf()).(PositionComponent)
appearance := sr.ecsController.GetComponent(e, AppearanceComponent{}.TypeOf()).(AppearanceComponent)
// Get the coordinates of the entity, in reference to where the camera currently is.
cameraX, cameraY := gameCamera.ToCameraCoordinates(pos.X, pos.Y)
if gameMap.IsVisibleToPlayer(pos.X, pos.Y) {
// Clear the cell this entity occupies, so it is the only glyph drawn there
for i := 1; i <= 2; i++ {
ui.ClearArea(cameraX, cameraY, 1, 1, i)
}
ui.PrintGlyph(cameraX, cameraY, appearance.Glyph, "", appearance.Layer)
}
}
}
}
All we've done here is add a call to a built in method of our game map, IsVisibleToPlayer
, which, given the players position, will return true if the entity being rendered is within the players field of view. If not, it simply doesn't get drawn. That should fix our floating entity issue.
Finally, lets implement a system to process any entities with the simple_ai
component attached. In systems.go, add a new system:
type SystemSimpleAi struct {
ecsController *ecs.Controller
mapSurface *gamemap.GameMap
}
func (sas SystemSimpleAi) Process() {
// Process all entities that have the simple AI component attached to them
// For now, just have them print something
for _, entity := range sas.ecsController.GetEntitiesWithComponent(SimpleAiComponent{}.TypeOf()) {
//Act
if sas.ecsController.HasComponent(entity, AppearanceComponent{}.TypeOf()) {
// For the time being, just have the AI move around randomly. This will be fleshed out in time.
pos := sas.ecsController.GetComponent(entity, PositionComponent{}.TypeOf()).(PositionComponent)
dx := rng.RangeNegative(-1, 1)
dy := rng.RangeNegative(-1, 1)
var newX, newY int
if !sas.mapSurface.IsBlocked(pos.X + dx, pos.Y + dy) && !GetBlockingEntities(pos.X + dx, pos.Y + dy, sas.ecsController){
newX = dx + pos.X
newY = dy + pos.Y
} else {
newX = pos.X
newY = pos.Y
}
cameraX, cameraY := gameCamera.ToCameraCoordinates(pos.X, pos.Y)
ui.PrintGlyph(cameraX, cameraY, ui.EmptyGlyph, "", 1)
newPos := PositionComponent{X: newX, Y: newY}
sas.ecsController.UpdateComponent(entity, PositionComponent{}.TypeOf(), newPos)
}
}
}
There really isn't anything new here. We check every entity in the ecsController, and if we find one that has the simple_ai
component, we randomly move it, checking to make sure its location isn't blocked. Thats it. Nothing special, but this will add some life to our cavern. We'll need to register this system in main.go, registerSystems
:
func registerSystems() {
render := SystemRender{ecsController: ecsController}
input := SystemInput{ecsController: ecsController}
simpleAi := SystemSimpleAi{ecsController: ecsController, mapSurface: gameMap}
ecsController.AddSystem(input, 0)
ecsController.AddSystem(simpleAi, 1)
ecsController.AddSystem(render, 2)
}
We want our AI to run before we render anything (even though we're manually processing rendering, its good to keep things ordered properly).
Finally, lets tie it all together, by calling a few methods in our main game loop:
func main() {
// Register all screens
registerScreens()
// Initialize the game map
gameMap = SetupGameMap()
placeEnemies(100)
registerSystems()
excludedSystems := []reflect.Type{reflect.TypeOf(SystemRender{})}
// Set the current screen to the title
_ = screenManager.SetScreenByName("title")
screenManager.CurrentScreen.Render()
ui.Refresh()
for {
// First up, clear everything off the screen
for i := 0; i <= 2; i++ {
ui.ClearWindow(windowWidth, windowHeight, i)
}
// Process the players Field of Vision. This will dictate what they can and cant see each turn.
playerPosition := ecsController.GetComponent(player, PositionComponent{}.TypeOf()).(PositionComponent)
playerFOV.SetAllInvisible(gameMap)
playerFOV.RayCast(playerPosition.X, playerPosition.Y, gameMap)
// Check if the current screen requires the ECS. If so, process all systems. If not, handle any input the screen
// requires, and do not process and ECS systems
if screenManager.CurrentScreen.UseEcs() {
ecsController.Process(excludedSystems)
} else {
screenManager.CurrentScreen.HandleInput()
}
screenManager.CurrentScreen.Render()
ecsController.ProcessSystem(reflect.TypeOf(SystemRender{}))
ui.Refresh()
}
ui.CloseConsole()
}
We've shifted a few things around, namely when we generate our map. But, after that, we call our new placeEnemies
function, which will populate our map with some randomly moving baddies to shuffle around!
The code for this part of the tutorial can be found here, tagged as v1.0.0-tutorial6