MaxCodes.info - Basic Shooting Mechanics

Shooting Goblins for Fun and Profit: Part deux

Every Goblin Gets Got: Shooting to kill


       Allright everyone, welcome to pt. 2 of my basic shooting mechanics series. I realized when i finished part one that some of you might have felt
a little short-changed. What good is being able to shoot/throw spears/spit fireballs/what-ever if whatever your shooting at shrugs it off like dragon eating hot wings? So heres where the
the rubber hits the road, or in the case where the bullet meets the goblin. To follow along head over to my GitHub Repo for this project and grab
the source code. The reason i recommend getting the code before reading through the rest of this article is that in order to developed the rest of these concepts beyond just launching projectiles
especially to launch them at an opponent and have it respond in kind, you need practically the basic frame work for an entire game. Things like gamestate, pathfinding, a basic A.I. and game engine
are required. I've covered these topics in other articles, particularly the Making Goblins Move series and the BearLib RogueLike tutorial, and which are beyond the scope
of this series. If you haven't read those yet, or if your still shakey on the concepts, i recommend giving them a review. With that bein said lets begin.


       In part one we put together the pieces to be able to bust a cap at some Goblin ass, now were going to splatter them all over the wall. Coincidentally we're going
to pick up in the same function we left off at last time: the bulletStatus() function, though it will look a little different now having been incorporated into the game engine.
This is what it looked like at the end of Part one:

                        std::vector bulletStatus(std::vector rnds, World* Map)
                        {
                            if (rnds.size() > 0)
                            {
                                int count = 0;
                                for (auto p: rnds)
                                {
                                    p->render(Map);
                                    if (!p->go)
                                    {
                                        rnds.pop_back();
                                    }
                                }
                            }
                            return rnds;
                        }
                    

Its undergone some changes having been folded into the engine, one bonus being that we're not passing and returning objects anymore, as all of the objects are members of the engine class now:
                        void engine::bulletStatus()
                        {
                            int count = 0, cnt2 = 0;
                            for (auto p: caps)
                            {
                                p->render(bf->map);
                                if (!p->go)
                                {
                                   if (p->killshot)
                                      checkDead(p->pos) 
                                   caps.erase(caps.begin()+count);
                                }
                                count++;
                            }
                        }

                        void engine::checkDead(Point impact)
                        {
                            int count = 0;
                            for (auto g : gobs)
                            {
                                if (impact == g->pos) //marked for death kid.
                                {
                                    g->die(bf->map);
                                    gobs.erase(gobs.begin()+count);
                                }
                                count++;
                            }
                        }
                    

As you can see we've also added the call to a new function, checkDead(). In the part one we setup the killshot bool and the impact point to keep track of when a bullet made contact
With a target, and saving the position of that target. The checkDead() function takes Point impact as its input, and then iterates over the vector of goblins that the game engine uses for
holding said goblins. It compares the All of the goblins current position Point, and if that matches Point impact, it means that that is the unlucky goblin whos been shot.
From there we call a function in the ent class, die(), and remove the goblin from the vector, taking them out of the game.

                    void ent::die(World* map)
                    {
                        map->layout[pos.x][pos.y].populated = false;
                        map->layout[pos.x][pos.y].blocks = false;
                        terminal_color("red");
                        terminal_print(pos.x,pos.y-1, ".");
                        terminal_print(pos.x-2,pos.y,".:X:.");
                        terminal_print(pos.x,pos.y+1,".");
                    }
                

The first two lines do a little housekeeping to prevent the "ghost" of the goblin from obstructing the map. The otherlines make a little splatter effect when they get shot. So now we have the
full effect, we can shoot projectiles and when they hit a target the target reacts appropriatly, we've shown the concept works, what can we do now to enhance the playing experience?
We've actually just opened our selves up to a plethora of game enriching possibilities. The first thing we could do is put a limit on the number of bullets our player has. To do this we add a shot counter
to our ent class, and assign the player a set number of bullets. We can do this two ways: we can set the counter to the number of bullets we're going to give the player, and allow them to shoot until the
counter decrements to zero, or we could have to variables, one for shots fired, and the other being MAXSHOTS. Essentially, one method is counting down, the others counting up, the choice is yours.
For now lets go with the first option. We add bullets_left to our ent class, and assign its value in the constructor:

                    class ent {
                        public:
                          std::unordered_mapfacing;
                          int bullets_remaining;
                          Point pos;
                          char face;
                          char ch;
                          int id;
                          void die(World* map);
                          bool canMove(World* Map, int x, int y);
                          void move(World*, int x, int y);
                          void render();
                          void turn(bool dir);
                          void dijk_step(World* map);
                          bullet* shoot();
                          ent(int x, int y, int id, char ch);
                          ent();
                        };

                        ent::ent(int x, int y, int id, char ch)
                        {
                            facing['N'] = {0,-1};
                            facing['S'] = {0, 1};
                            facing['E'] = {1,0};
                            facing['W'] = {-1,0}; 
                            this->pos = {x,y};
                            this->ch = ch;
                            this->id = id;
                            this->face = 'N';
                            bullets_remaining = 25;
                        }
                

