DEV Community

Chig Beef
Chig Beef

Posted on

Adding Enemies (Coslore3D Pt:4)


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
Enter fullscreen mode Exit fullscreen mode

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,
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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


if angle > c.fov/2.0 && angle < 360-c.fov/2.0 {
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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).

But here is our result?
A line in a 3D raycasted world (nothing special)

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.

Basic enemy 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)
Enter fullscreen mode Exit fullscreen mode

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.

An enemy in a raycasted world

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.


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

Top comments (0)