Intro
This is a new series following Cosplore3D, a raycaster game to learn 3D graphics. This project is part of 12 Months 12 Projects, a challenge I set myself. I'm at such an early point in this project and it means I have a lot of choice, which can be hard to handle since I've only got a month and have to spend my time wisely. However, I decided to work on enemies as it should give quite a bit of life to the game.
Enemy Struct
As with everything, we need to keep some data about the object we have.
type Enemy struct {
x float64
y float64
images []ebiten.Image
target Player
health uint32
speed float64
}
Obviously, we will need the enemies position. We also don't just want one image, we want a slice of images. We do this so in the future it's easier to implement what DOOM does for its sprites, which is have a separate one in use depending on what angle you are looking at it. For now, we will only use on image. We also give the enemy a target, so that it can move towards the player. Of course, enemies can't die if they don't have health to begin with, and similar to the player it has speed.
Enemies In The Level
Placing enemies should be easy enough. All we need to do is use a code for our enemy, such as 9. When we find that code, we replace the tile with empty space, and create a new enemy there.
if code == 9 {
code = 0
enemies = append(enemies, Enemy{
float64(col)*tileSize + tileSize*0.5,
float64(row)*tileSize + tileSize*0.5,
[]ebiten.Image{g.images["blob1"]},
Player{},
100,
1,
})
}
Nothing too special, and you may notice that we use float64(col|row)*tileSize + tileSize*0.5
. This places the enemy in the center of the tile, not on the corner. We also give the enemy a target of an empty Player
struct, since we don't need to chasing after the actual Player
right now.
Drawing Something On The Screen
Before we use an actual image, we should try to draw something on the screen, so that we get the placement right. And yes, it was worth doing this because it took me a while to get right, there's always something wrong with trigonometry. First, we need to get some variables ready.
dx := e.x - c.x
dy := e.y - c.y
dis := math.Sqrt(math.Pow(dx, 2) + math.Pow(dy, 2))
angle := to_degrees(math.Acos(dx / dis))
We get the difference in x and y between the player (well, the player's Camera
) and enemy. We also get the distance, and the angle this makes.
Now, I know what you're thinking, "math.Acos
only returns angles between 0 and pi", well, that's where we use sine.
if math.Asin(dy/dis) < 0 {
angle = -angle
}
math.Asin
returns angles between pi/2 and -pi/2, so if we get a negative value from it, then we should have a negative angle. Using both functions together gives us an angle in the full 360 degree view (or 2pi if you want).
Now we need to start using this angle against the camera's angle.
angle -= c.angle
bound_angle(&angle)
if angle > c.fov/2.0 && angle < 360-c.fov/2.0 {
return
}
If the enemy is 90 degrees to the player, and the player is facing 90 degrees, it should make sense that the remaining angle is 0. Using subtraction, however, could lead us to end up outside of the range 0 to 360, so we use bound_angle
to keep it in there. Lastly, we can get out of the function (effectively not drawing the enemy) if it's not in our field of view. Surprisingly, this if statement didn't take me too long to figure out compared to a lot of the other code.
Now we just have to place and draw the line.
lineX := (angle/(c.fov/2.0))*screenWidth/2.0 + screenWidth/2.0
for lineX > screenWidth {
lineX -= screenWidth
}
ebitenutil.DrawLine(screen, lineX, 0, lineX, screenHeight, color.RGBA{255, 0, 0, 255})
We're drawing a red line from the top of the screen to the bottom of the screen for now, so all we need to figure out is the line's x position. We just map the angle we've found from being within the field of view, to being on the screen. We use the for loop to make sure we don't have a value such as 7,000, which I was getting for some reason (it's so confusing).
Drawing With An Image
I'm sure you're very excited after you saw that line, now you just wait until we draw an image. First though, we need an image.
Wow, such amazing art, this game is going to be so visually appealing, as if it isn't already.
Anyway, now that we've got this image we need to figure out how to draw it.
ogW, ogH := e.images[0].Size()
sW := float64(ogW) / (dis / tileSize)
sH := float64(ogH) / (dis / tileSize)
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(sW, sH)
op.GeoM.Translate(lineX-(sW*float64(ogW)/2.0), screenHeight/2+sH*float64(ogH))
screen.DrawImage(&e.images[0], op)
First, we need to get the size the image currently is. We then need to figure out to what amount we scale the image, which is depended on distance (look, I know it's meant to use an angle and probably cosine, but I was too sick of trigonometry to do it).
We then scale the image by this new size. We can't just translate the image to lineX
, otherwise it will be on the right side of the line, and we want it in the center. So we do some extra manipulation of all those variables. Also, the blob looked like it was floating when I translated it into the center of the screen, so I made it hang a little lower.
That just looks absolutely beautiful. It scales as we move back and forth, and we can place as many as we want around the map. Now, we can see them through walls which is annoying, but we will (probably) fix that later.
Next
Look, I know it's hard looking at my programmer art, but you're going to have to hold on because I'm thinking of working on the HUD, which will include a health system and weapons. This should hopefully lead us one step closer to some actual gameplay in the game