当前位置: 代码迷 >> 综合 >> DEVELOPING A CASUAL GAME WITH SILVERLIGHT 2 – PART 2
  详细解决方案

DEVELOPING A CASUAL GAME WITH SILVERLIGHT 2 – PART 2

热度:39   发布时间:2023-12-15 18:15:59.0

DEVELOPING A CASUAL GAME WITH SILVERLIGHT 2 – PART 2

Author: Joel Neubeck – Director of Technology / Silverlight MVP
Blog: http://joel.neubeck.net/

Expression Newsletter, subscribe now to get yours.

  • Module 1: Getting Started – Architecture / framework
  • Module 2: Movement and collision detection [this article]
  • Module 3: Design – Sprites, boards and dialogs
  • Module 4: Animations and sound
  • Module 5: Initialization and Deployment
  • Module 6: Advanced concepts (Physics, Multiplayer, Optimization)

In this series of articles, we are exploring the process of designing and building a casual online game in Silverlight 2  (SL2).  Throughout the series we target the interactive developer, as we construct our own version of the classic 1980’s game “Sabotage”.   The premise of this game is quite simple: get as many points as possible by shooting down parachuters and helicopters before the enemy destroys your bunker.

Module 2: Movement and collision detection

In our first module we built the architectural framework that will serve as the foundation for our game.   In this second article we will places all of our sprites on our stage and demonstrate how to make our helicopters move across the sky, drop a paratrooper, as we try to shoot both out of the sky. 

Movement

As we described in our first article , our game is built around a single controller.  This controller acts as the game’s central nervous system.  The controller will be responsible for maintaining the primary game loop used to move sprites and monitor collisions.

Note: Since Article 1 was published; Silverlight 2.0 RTM exposed a better technique for executing a game tick.  In this and future articles I will use the CompositionTarget.Rendering event to move sprites and check for collisions.

THE SPRITE

Each of our sprites comprises two elements: a view and a model.   The view represents what our element looks like, where as our model maintains the sprite’s behavioral state.  To minimize the amount of code each model requires, we will define a base class that each sprite must inherit.  Figure 1 demonstrates the interface our sprite class will implement.  Take note of the custom class we are using called Vector.  This object is very similar to System.Windows.Point, but contains a handful of convenient helper functions and operators, which are useful when working with 2d vector math.

In our implementation of MVC our models are active, suggesting that they must notify the appropriate view of a change in state, by adhering to an observer pattern. InFigure 1 we define two events: Moved and Collision.  These public events allow each view to subscribe to events fired by a model, when there is a change in state.  This change will be either a new position or when two objects have collided.

HELICOPTER

Our helicopter is probably our simplest moving sprite.  The helicopter will move across the screen, starting from the left, moving towards the right at a consistent speed.  For any of our sprites to move within our game we must make use of four very important properties found within our “Sprite” base class (Figure 1).

  • Velocity –the rate of change of position or the speed and direction of the sprite along the x-axis and y-axis
  • Direction – the relative position of the sprite in respect to the top of our stage.  Direction is a normalized vector.
  • Speed – the rate of motion or magnitude component of our velocity
  • Position – Used to maintain the current X and Y location of the sprite

Figure 1 - Sprite Object Model

At the appropriate time in the game, our controller will insert a helicopter into our game shell positioned at a negative X (to the left of our stage) and a random Y position (between 0 and 50 pixels).  As our game tick fires, we will call a “Move” method (Figure 2) found within our helicopters model.

