Shooting Goblins for Fun and Profit: Part deux |
Every Goblin Gets Got: Shooting to killAllright 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 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 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 class ent { public: std::unordered_map 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 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 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 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 class bullet { public: std::unordered_map as you can see, when render() is called the switch statement then calls the apropriate updatePos() function: updatePos() for the regular pullets we've 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 |
MaxCodes.info |
| ||||
| MaxCodes.info
| |