
Instead of the preface.
I have always loved and will love computer games. There is in them some kind of inner magic that always attracts and fascinates at the same time. I've played dozens of games in my entire life, starting with veterans Wolfenstein and Dune 2 and ending with modern blockbusters. And now, finishing off another hit and watching the final movie and captions, in the head more and more often flashes the bust “What if ...?”
But really, what if you take and write your own game? Of course, it is clear that it will not be possible to do the AAA title alone, and these are years of work and so on and so forth, but will you master the way? It just so happens that in desktop programming, I am frankly weak, and there are not so many options for a practicing web developer. But in recent years, everything has changed dramatically, and now the browser has a lot in common with a coffee maker, and javascript can easily satisfy even the needs of military departments, not just my own.
')
That's just during the next reflection and quite a serious cold, I caught the eye of an article about Box2d in the game psychologist Ant.Karlov'a. Having read and remarked, I very quickly found the JS-port of this library, and the old crazy idea to do something small and, most importantly - my own, began to pester me with new forces.
In general, less pathetic, more work. I hope you will be interested. May the harsh gods forgive me for using Angry Birds in KPDV ^ _ ^
Intro and Disclaimer
The ultimate goal of the entire cycle of notes is to make a full-fledged clone of Angry Birds, with at least 3 types of throwing weapons and 5 different levels. Yes, there are already enough of them, and in different layouts, and even Angry Birds itself, at least 3 versions, but do you need to practice something? In the process, I will try to touch on all aspects - the physics engine itself, work with raster graphics, the level editor (and you will definitely need it), menu binding / credits / settings, sound and music, and a thousand other important details. Actually, I don’t have all of this myself, but in the process of writing I’m going to tell here what and how I managed to do it and what it led to.
I will show some particularly long pieces of code in ellipsis. For particularly impatient at the end of the notes there is a link to a working example.
The only thing is that I am not a professional game developer, many steps are given to me with the help of very large healing rakes and filling of painful cones, so if I make a mistake somewhere, do not judge strictly and do not throw tomatoes on it.
Go!
So let's get started. The first thing we need is to create a game world, fill it with a test level and try to throw the first “bird” into the first “pig”. Since we saw pseudo-real physics in Angry Birds itself, we will need a physics engine that will be busy handling collisions and moving birds, pigs, and other objects around the game world.
I chose Box2d as a physics engine for several reasons:
1. He has a fairly strong Flash-community, where you can ask for help or advice.
2. The Box2dWeb library that I decided to use is Box2dAS direct port, so there is at least an API Reference.
3. Enough useful information, true in anti-Mongolian, about the development process and the engine's insides.
4. And, of course, hundreds of successful applications.
In general, we have an engine - now we need to roughly present everything that we need and create our small and cozy world. We begin, perhaps, with a small HTML-blanks. Throw on the canvas page and create the base object of our game.
< html >
< head >
< title > Angry Birds WRYYYYYYYY !!!! < / title >
< script src = "/js/jquery.js" type = "text / javascript" > < / script >
< script src = "/js/box2d.js" type = "text / javascript" > < / script >
< script >
$ (document) .ready (function () {
new Game ();
});
< / script >
< body >
<canvas id = "canvas" width = "800" height = "600" style = " background- color : # 333333 ; "> </ canvas>
< / body >
< script type = "text / javascript" >
Game = function () {
...
}
< / script >
< / html >
Great, we have a template for further experiments. Now you need to create a physical world and start actively using Box2d. This is what we will do.
Game = function ( ) {
// I decided to immediately denote object types as constants
const PIG = 1 ;
const BIRD = 2 ;
const PLANK = 3 ;
// Flag that we need when processing the "throw".
isMouseDown = false ;
// Future gum for virtual slingshot
mouseJoint = false ;
// Used in cast processing
body = false ;
mousePosition = { x : 0 , y : 0 } ;
// All necessary resources for our current needs.
// I decided to redefine right away in order not to fence kilometers of incomprehensible and unreadable code.
// The value of each resource I will try to explain further in the article.
b2AABB = Box2D. Collision . b2AABB ;
b2World = Box2D. Dynamics . b2World ;
b2Vec2 = Box2D. Common . Math b2Vec2 ;
b2DebugDraw = Box2D. Dynamics . b2DebugDraw ;
b2Body = Box2D. Dynamics . b2Body ;
b2BodyDef = Box2D. Dynamics . b2BodyDef ;
b2FixtureDef = Box2D. Dynamics . b2FixtureDef ;
b2PolygonShape = Box2D. Collision . Shapes . b2PolygonShape ;
b2CircleShape = Box2D. Collision . Shapes . b2CircleShape ;
b2MouseJointDef = Box2D. Dynamics . Joints . b2MouseJointDef ;
b2ContactListener = Box2D. Dynamics . b2ContactListener ;
// Create our world.
world = new b2World (
new b2Vec2 ( 0 , 10 ) // Gravitational Vector.
, true // doSleep flag.
) ;
init = function ( ) { // Initialization of everything in our world, called when an object is created
...
}
init ( ) ;
}
I would like to dwell on the parameters of our world in more detail. The first is the vector of gravity. Box2d simulates the physics of the surrounding world in some detail, therefore, it could not do without the influence of gravity. With us, this vector sets the acceleration of free fall equal to 10 virtual meters per second along the Y axis. The second parameter: doSleep, allows not counting inactive elements at the current moment. This greatly affects the speed of work and I do not advise changing this parameter.
I would also like to add that by default Box2d uses the coordinate system with the origin in the upper left corner.
I think that all those present here are aware that the screen canvas is processed frame-by-frame in any game, and Box2d is no exception. Therefore, we need to create an update method for it and drive this method into SetTimeout so that it is invoked constantly to recalculate objects and redraw graphics.
Game = function ( ) {
...
init = function ( ) {
window. setInterval ( update , 1000/60 ) ;
}
update = function ( ) {
world. Step ( 1/60 , 10 , 10 ) ;
world. DrawDebugData ( ) ;
world. ClearForces ( ) ;
}
}
In the update () function, we, first of all, set the world update rate - 60 frames per second, and also set the maximum number of processed events for changing the speed of objects and their position for 1 work cycle. With an increase in these parameters, the overall reaction rate will increase, but with an increase in the number of objects we can stumble into system resources, while decreasing, we will get an incorrect processing of objects from tick to tick. 10 - quite sane mean value.
And, of course, now we should tie our canvas to the engine's processor, so that we can draw something on it.
Game = function ( ) {
...
init = function ( ) {
buildWorld ( ) ;
initDraw ( ) ;
window. setInterval ( update , 1000/60 ) ;
}
initDraw = function ( ) {
debugDraw = new b2DebugDraw ( ) ;
debugDraw. SetSprite ( document. GetElementById ( "canvas" ) . GetContext ( "2d" ) ) ;
debugDraw. SetDrawScale ( 30.0 ) ;
debugDraw. SetFillAlpha ( 0.5 ) ;
debugDraw. SetLineThickness ( 1.0 ) ;
debugDraw. SetFlags ( b2DebugDraw. E_shapeBit | b2DebugDraw. E_jointBit ) ;
world. SetDebugDraw ( debugDraw ) ;
}
buildWorld = function ( ) {
fixDef = new b2FixtureDef ( ) ;
fixDef. density = 1.0 ;
fixDef. friction = 0.5 ;
fixDef. restitution = 0.2 ;
}
}
Yes, it is worth adding that for display in the current view we will only use Debug mode - i.e. direct representation of objects in our physical world without textures and graphics. What is called - As Is. It is for this purpose that we call the DrawDebugData () method in the update () function, and it is for this display that we create the b2DebugDraw () object. In the same place we set the scale, translucency and specify by bits what exactly we need to draw. In more detail, everything about b2DebugDraw () is quite well described in the API Reference.
It is also worth adding that Box2d does not handle pixels as grid data. It was originally designed for the use of the standard C system - therefore we have meters, kilograms, seconds, and the corresponding transformations. Now I will not go deep into this area, just keep in mind that 1 meter in the Box2d view is about 30 pixels of screen space.
Well, add a little more about the structure fixDef () - this is a set of basic parameters of the physics of objects. It is in this structure that describes the main parameters by which the physics of the process of interaction of objects will be calculated. As you can see, we specified the parameters for friction, density and elasticity of objects.
Drawing
We came very close to our cherished goal - designing the mechanics of the game, but first we need to put some objects into our world and then try to do something with them. Therefore, we will add in our init () methods for constructing constraints for our space and draw our level with rectangles as unbreakable boards and circles as “pigs”.
Game = function ( ) {
...
PigData = function ( ) { } ;
PigData. prototype . GetType = function ( ) {
return PIG ;
}
PlankData = function ( ) { }
... // GetType () is the same as PigData.GetType ()
buildWorld = function ( ) {
...
bodyDef = new b2BodyDef ( ) ;
bodyDef. type = b2Body. b2_staticBody ;
fixDef. shape = new b2PolygonShape ;
fixDef. shape . SetAsBox ( 20 , 2 ) ;
bodyDef. position . Set ( 10 , 600/30 + 1.8 ) ;
world. CreateBody ( bodyDef ) . CreateFixture ( fixDef ) ;
... // 3 more walls
canvasPosition = $ ( "#canvas" ) . offset ( ) ;
}
buildLevel = function ( ) {
createPlank ( 22 , 20 , 0.25 , 2 )
... // Drawing 8 more boards
createPig ( 20 , 11 , 0.5 ) ;
... // 2 more additional pigs
}
createPlank = function ( x , y , width , height ) {
bodyDef. type = b2Body. b2_dynamicBody ;
fixDef. shape = new b2PolygonShape ;
fixDef. shape . SetAsBox (
width
height
) ;
bodyDef. position . x = x ;
bodyDef. position . y = y ;
plank = world. CreateBody ( bodyDef ) ;
plank. SetUserData ( new PlankData ( ) ) ;
plank. CreateFixture ( fixDef ) ;
}
createPig = function ( x , y , r ) {
... // Method analogous to the createPlank method, except that CircleShape and PigData () are used;
}
}
Box2d allows you to bind to any physical object an additional "user" interface, with which you can directly implement the game logic. That is why we added 2 objects - PigData and PlankData, which return us the type of object. This will become important a little later when we deal with collision handling.
The BodyDef structure is intended to describe the geometric characteristics of an object and its general behavior (FixtureDef also describes its physical properties). It is in BodyDef that we indicate how a particular block will be computed, whether it will be static, or whether it will be dynamic. In general, in Box2d, as the documentation tells us, there are 3 types of objects - static, i.e. fully static objects (in DebugView, green objects), dynamic — fully independent dynamic objects, and kinematic — objects that are responsible for motion. As an example, static is the road, dynamic is the suspension of the car and the wheels, and kinetic is the engine of the car.
We also indicate the geometrical shape of the object, the position of its upper left corner and dimensions. Everything is in meters (Oh, how long I guessed why my dimensions and pixels do not match!).
Well, in the end we are drawing a new method, drawLevel (), which will draw us the entire level along with boards and pigs.
Slingshot
The most interesting part of the story. Now we will try to create an arbitrary prototype of the slingshot, which will launch our birds in pigs.
For this we need a mouse.
Game = function ( ) {
...
init = function ( ) {
bindMouse ( ) ;
...
}
bindMouse = function ( ) {
$ ( document ) . mousedown ( function ( e ) {
isMouseDown = true ;
handleMouse ( e ) ;
$ ( document ) . bind ( "mousemove" , { } , handleMouse ) ;
} ) ;
$ ( document ) . mouseup ( function ( e ) {
isMouseDown = false ;
$ ( document ) . unbind ( "mousemove" ) ;
} ) ;
}
handleMouse = function ( e ) {
mouseX = ( e. clientX - canvasPosition. left ) / 30 ;
mouseY = ( e. clientY - canvasPosition. top ) / 30 ;
} ;
update = function ( ) {
if ( isMouseDown ) {
if ( ! ( body ) ) {
mousePosition = { x : mouseX , y : mouseY } ;
createPig ( mouseX , mouseY , 0.40 ) ;
body = getBodyAtMouse ( ) ;
md = new b2MouseJointDef ( ) ;
md. bodyA = world. GetGroundBody ( ) ;
md. bodyB = body ;
md. target . Set ( mousePosition. X , mousePosition. Y ) ;
md. collideConnected = true ;
md. maxForce = 300.0 * body. GetMass ( ) ;
mouseJoint = world. CreateJoint ( md ) ;
body SetAwake ( true ) ;
}
body SetPosition ( new b2Vec2 ( mouseX , mouseY ) ) ;
}
if ( mouseJoint && ! isMouseDown ) {
mouseX = mousePosition. x ;
mouseY = mousePosition. y ;
if ( getBodyAtMouse ( ) ) {
world. DestroyJoint ( mouseJoint ) ;
mouseJoint = null ;
body = false ;
}
}
...
}
}
So what did we do here? The first is, of course, the mouse handlers mousedown and mouseup have nailed to our little game. Now, if you click on the mouse button in the game field, the isMouseDown flag will be set, well, and when you move the mouse, the coordinates stored in mouseX and mouseY will change. The second thing we have achieved is the dynamic creation of an object when the mouse is clicked; I brought this part to the update () method. Roughly speaking, we immediately create a new “bird”, if it was not there, even though it is here and is an object with the “pig” type - it flies no worse.
It is more interesting - with the help of the GetBodyAtMouse () method, we immediately receive the body of our bird object for processing and use MouseJoint to attach it to our world. Those. it is MouseJoint that creates the slingshot gum that will launch the birdie. There we also indicate the direction of action of our flexible coupling and maximum force.
In the example it is clearly seen that after clicking on the mouse button, a new circle appears with a characteristic bluish ligament attached to the center. If you do not press the mouse button, the bundle will continue to reach the center of the circle. But if you squeeze ...
If you release - the isMouseDown flag changes and the second condition starts to work, which using the same method getBobyAtMouse determines whether the body of the attached ligament has crossed the body, and if it has crossed, removes the ligament. In turn, the body, i.e. our bird pigs goes on a free flight for fear of all pigs not flying ^ _ ^.
I would like to dwell on the GetBodyAtMouse () method in more detail, since it is very interesting.
function getBodyAtMouse ( ) {
mousePVec = new b2Vec2 ( mouseX , mouseY ) ;
var aabb = new b2AABB ( ) ;
aabb. lowerBound . Set ( mouseX - 0.001 , mouseY - 0.001 ) ;
aabb. upperBound . Set ( mouseX + 0.001 , mouseY + 0.001 ) ;
selectedBody = null ;
world. QueryAABB ( getBodyCallback , aabb ) ;
return selectedBody ;
}
getBodyCallback = function ( fixture ) {
if ( fixture. GetBody ( ) . GetType ( ) ! = b2Body. b2_staticBody ) {
if ( fixture. GetShape ( ) . TestPoint ( fixture. GetBody ( ) . GetTransform ( ) , mousePVec ) ) {
selectedBody = fixture. GetBody ( ) ;
return false ;
}
}
return true ;
}
The first thing that is done here is to create a new vector with the current mouse position. Next, we create a structure that selects a part of the screen space in the Box2d coordinate system under the mouse pointer, and with the help of getBodyCallback we determine whether the area under the mouse intersects with any body at all, and if so, set the new selectedBody. In principle, everything is simple and nontrivial.
Pigs
To this part, in principle, everything went quite monotonously in the outline of standard examples. But for correct handling of collisions I had to dig a little in the manual. What needs to be done now? Just a little bit - to ensure that our pigs, located on the level, disappeared. But not just from the contact, but from contact with a certain effort. To do this, we will need to redefine the standard contact handler and add the total impulse count that our mumps received during the collision.
Game = function ( ) {
...
GameContactListener = function ( ) { } ;
GameContactListener. prototype = b2ContactListener. prototype ;
GameContactListener. prototype . PostSolve = function ( contact , impulse ) {
if ( contact. GetFixtureB ( ) . GetBody ( ) . GetUserData ( ) ) {
var BangedBody = contact. GetFixtureB ( ) . GetBody ( ) ;
if ( contact. GetFixtureB ( ) . GetBody ( ) . GetUserData ( ) . GetType ( ) == PIG ) {
var imp = 0 ;
for ( a in impulse. normalImpulses ) {
imp = imp + impulse. normalImpulses [ a ] ;
}
if ( imp > 3 ) {
destroyedBodies. push ( BangedBody ) ;
BangedBody. visible = false ;
BangedBody = null ;
}
}
}
var contactListener = new GameContactListener ( ) ;
world. SetContactListener ( contactListener ) ;
update = function ( ) {
...
destroyedBodies = Array ( ) ;
...
world . ClearForces ( ) ; // IMPORTANT!
while ( destroyedBodies. length > 0 ) {
world. DestroyBody ( destroyedBodies. Shift ( ) ) ;
}
}
}
The first thing we do here is redefine the standard ContactListener. This is the object responsible for handling all collisions. In the same place, using our method, we check UserData () and are satisfied that we really hit the piggy, and if we are satisfied with the impulse with which it hit, we hide it from the battlefield and put it into the array for deletion.
I honestly sprinkle my head with ashes — I picked up the power of the impulse at random, since there were no longer any forces to consider the necessary impulse of effort manually. I hope, picked up the best option. Also, this condition is important in that when creating the world, collisions also occur until the world comes to a static state (Did you observe a slight twitch of floor slabs in creating a level in some Flash games? In my example, this there is one too. Actually, so that the pigs do not disappear in case of such collisions, we need to check the total impulse).
Further at the end of the update method, after calling ClearForces (), we digest our array and remove all the pigs that were mercilessly beaten by a flying sister. I specifically singled out this place as important - you will not be able to remove any object until the physics calculation process takes place. These are the conditions of Box2d. Only after all the objects have got rid of mathematics is it possible to freely remove them. That is why the pigs burst only after the scene, in fact, ended.
Results
Fuf, kind of like a sheet completed. I apologize for the abundance of code and possible errors.
As I promised - a link to
an example in which everything that I described above is fully implemented.
Well, the screenshot of the resulting work:

In fact, we now have a lot of inaccuracies - you can run a bird from any place of the world in any direction, the power is much greater than necessary, there is no scoring on the fact of mumps death, there is a bug, when the bird can be run past the intersection and it will not come off the bundle, and the bird is not a bird at all, but a pig! But nevertheless, the first layer of fully working game mechanics was drawn, and it is quite possible to finish some little things into it.
Of the plans for the near future - to change the logic of determining when to destroy the coupling, because now you can run the bird in a circle. And, of course, you need to take up the level editor. But this is a topic for the next article.
Thank you for your attention, I will again constructive criticism ^ _ ^
List of useful
code.google.com/p/box2dweb - Box2dWeb, Box2dAS port for Javascript.
www.box2dflash.org/docs/2.1a/reference - API Reference on anti-
mongol .
docs.google.com/View?id=dfh3v794_41gtqs6wf4&pli=1 - A fairly comprehensive guide in Russian. Thanks VirtualMaestro and his blog - flashnotes.ru. There is also a lot of useful about Box2d.
www.emanueleferonato.com - A very useful blog about Box2d in particular, and indie in general.
ant-karlov.ru - mastermind blog, if someone is interested ^ _ ^