And then we change the shoot() function to only shoot while we have bullets left:


                    bullet* ent::shoot()
                    {
                        if (bullets_remaining > 0)
                        {
                            return new bullet(face, pos);
                        }
                    }
                

You could make the game even more challenging by say, assigning one less bullet than there are Goblins, or instead of having one shot one kill we could have the bullets
slow the goblin the down, or otherwise injure them so that it requires more than one shoot to put a Goblin down. There are litteraly dozens, if not hundreds of things we
could build off of this feature to expand game play. I wont go into the intricacies of an Item Component System as they can get fairly complex, but one thing you could do
is have magazines of ammo laying around as items on the map for your character to find and reload with.

You could also modify your game loop to do something like this:

                    //---------begin gameloop code slice------
                    if (terminal_has_input())
                    {
                        if (state)
                        {
                            keypress=terminal_read();
                            keylog.push_back(keypress);
                            switch (keypress)
                            {
                                case TK_UP: me->move(Map,0,-1); break;
                                case TK_DOWN: me->move(Map,0,1); break;
                                case TK_LEFT: me->move(Map,-1,0); break;
                                case TK_RIGHT: me->move(Map,1,0); break;
                                case TK_SPACE: caps.push_back(me->shoot()); break;
                                case TK_Q: terminal_close(); exit(0); break;
                                default: break;
                            }
                            bf->setMapValue(me->pos, 200);
                            check_keys(keylog);
                            state = false;
                            }
                    }
                    //-------------end gameloop code slice--------
                        void engine::check(std::vector keylog)
                        {
                          int i, p = 2;
                          bool maybe = false;
                          int sauce[] = { 82, 82, 81, 81, 80, 79, 80, 79, 4, 5, 44};
                          if (keylog.size() > 10)
                          {
                                 for (i = 0; i <= 11; i++)
                                 {
                                      if (keylog[i] == sauce[i])
                                      {
                                          maybe = true;
                                      } else {
                                          maybe = false;
                                      }
                                      
                                  }
                              }
                          
                          if (maybe == true) {
                              bullets_remaining = 10000;                            
                          }
              
                      }
                

I'll let you figure out what you just read on your own. If you're into video games it shouldnt be terribly difficult.


Goodness, Gracious, Great Balls of Fire!

So far we've implemented your run of the mill projectile that can be used as shooting a gun, launching an arrow, or maybe even firing off a lazer or a missile. Thats all well and good for mere mortals, but what our character happens to be an all powerful wizard with a penchant for killing Goblins around corners with balls of fire?

If you've taken a peek at the GitHub repo for this project, i'm sure you've noticed that the game engine
If you've been following with my articles, i'm sure you've picked up on a few tidbits about me personally. One) i mention Goblins entirely too often, and Two) I freaking LOVE the
breadth first search algorithm. This algorithm is the freaking swiss army knife of the grid based game world. Afterall, a grid is just a spcialized form of graph, with our Points
being Vertexes the border between them being Edges. I'm going to show you to neat tricks that work very similarly to each other, each using breadth first search
but in their own unique way.

It's upgrade time! Since were going to have our Bullet class have different types of projectiles, we need to have a way of specifying which kind of projectile it is that we
need. I added a char value to the Bullet class, and named it bullet_kind, i then added a parameter to the bullet constructor so when its instantiated the type of bullet is assigned to it.
and being that our render() function is what calls updatePos, i added a switch statement for determining how to manage our bullet during its life time. Through careful design we're able to
to keep the bulletStatus() function unchanged.
With the aforementioned changes applied, they now look like this:

                    class bullet {
                        public:
                          std::unordered_map facing;
                          std::vector heatSeeking;
                          char d;
                          const char ch = '*';
                          const char ch_M = '%';
                          Point pos;
                          Point impact;
                          char bullet_kind;
                          bool killshot;
                          bool go;
                          bool running;
                          void updatePosMagic(World* Map);
                          void updatePos(World* Map);
                          void render(World* Map);
                          inline bool inBounds(Point);
                          bullet(char dir, Point spos, char kind);
                          ~bullet();
                      };
                      
                      bullet::bullet(char dir, Point spos, char kind) {
                        this->facing['N'] = {0,-1};
                        this->facing['S'] = {0, 1};
                        this->facing['E'] = {1,0};
                        this->facing['W'] = {-1,0}; 
                        this->d = dir;
                        this->go = true;
                        this->killshot = false;
                        this->pos = spos;
                        this->bullet_kind = kind;
                        this->running = false;
                      }    
                      void bullet::render(World* Map)
                      {
                        if (go == true)
                        {
                            switch (bullet_kind)
                            {
                                case 'r':
                                    updatePos(Map);
                                    terminal_layer(4);
                                    terminal_color("cyan");
                                    terminal_put(pos.x,pos.y, ch);
                                    break;
                                case 'M':
                                    updatePosMagic(Map);
                                    terminal_layer(4);
                                    terminal_color("flame");
                                    terminal_put(pos.x,pos.y, ch_M);
                                    break;
                                }
                            }
                        }
                