The first and most important thing our Move method does is to calculate a new position for our helicopter.  When our helicopters model was instantiated, we passed to the model the elements starting position, direction and speed.  Our direction was calculated by determining the Sin (X) and Cosine (Y) of a specified angle in radians.  Since our helicopter will move from left to right, this angle is 90 degrees [radian = (90 * (Math.PI / 180)].  Once we have calculated the direction, we can set its initial velocity by simply multiply our direction by an arbitrary speed.  For this game our helicopter speed will remain at a constant .5.

public new void Move(FrameworkElement container)
 {
     Vector newPosition = Vector.Add(Velocity, Position);
     //used to update each of our boundaries
     UpdatePosition(newPosition);
     //if the sprite has moved past the right side then deactivate it and stop timer
     if (base.Position.X > container.Width)
     {
         //deactivate
         base.Active = false;
         _timer.Stop();
     }
     this.OnMoved();
 }

Figure 2 - Helicopter Move Method

With all of these properties set, we can calculate the helicopters new position by adding its current position to its velocity.  Once we have the helicopters new location, we can run a simple check to make sure it is within the bounds of our stage.  Lastly, we will fire the “Move” event to trigger our view to react to the elements change in state.

EXECUTE MOVEMENT IN OUR VIEW

Now that we have completed updating the position of our sprite within the model, we can physically move the view within our stage. Figure 3 illustrates how our view will achieve movement.   Each of our sprites defines a TranslateTransform object that is set as the objects render transformation.  When the view is notified of movement (Model fires a Move event) the TranslateTransform.X and Y properties are updated.  These updates will physical move the UIElement to a new X and Y position within its parent control.  Each time that our game tick fires and chooses to move a helicopter, the whole process is repeated.  This exact same approach is used for moving any of our sprites, this included bullets, troopers and even our clouds.

public Helicopter(Models.Helicopter model)
 {
     // Required to initialize variables
     InitializeComponent();
     //store the model passed via dependency injection to a local variable
     _model = model;
     _translate.X = _model.Position.X;
     _translate.Y = _model.Position.Y;
      . . . .
     //attach as an observer  to the model
     _model.Moved += new Game.Models.MovedHandler(_model_Moved);
     //insert a transformationgroup into the root UIElment
     TransformGroup transforms = new TransformGroup();
     transforms.Children.Add(_translate);
     this.RenderTransform = transforms;
     this.HorizontalAlignment = HorizontalAlignment.Left;
     this.VerticalAlignment = VerticalAlignment.Top;
     this._model.Width = this.Width;
     this._model.Height = this.Height;
 }
 void _model_Moved(object sender)
 {
     _translate.X = _model.Position.X;
     _translate.Y = _model.Position.Y;
 }

Figure 3 - Helicopter view

Shooting our Gun

Now that we have a basic understanding of movement, we can explore a more complicated behavior in the game, shooting our gun.  Each time the user moves their mouse our gun tracks their movement and calculates the angle generated by the cursors position and the center of the gun.   Figure 4 illustrates the way in which we determine this angle. As the gun moves counter clockwise, the angle goes from 0-180?.   With a little basic trigonometry we can calculate any sides of our triangle and the angle produced, if we know the length of any two sides of a right triangle.  InFigure 4 the red line represents the adjacent side of the triangle, the green line represents the side opposite of our desired angle, and the blue line shows the length of our hypotenuse. In this scenario we can calculate the length of the hypotenuse and the length of the opposite side, by simply subtracting the guns mid point from the location of the mouse.   Figure 5 shows the calculation we use to determine our angle. Now that we have the angle, we can fire a custom event to the guns view to rotate our turret. 

public void SetAngle(Point mouse)
{
     double mX = mouse.X - _midpoint.X;
     double mY = (mouse.Y - _midpoint.Y)*-1;
     _crosshair = new Point(mX, mY);
     _angle = Helper.ConvertToDegrees(Math.Atan2(mY, mX));
     if(_angle <0)
         _angle += 360;
     OnTurretMoved();
}

Figure 5 - SetAngle method

Fire a Bullet

Once the angle of trajectory has been calculated, we can work on determining both the velocity (direction * speed) and the location we want the bullet to exit the turret. When our gun was placed into our shell, we were able to calculate the mid point of the gun. This mid point is the center of rotation or origin in which we rotate our turret.  We can use this mid point as our starting point when we project along the x and y-axis the exit point of our bullet. Figure 6 shows the trigonometry we use to make these calculations.  Here are a few important things to note:

  • The angle we used for rotating our turret is not the same angle we need to use to calculate direction.  Since we intend to shoot upward we need to add 90 degrees to this angle before we make any of our calculations. 
  • Our turret rotates in a perfect 180? arc, as a result, the length of the turret can be used as our radius when projecting the exact X and Y position of the end of the turret.

//we have to add 90 deg to our angle since we are shooting upwards
 double angle = _player.Model.Angle+90;
 //as the barrel rotates it creates a radius of 30 pixels minus the radius of the bullet (3)
 double radius = 27;
 double sin = Math.Sin(Helper.ConvertToRadians(angle));
 double cosine = Math.Cos(Helper.ConvertToRadians(angle));
 //determin the exit point of the bullet from the turret
 double exitX = (_player.Model.Midpoint.X-3) + sin * radius;
 double exitY = (_player.Model.Midpoint.Y-3) + cosine * radius;
 Vector exitV = new Vector(exitX, exitY);
 //calculate the direction the bullet will be traveling.
 Vector directionV = new Vector(sin, cosine);

Figure 6 – Calculate Exit point and direction of bullet

Collision Detection

A game wouldn’t be a game if objects did not react when they collided with each other.  When a bullet strikes the parachute of a falling paratrooper we need to react and send that trooper plummeting to the ground.   That type of collision detection can be factored into a single methodology that can be used for any of our game elements. 

BROAD PHASE

Before we can test collision between any two objects, we need to determine which pairs of objects should be tested.  In a complex games it is feasible to have dozens if not hundreds of objects on a stage at any given time.  If we were to simply test each object against another, the majority of our time would be spent testing impossible collisions.  Lets take for example a paratrooper that is falling from the sky.  If that trooper was dropped on the left side of the stage and I fire a bullet towards the right, there is no need for me to test that collision. 

One possible solution to this challenge is to split the stage into a uniform grid of tiles (grid[c,r]).  The site of a tile is very important.  To minimize the number of potential cells a single sprite can touch, I have chosen to set my cells dimension equal to my largest sprite.  This results in a single sprite never occupying more than four cells. 

To test what cells the sprite is overlapping, the first thing we need to do is determine where the sprites center falls.  From that cell we can simply test if the sprites bounds overlap the cells above or bellow or to the left or right.  Since our grid is the size of our largest sprite, it makes it impossible for the sprite to be in more then two adjacent cells.  Once we have determined which adjacent cell might be occupied, we continue the approach and test to see if its bounds extend into the upper or lower cells.  When all tests are completed, we have a sprite that falls in 1,2,or 4 cells.

As objects move throughout the stage (world) we will constantly maintain this 2-dimensional array of which of these cells each object occupy. If two objects are not in the same cell, it is impossible that any of there boundaries are intersecting.

Here is how I maintain my array.  The first part of my approach is to define a custom property on my Sprite.   This  cells property is a generic List<T> of Cell objects.  A Cell is a simple struct that indicates both the column and row the sprite is occupying.   This property is used to quickly remove the sprite from a previous cell, as the object moves throughout my stage.  The second part of my approach is to store a reference to the sprite in a generic LinkedList<T> collection.  This list is then stored in the appropriate cell of my 2d array.  When I am ready to begin my narrow phase of collision testing, a LinkedList is a pretty efficient way for me to move “Next” and “Previous” through my collection of sprites.  Figure 7 is the method I execute on each game tick to iterate over this array.

void CompositionTarget_Rendering(object sender, EventArgs e)
 {
     if (_running)
     {
         MoveBullets();
         MoveHelicopters();
         MoveTroopers();
         for (int r = 0; r<_gridMoving.GetLength(0); r++)
         {
             for (int c = 0; c < _gridMoving.GetLength(0); c++)
             {
                 LinkedList<ISprite> cell = _gridMoving[r, c];
                 if (cell != null)
                 {
                     LinkedListNode<ISprite> first = cell.First;
                     while (first != null)
                     {
                         ISprite sprite = first.Value;
                         LinkedListNode<ISprite> next = first.Next;
                         while (next != null)
                         {
                             if (sprite.Moving)
                             {
                                 if (CheckIntersection(sprite, next))
                                     break;
                            }
                             next = next.Next;
                         }
                         first = first.Next;
                         if (sprite.Moving)
                         {
                             cell.Remove(sprite);
                         }
                     }
                 }
             }
         }  
     }
 }

Figure 7 – CompositionTarget_Rendering handler

NARROW PHASE

Now that we have a collection of objects to test, we can take a narrower approach to determine if two objects are intersecting.  The foundation of our narrow phase of collision detection is a set of boundaries associated with each sprite.  These boundaries come in the form of an axis aligned bounding box (AABB) or a bounding sphere.  Each of our sprites can be composed of as many collision boundaries as required to achieve the precise level of collision detection.  For example, a paratrooper is comprised of two collision boundaries: a box surrounding the parachute and a second box surround the trooper.  By defining two collision boundaries, we can react differently to a collision depending on what part of the paratrooper was struck.  If for example a bullet strikes the trooper body, an explosion will occur.  If the bullet strikes the parachute, the trooper will plummet to the ground spinning out of control. Figure 8 illustrates how we define our paratrooper’s collision boundaries, where as Figure 9 demonstrates a BoundingSphere used for our bullet.

Vector extent = new Vector(10,5);
Vector center = Vector.Add(extent, _model.Position);
Models.Collision.BoundingBox box1 = new 
      BoundingBox(Models.Paratrooper.BoundaryNames.Parachute.ToString(),
      Center,extent);
_model.Boundaries.Add(box1);
 extent = new Vector(6.5, 7);
 center = Vector.Add(new Vector(10, 21), _model.Position);
 Models.Collision.BoundingBox box2 = new
      BoundingBox(Models.Paratrooper.BoundaryNames.Trooper.ToString(),
      center,extent));
 _model.Boundaries.Add(box2);

