{"id":21007,"date":"2013-09-09T07:50:54","date_gmt":"2013-09-09T12:50:54","guid":{"rendered":"http:\/\/www.shamusyoung.com\/twentysidedtale\/?p=21007"},"modified":"2015-07-01T03:45:22","modified_gmt":"2015-07-01T08:45:22","slug":"project-good-robot-11-when-stuff-collides","status":"publish","type":"post","link":"https:\/\/www.shamusyoung.com\/twentysidedtale\/?p=21007","title":{"rendered":"Project Good Robot 11: When Stuff Collides"},"content":{"rendered":"<p>A running theme of this project is that 2D game development is like programming in Easy Mode. Everything takes less code, requires fewer steps, uses less CPU \/ memory, and has a larger margin for error. It&#8217;s amazing to be able to just make something without constantly getting snagged on annoying tech issues, performance trade-offs, and gameplay compromises.<\/p>\n<p>Take collision detection, for example.<\/p>\n<p>Early in the project I used rectangle-based collision.  If I shot at a bad robot, the game would check for a collision between the bullet and the square region where it was drawing the enemy.<\/p>\n<p><table width='600'  cellpadding='0' cellspacing='0' border='0' align='center'><tr><td><img src='https:\/\/www.shamusyoung.com\/twentysidedtale\/images\/gr11_collision1.jpg' class='insetimage' width='600' alt='Okay, pixels don&#8217;t suffer zooming nearly as gracefully as polygons. Then again, you can fix this by just not letting the player zoom in too far. Good luck keeping the player from looking at the walls too closely in your FPS.' title='Okay, pixels don&#8217;t suffer zooming nearly as gracefully as polygons. Then again, you can fix this by just not letting the player zoom in too far. Good luck keeping the player from looking at the walls too closely in your FPS.'\/><\/td><\/tr><tr><td class='insetcaption'>Okay, pixels don&#8217;t suffer zooming nearly as gracefully as polygons. Then again, you can fix this by just not letting the player zoom in too far. Good luck keeping the player from looking at the walls too closely in your FPS.<\/td><\/tr><\/table><\/p>\n<p>But this is a crappy solution. Basically every enemy is shaped like a box for the purposes of collision. That&#8217;s good enough when shots hit center-mass, but it&#8217;s really unsatisfying to have bullets score a hit when they pass through the (usually empty) corners of the rectangle. Unless I&#8217;m going to make all the foes square, this isn&#8217;t a viable technique. <\/p>\n<p>So I change it to check using distance calculations:<\/p>\n<p><!--more--><table width='600'  cellpadding='0' cellspacing='0' border='0' align='center'><tr><td><img src='https:\/\/www.shamusyoung.com\/twentysidedtale\/images\/gr11_collision2.jpg' class='insetimage' width='600' alt='Now I only score a hit if my shot enters the purple circle.' title='Now I only score a hit if my shot enters the purple circle.'\/><\/td><\/tr><tr><td class='insetcaption'>Now I only score a hit if my shot enters the purple circle.<\/td><\/tr><\/table><\/p>\n<p>This turns the robot hit zone into a circle. Now, this still isn&#8217;t perfect. Sometimes bits of the robot poke out from this circle, and bullets go through those points. But this is hard to notice and you can&#8217;t really see it happening unless you catch it in screenshot. This is basically good enough.<\/p>\n<p>Well, until we get here:<\/p>\n<p><table width='600'  cellpadding='0' cellspacing='0' border='0' align='center'><tr><td><img src='https:\/\/www.shamusyoung.com\/twentysidedtale\/images\/gr11_collision3.jpg' class='insetimage' width='600' alt='You&#8217;re a big one, aren&#8217;t you? What have they been feeding you?' title='You&#8217;re a big one, aren&#8217;t you? What have they been feeding you?'\/><\/td><\/tr><\/table><\/p>\n<p>Neither of these collision systems is good enough in this case. This boss is very large and its shape is complex. Having bullets pass through parts of it is frustrating for the player. Having bullets strike empty air is unsatisfying. Neither case is acceptable. There&#8217;s no way around it. We need pixel-accurate hit detection.<\/p>\n<p><a href=\"http:\/\/nehe.gamedev.net\/tutorial\/picking_alpha_blending_alpha_testing_sorting\/16005\/\" title=\"NeHe: Picking, Alpha Blending, Alpha Testing, Sorting\">This NeHe tutorial<\/a> is the usual go-to approach for hobbyists. It&#8217;s pretty brute-force, but it gets the job done. <\/p>\n<p>In the abstract, it works like this:<\/p>\n<p>You zoom in with the rendering camera so that the bullet (or whatever point you&#8217;re checking for collision) would fill the entire screen.  Then you tell OpenGL to go into &#8220;select&#8221; mode. Now start drawing stuff that you think this bullet might be crashing into. Give each one some sort of identifying number.<\/p>\n<p><em>Okay, OpenGL, here is object #1<\/em>. (Draw the first thing you think the bullet might crash into. It might be one polygon or ten thousand. OpenGL will treat all of them as &#8220;Object #1&#8221;.)<\/p>\n<p>Then you give object#2, #3, and as many different objects as you&#8217;d like to check.<\/p>\n<p>OpenGL won&#8217;t actually draw this stuff. Instead, it will keep a list of every object that lands inside the screen. (And remember the screen is super-zoomed in on the bullet, so anything that is within the screen is touching the bullet.) When you&#8217;re done, take OpenGL out of select mode and it will give you the list of stuff that would have been drawn.  You then have a list of all the stuff the bullet is touching, and can respond accordingly. <\/p>\n<p>Now, the most obvious objection to this approach is that it&#8217;s slow. You&#8217;re basically doing a little rendering loop for every single bullet in play. That sort of thing can get out of control fast. I also dislike it because it&#8217;s really cumbersome.  It takes a lot of lines of code to make this happen. You need to do all the math to zoom the camera, you need to set up select mode, you need to gather up likely candidates for collision, you need to perform some sort of abbreviated render loop to draw those candidates, you need to extract the list from OpenGL and sort through it, then you need to clean everything up and put the camera back where it belongs. <\/p>\n<p>In a 3D game, things are even worse. We&#8217;d have to take some other approach entirely. It would be time to bust out the trig textbooks and start intersecting lines (bullet paths) with planes (the polygons of all the stuff you&#8217;re shooting) to find out which dudes the player has shot. (Or whatever stuff is running into other stuff in this game.) This is also slow, complicated, and really annoying to debug. <\/p>\n<p>But we&#8217;re working in 2D, and we&#8217;re drawing all of our robots from a single texture. Right now, that texture looks like this:<\/p>\n<p><table width='600'  cellpadding='0' cellspacing='0' border='0' align='center'><tr><td><img src='https:\/\/www.shamusyoung.com\/twentysidedtale\/images\/gr11_sprites.jpg' class='insetimage' width='600' alt='Sprites are 64&#215;64 in the original. I dunno if I want to mess with that.  I&#8217;ll think more about art once the tech is working.' title='Sprites are 64&#215;64 in the original. I dunno if I want to mess with that.  I&#8217;ll think more about art once the tech is working.'\/><\/td><\/tr><tr><td class='insetcaption'>For the purposes of illustration, I&#8217;ve made the transparent areas magenta. <\/td><\/tr><\/table><\/p>\n<p>This gives me an idea. Normally I load this texture into memory, hand it off to OpenGL, and then throw it away after OpenGL makes a copy for itself. But instead of throwing it away, I&#8217;m going to hang onto it.  And I&#8217;m going to switch back to using the old rectangle-based collision I started with. So let&#8217;s say the player shoots at a robot:<\/p>\n<p><table width='600'  cellpadding='0' cellspacing='0' border='0' align='center'><tr><td><img src='https:\/\/www.shamusyoung.com\/twentysidedtale\/images\/gr11_collision4.jpg' class='insetimage' width='600' alt='BZAP!' title='BZAP!'\/><\/td><\/tr><\/table><\/p>\n<p>For the purposes of what we&#8217;re doing here, our bullets are single pixel in size, even if they&#8217;re visually massive and giving off a little cloud of glowing particle effects to make them seem even larger. If we detect our bullet has landed inside of the rectangle of a robot, then we ask the robot where it&#8217;s drawing from on the sprite sheet:<\/p>\n<p><table width='600'  cellpadding='0' cellspacing='0' border='0' align='center'><tr><td><img src='https:\/\/www.shamusyoung.com\/twentysidedtale\/images\/gr11_collision5.jpg' class='insetimage' width='600' alt='Not very impressive looking outside the normal in-game context, is it?' title='Not very impressive looking outside the normal in-game context, is it?'\/><\/td><\/tr><\/table><\/p>\n<p>How far is the bullet from the upper-left corner of the hit box? 25% of the way across and 74% of the way down? That would mean the bullet is touching this single pixel of the robot&#8217;s texture:<\/p>\n<p><table width='600'  cellpadding='0' cellspacing='0' border='0' align='center'><tr><td><img src='https:\/\/www.shamusyoung.com\/twentysidedtale\/images\/gr11_collision6.jpg' class='insetimage' width='600' alt='64&#215;64 pixels of glorious detail. Eat your heart out, Crysis!' title='64&#215;64 pixels of glorious detail. Eat your heart out, Crysis!'\/><\/td><\/tr><\/table><\/p>\n<p>I look at that pixel in the texture data I saved earlier and check to see if it&#8217;s opaque. It is? Then the player just shot this robot.<\/p>\n<p>Now we have pixel-perfect collision for basically free, in just a couple of lines of code. <\/p>\n<p>The only problem is this:<\/p>\n<p><table width='600'  cellpadding='0' cellspacing='0' border='0' align='center'><tr><td><img src='https:\/\/www.shamusyoung.com\/twentysidedtale\/images\/gr11_collision7.jpg' class='insetimage' width='600' alt='Bzzzzzzzzz.' title='Bzzzzzzzzz.'\/><\/td><\/tr><\/table><\/p>\n<p>This is some sort of spinning&#8230; sawblade&#8230; cutty-thing. I&#8217;m going to re-do the art for this one. Someone pointed out it looks kind of swastika-ish, and not like &#8220;shuriken made of scythes&#8221;, which is what I was kind of going for. Of course, I might re-do the art for everything. The robots are basically just the point where my prototypes looked acceptable enough that I could ignore them and go back to coding.<\/p>\n<p>At any rate, this reveals a slight problem with collision.  The blades are spinning very fast.  The bullets are moving very fast. The robot is moving fast.  And outside of that circle in the center, the robot&#8217;s texture is about 80% empty space. Which means if bullets pass through the area where the blades are, they only have a 20% chance of landing on a solid pixel each frame. If the bullet spends 5 frames travelling through the robot&#8217;s space, that means it still has a ~33% chance of passing all the way through without hitting it at all. <\/p>\n<p>You could excuse this by saying it&#8217;s like shooting through helicopter blades: Some rounds are bound to slip by.  But it doesn&#8217;t <em>feel<\/em> right. It feels like collision detection is broken. <\/p>\n<p>There are a bunch of ways I could fix this.  The most obvious would be to have it trace all the points along the bullet&#8217;s path if it finds itself in the hit-box of a robot. That would help, but I think there would still be a chance for the bullet to pass through when the blades are moving fast. A more lazy way would be to make some of that empty dead space between the blades semi-opaque. An opacity of 1 (out of 256) would be invisible to the player but would cause bullets to collide the way your eye expects.<\/p>\n<p>A well. When in doubt, put it off. I can make this call later after I&#8217;m sure I&#8217;m done messing with the art.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A running theme of this project is that 2D game development is like programming in Easy Mode. Everything takes less code, requires fewer steps, uses less CPU \/ memory, and has a larger margin for error. It&#8217;s amazing to be able to just make something without constantly getting snagged on annoying tech issues, performance trade-offs, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[498],"tags":[],"class_list":["post-21007","post","type-post","status-publish","format-standard","hentry","category-good-robot"],"_links":{"self":[{"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=\/wp\/v2\/posts\/21007","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=21007"}],"version-history":[{"count":0,"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=\/wp\/v2\/posts\/21007\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=21007"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=21007"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.shamusyoung.com\/twentysidedtale\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=21007"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}