as you can see, when render() is called the switch statement then calls the apropriate updatePos() function: updatePos() for the regular pullets we've
already implemented, and updatePosMagic() for the fireballs were implementing now. As you can see, we've also assigned the fireballs their own symbol, "%".
updatePos()'s only requirements were to keep the bullet traveling in a straight line until it hit a target, hit a wall, or went off screen. updatePosMagic()
is a whole different story. Lets take a peek at updatePosMagic():

            void bullet::updatePosMagic(World* map)
            {
              if (heatSeeking.empty())
              {
                mctk::breadthFirst seek(map);
                heatSeeking = seek.pathFind(pos);
                go = true;
              }
              if (!heatSeeking.empty())
              {
                pos = heatSeeking.back();
                heatSeeking.pop_back();
                terminal_put(pos.x,pos.y,ch_M);
                terminal_refresh();
                if (map->layout[pos.x][pos.y].populated)
                {
                    killshot = true;
                    go = false;
                    impact = pos;
                }
              }
              if (heatSeeking.empty()&&go==true)
              {
                go = false;
              }
            }
           
It's a neat little trick actually. What's going on here, is that when updatePosMagic() is called for the very first time it calls an a pathfinding
algorithm. I used a basic breadth first search pathfinder like i covered in Making Goblins Move Part Two And Making Goblins Move Part Three.
Since i'm using Dijkstra Mapping for goblin movement, my first thought was to use the Dijkstra Map for guiding the fireballs to the goblins, it would have been perfect!
Unfortunatly, having all the values decrement towards one location results in values incrementing in EVERY DIRECTION. You could throw a fire ball in front of you and can whip around behind you.
Of course, this could have been fixed by generating seperate Dijkstra Maps: one leading the Goblins towards the player, and one leading the fireballs towards the goblins. It turns out that
using a seperate pathfinding algorithm to creat a one to one path is a better approach, because unlike the Dijkstra Map, it doesnt need to visit ever single node.
The pathfinder is implemented with an "early exit" that triggers when the search encounters the first target (Goblin) it runs comes across. It then return a path in
the form of a vector of points. The bool "running" is negative the first time updateMagicPos() is called, however once the path has been plotted running is switched to true
in this way we avoid calling a search and fetching a path every time the function is called. With running set to true, the algorithm switches gears, now its pop'ing Points off the end
of the vector, and using that Point as its next position. And just like that, we can launch fireballs at targets around a corner, down an alley, a couple rooms over, where it is
that our search reaches when it encounters its first Goblin. The only other changes are another keypress command, and a slight modification to our ent's shoot() function:
               /************game loop code snippet*******/
               switch (keypress)
                {
                    case TK_UP: me->move(Map,0,-1); break;
                    case TK_DOWN: me->move(Map,0,1); break;
                    case TK_LEFT: me->move(Map,-1,0); break;
                    case TK_RIGHT: me->move(Map,1,0); break;
                    case TK_SPACE: caps.push_back(me->shoot('r')); break;
                    case TK_TAB: caps.push_back(me->shoot('M')); break;
                    case TK_1: caps.push_back(me->shoot('G')); break;
                    case TK_Q: terminal_close(); exit(0); break;
                    default: break;
                
                }
                /************end snippet**************/
                bullet* ent::shoot(char t)
                {
                    return new bullet(face, pos, t);
                }
           

Well, thats there is to it for now, I hope you enjoyed reading this and can implement in your future projects. As always, a full working example of
the code shown here is available on my GitHub Page And feel free to leave a comment below! -max


(c) 2020 Max Goren
MaxCodes.info


(c) 2020 Max Goren
MaxCodes.info