Figure 8 – Paratrooper boundaries

double radius = (ellipse.Width / 2);
Vector center = Vector.Add(_model.Position, radius);
_model.Boundaries.Add(new BoundingSphere("outline", radius, center));

Figure 9 – Bullet boundaries

Looking a bit deeper into how our BoundingBox and BoundingSphere objects are constructed, we can see that both implement the identical interface, (IBoundary) which requires both to define an “Intersects” method.  This method will be called each time we would like to determine if a specific sprites collision boundary is intersecting with another game elements collision boundary.  Figure 10 illustrates one way we might test the intersection of a bullet with each of paratrooper’s defined boundaries.

foreach (Models.Collision.IBoundary bounds in _troopers[j].Model.Boundaries)
 {
     if (bounds.Intersects(_bullets[i].Model.Boundaries[0]))
     {
         _bullets[i].Model.Collide(_troopers[j].Model as Sprite);
         _troopers[j].Model.Collide(_bullets[i].Model as Sprite);
     }
 }

Figure 10 – Testing intersection

INTERSECTION

In collision detection the most popular way of determining if two convex objects intersect is to use the separating axis theorem (SAT).  This theorem states that “the projection of two convex shapes onto some line will be separate if and only if they are not intersecting” Wikipedia.  If you search the Internet, you will find a ton of examples of how this theorem is used in collision detection.  For this game, I have chosen to keep my boundaries confined to either an Axis-Align Bounding Box (AABB) or a sphere.  This certainly limits my accuracy of collision, but allow for a more simplified approach to calculating intersection.

AABB TO AABB

When a paratrooper collides with the ground, we will always have two AABB colliding (one stationary and one moving).  Figure 11 illustrates what these boundaries would look like if we visualized each within the game.

The most common way to test if two objects are overlapping is to check for intersection on each game tick.  The potential issue with this approach is that if any of your objects are moving at a fast velocity, then they might pass through each other without detecting the collision.  To mitigate this risk, we can implement a sweep algorithm.  In a sweep test, we will take the objects velocity into account to calculate both the first and last time both objects overlap.  With these two values, we could then apply a more precise test to determine intersection.  Figure 12 illustrates some of the calculations necessary to determine if a separating axis exists between our objects.  If objects overlap along all of the possible separating axes, then we are guaranteed of a collision.  If we find any separation, then we can stop checking because it is impossible that the two objects are overlapping.

Figure 12 - AABB Separating axis calculations

SPHERE - AABB

When a bullet collides with a paratrooper we must test the intersection of a sphere (bullet) with one of the paratrooper AABB regions.  In many games, a bullet is very small in diameter and traveling at an extremely high velocity.   These two factors make it realistic that if we used a simple intersection test the bullet might pass through the target without identifying a collision.    Fortunately, our bullet is fairly large in diameter, 6 pixels, and is not traveling at a very fast velocity.  As a result, we can use a very simple form of intersection test to achiever a reliable result.  In general, spheres are not directly handled by the SAT because they have an infinite number of separating axes.  One possible solution, is to calculate the point on the AABB that is closest to our sphere then calculate the distance from that point to the center of our sphere.  If the distance is less then the radius of our sphere, then we know that it is intersecting. Figure 13 shows an example of how this form of intersection test might be calculated.

private bool Intersect(BoundingBox b)
 {
     double x = 0.0;
     double y = 0.0;
     Vector bCenter = b.GetCenter();
     Vector sRelative = Vector.Subtract(this.Center, bCenter);
     //X axis
     if (sRelative.X < -bCenter.X)
         x = -bCenter.X;
     else if (sRelative.X > bCenter.X)
         x = bCenter.X;
     else
         x = sRelative.X;
     //Y axis
     if (sRelative.Y < -bCenter.Y)
         y = -bCenter.Y;
     else if (sRelative.Y > bCenter.Y)
         y = bCenter.Y;
     else
         y = sRelative.Y;
     //Now that we have the closest point on the AABB, get the distance from
     //that to the sphere center, and see if it's less than the radius
     Vector dist = Vector.Subtract(sRelative, new Vector(x, y));
     if (dist.LengthSquared < this.Radius * this.Radius)
     {
         this.Hit = true;
         b.Hit = this.Hit;
     }
     else
     {
         this.Hit = false;
         b.Hit = this.Hit;
     }
     return this.Hit;
 }

Figure 13 - Sphere intersecting with AABB

Now that we have some objects moving around the screen and colliding, its time to start visualizing each element!  In the next article, we will focus on our design.  We will begin to draw each of our sprites and start to visualize each of the elements that go into our game.

For complete source or to make a comment on this article, drop by my blog at: 
http://joel.neubeck.net/2008/11/casual-game-m2-expression-newsletter/

Thank you and see you next time.

Joel Neubeck, Director of Technology, Terralever

 

Resources:

Gamedev.net - http://www.gamedev.net/

Miguel Gomez - http://www.gamasutra.com/features/19991018/Gomez_3.htm

Emanuele Feronato - http://www.emanueleferonato.com/2007/04/28/create-a-flash-artillery-game-step-1/

Raigan Burns and Mare Sheppard -http://www.metanetsoftware.com/technique/tutorialB.html

http://www.metanetsoftware.com/technique/tutorialA.html

This article appeared in the November Expression Newsletter, subscribe now to get yours.

  相关解决方案