CU_MDX_FourBlocksOfFun.zip (172.1 KB, 2,027 hits)
CUnitFramework.zip (101.1 KB, 12,576 hits)
inside the project (not solution) directory.
This tutorial uses the October 2005 SDK version. Microsoft changed the functionality after this version and I never got around to updating the code.
Tetris is the game that everyone says to create_ for your first game. It’s got everything a basic game should have: user-interaction, time management, basic collision detection, etc. We’ve already learned everything about Managed DirectX that we need to know to create_ Tetris, it’s just a matter of implementing the code.
Tetris’ main idea is to align shapes. If you fill up a row completely, it disappears and you score. There are many different kinds of shapes but they all share the same basic functionality. Therefore, we will create_ a Shape class that all the different types of shapes will inherit from. Every Shape in Tetris is composed of 4 blocks, the basic unit of Tetris. We will create_ a Block class and each Shape will have an array of 4 Blocks.
using System.Collections.Generic;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace CUnit
{
class Block : ICloneable
{
public static Rectangle BlockRectangle = new Rectangle( 0, 0, 15, 15 );
public static int BlockWidth = 16;
private Vector3 m_position;
private bool m_active;
/// <summary>Creates a new Block</summary>
/// <param name=”x”>X position</param>
/// <param name=”y”>Y position</param>
public Block( float x, float y )
m_active = true;
}
/// <summary>Renders the block</summary>
/// <param name=”sprite”>Sprite to render with</param>
/// <param name=”color”>Color of the block</param>
public void Render( Sprite sprite, Color color )
{
}
}
/// <summary>Gets and sets whether the block is active</summary>
public bool Active
{
set { m_active = value; }
}
/// <summary>Gets and sets the Blocks X position</summary>
public float X
set { m_position.X = value; }
}
/// <summary>Gets and sets the Blocks Y position</summary>
public float Y
set { m_position.Y = value; }
}
/// <summary>Clones the block</summary>
public object Clone()
{
block.Active = Active;
return block;
}
}
}
This is the Block class. Each block has a position and a method to render itself. We will be using Sprites to render our game, so render the Block with Sprite.Draw. Notice that the Texture, Rectangle and BlockWidth variables are static. This is because every Block in Tetris shares these values so there is no point allocating the memory for new values.
using System.Collections;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
namespace CUnit
{
class Shape : ICloneable
{
{
Color.Yellow, Color.Blue, Color.Orange
}
;
public enum ShapeType { Straight, Box, T, Zig, Zag, L, J };
protected Block[] m_blocks;
protected Color m_color;
protected int m_state;
protected ShapeType m_type;
/// <summary>Creates a new Shape</summary>
public Shape()
m_state = 0;
// Make random color
Random random = new Random( DateTime.Now.Millisecond );
int index = random.Next( 0, s_Colors.Length );
m_color = s_Colors[index];
}
/// <summary>Moves each block down 1 unit</summary>
public void MoveDown()
{
}
}
/// <summary>Moves each block right 1 unit</summary>
public void MoveRight()
{
{
}
}
/// <summary>Moves each block left 1 unit</summary>
public void MoveLeft()
{
}
}
/// <summary>Renders the shape</summary>
/// <param name=”sprite”>Sprite to render with</param>
public void Render( Sprite sprite )
{
}
}
/// <summary>Deactivates the blocks in a given row</summary>
/// <param name=”row”>Row to deactivate</param>
public void DeactivateBlocks( int row )
{
{
}
}
}
/// <summary>Determines if a Shape is blocked on the bottom.</summary>
/// <param name=”bottomBorder”>Bottom border</param>
/// <param name=”shapes”>Shapes that are settled</param>
/// <returns>true if the Shape is blocked, false otherwise.</returns>
public bool TouchingBottom( int bottomBorder, ArrayList shapes )
// or if they are touching the blocks of other shapes
foreach ( Block b1 in m_blocks )
{
{
}
foreach ( Shape s in shapes )
{
{
{
}
}
}
}
return false;
}
/// <summary>Determines if a Shape is blocked on the right.</summary>
/// <param name=”bottomBorder”>Right border</param>
/// <param name=”shapes”>Shapes that are settled</param>
/// <returns>true if the Shape is blocked, false otherwise.</returns>
public bool TouchingRight( int rightBorder, ArrayList shapes )
// or if they are touching the blocks of other shapes
foreach ( Block b1 in m_blocks )
{
{
}
foreach ( Shape s in shapes )
{
{
{
}
}
}
}
return false;
}
/// <summary>Determines if a Shape is blocked on the left.</summary>
/// <param name=”bottomBorder”>Left border</param>
/// <param name=”shapes”>Shapes that are settled</param>
/// <returns>true if the Shape is blocked, false otherwise.</returns>
public bool TouchingLeft( int leftBorder, ArrayList shapes )
{
// or if they are touching the blocks of other shapes
foreach ( Block b1 in m_blocks )
{
{
}
foreach ( Shape s in shapes )
{
{
{
}
}
}
}
return false;
}
/// <summary>Determines if a Shape is intersecting another Shape or any of the borders.</summary>
/// <param name=”shapes”>Shapes that are settled</param>
/// <param name=”leftBorder”>Left border</param>
/// <param name=”rightBorder”>Right border</param>
/// <param name=”bottomBorder”>Bottom border</param>
/// <returns>true if the Shape is intersecting, false otherwise.</returns>
public bool IntersectingShapesOrBorders( ArrayList shapes, int leftBorder, int rightBorder, int bottomBorder )
{
{
if ( b1.X < leftBorder || b1.X > rightBorder || b1.Y > bottomBorder )
{
}
// Intersecting another Shape?
foreach ( Shape s in shapes )
{
{
{
}
}
}
}
return false;
}
/// <summary>Moves blocks down if a row has been deactivated</summary>
/// <param name=”row”>Deactivated row</param>
public void MoveBlocksAboveRowDown( int row )
{
{
}
}
}
/// <summary>Checks if all the blocks of the Shape are inactive</summary>
/// <returns>true if all the blocks are inactive, false otherwise</returns>
public bool AllInactive()
{
{
{
}
}
return true;
}
/// <summary>Rotates the shape</summary>
public virtual void Rotate()
}
/// <summary>Gets and sets the Shape’s blocks.</summary>
public Block[] Blocks
set { m_blocks = value; }
}
/// <summary>Gets and sets the Shape’s Color.</summary>
public Color Color
set { m_color = value; }
}
/// <summary>Gets and sets the Shape’s rotation state.</summary>
public int State
set { m_state = value; }
}
/// <summary>Clones the Shape.</summary>
public object Clone()
{
Shape newShape = null;
switch ( m_type )
{
case ShapeType.Straight:
newShape = new ShapeStraight();
break;
case ShapeType.Box:
newShape = new ShapeBox();
break;
case ShapeType.T:
newShape = new ShapeT();
break;
case ShapeType.Zig:
newShape = new ShapeZig();
break;
case ShapeType.Zag:
newShape = new ShapeZag();
break;
case ShapeType.L:
newShape = new ShapeL();
break;
case ShapeType.J:
newShape = new ShapeJ();
break;
default:
throw new Exception( “Invalid type.” );
}
Block[] blocks = new Block[4];
for ( int i = 0; i < 4; i++ )
{
blocks[i] = (Block)m_blocks[i].Clone();
}
newShape.Blocks = blocks;
newShape.Color = m_color;
newShape.State = m_state;
return newShape;
}
}
}
This is the base Shape class. It contains all the methods we need to manipulate the Shape. I’ll now give a brief overview of each method. Each Shape is assigned a random color as shown in the constructor using the Random class. The MoveDown, MoveRight, and MoveLeft methods do just what their names imply and are used to move the shapes with the keyboard. The Render method loops through all 4 of its Blocks and Renders them. We call the DeactivateBlocks method when a row has been completely filled up. This deactivates all the Blocks of a given row so they will no longer be rendered or accounted for.
The TouchingBottom, TouchingRight, and TouchingLeft methods are the collision detection methods. They determine when a Shape can no longer move down, right, or left. This will happen when another Shape is in the way or the Shape has reached one of the borders of the play board.
The IntersectingShapesOrBorders method is used to see if a Shape is currently intersecting a border or another Shape. When a Shape is rotated, some of its Blocks may be moved to a location where they intersect another Shape or are out of bounds, which is bad. Therefore, when we rotate a Shape, we will first Clone the Shape, rotate the clone and see if the clone is intersecting any Shapes or borders. If it isn’t then we can rotate the original Shape.
The MoveBlocksAboveRowDown is called when a row has been deactivated. This method moves all Blocks above a given row down one unit to take the place of the deactivated row. The AllInactive method checks if all of the Blocks in the Shape are not active. We will use this method to remove the Shape from our main ArrayList of Shapes, since we don’t need to keep around a Shape if all of its Blocks are inactive.
The Rotate method is dependant upon the type of Shape and will be implemented in the inherited Shapes. When we rotate, however, we need to keep track of the Shape’s current rotation state so we know how to make our next rotation. This is what the m_state variable keeps track of.
Finally, the Clone method is the implementation of the ICloneable interface. We use this method in conjuction with the IntersectingShapesOrBorders method as explained above.
namespace CUnit
{
/// <summary>Straight shape</summary>
class ShapeStraight : Shape
{
/// <summary>Initializes the blocks of the shape</summary>
public ShapeStraight()
{
m_blocks[0] = new Block( 0, 0 );
m_blocks[1] = new Block( Block.BlockWidth, 0 );
m_blocks[2] = new Block( 2 * Block.BlockWidth, 0 );
m_blocks[3] = new Block( 3 * Block.BlockWidth, 0 );
m_type = ShapeType.Straight;
}
/// <summary>Rotates the shape</summary>
public override void Rotate()
{
switch ( m_state )
{
case 0:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y -= Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[2].Y += Block.BlockWidth;
m_blocks[3].X -= 2 * Block.BlockWidth;
m_blocks[3].Y += 2 * Block.BlockWidth;
m_state++;
break;
case 1:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y += Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[2].Y -= Block.BlockWidth;
m_blocks[3].X += 2 * Block.BlockWidth;
m_blocks[3].Y -= 2 * Block.BlockWidth;
m_state–;
break;
default:
break;
}
}
}
/// <summary>Box shape</summary>
class ShapeBox : Shape
{
/// <summary>Initializes the blocks of the shape</summary>
public ShapeBox()
{
m_blocks[0] = new Block( 0, 0 );
m_blocks[1] = new Block( Block.BlockWidth, 0 );
m_blocks[2] = new Block( 0, Block.BlockWidth );
m_blocks[3] = new Block( Block.BlockWidth, Block.BlockWidth );
m_type = ShapeType.Box;
}
}
/// <summary>T shape</summary>
class ShapeT : Shape
{
/// <summary>Initializes the blocks of the shape</summary>
public ShapeT()
m_blocks[0] = new Block( 0, Block.BlockWidth );
m_blocks[1] = new Block( Block.BlockWidth, Block.BlockWidth );
m_blocks[2] = new Block( Block.BlockWidth, 0 );
m_blocks[3] = new Block( 2 * Block.BlockWidth, Block.BlockWidth );
m_type = ShapeType.T;
}
/// <summary>Rotates the shape</summary>
public override void Rotate()
switch ( m_state )
{
case 0:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y -= Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[2].Y += Block.BlockWidth;
m_blocks[3].X -= Block.BlockWidth;
m_blocks[3].Y += Block.BlockWidth;
m_state++;
break;
case 1:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y += Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[2].Y += Block.BlockWidth;
m_blocks[3].X -= Block.BlockWidth;
m_blocks[3].Y -= Block.BlockWidth;
m_state++;
break;
case 2:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y += Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[2].Y -= Block.BlockWidth;
m_blocks[3].X += Block.BlockWidth;
m_blocks[3].Y -= Block.BlockWidth;
m_state++;
break;
case 3:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y -= Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[2].Y -= Block.BlockWidth;
m_blocks[3].X += Block.BlockWidth;
m_blocks[3].Y += Block.BlockWidth;
m_state = 0;
break;
default:
break;
}
}
}
/// <summary>Zig shape</summary>
class ShapeZig : Shape
{
/// <summary>Initializes the blocks of the shape</summary>
public ShapeZig()
{
m_blocks[0] = new Block( 0, 0 );
m_blocks[1] = new Block( 0, Block.BlockWidth );
m_blocks[2] = new Block( Block.BlockWidth, Block.BlockWidth );
m_blocks[3] = new Block( Block.BlockWidth, 2 * Block.BlockWidth );
m_type = ShapeType.Zig;
}
/// <summary>Rotates the shape</summary>
public override void Rotate()
switch ( m_state )
{
case 0:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y += Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[2].Y += Block.BlockWidth;
m_blocks[3].X -= 2 * Block.BlockWidth;
m_state++;
break;
case 1:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y -= Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[2].Y -= Block.BlockWidth;
m_blocks[3].X += 2 * Block.BlockWidth;
m_state–;
break;
default:
break;
}
}
}
/// <summary>Zag shape</summary>
class ShapeZag : Shape
{
/// <summary>Initializes the blocks of the shape</summary>
public ShapeZag()
{
m_blocks[0] = new Block( Block.BlockWidth, 0 );
m_blocks[1] = new Block( Block.BlockWidth, Block.BlockWidth );
m_blocks[2] = new Block( 0, Block.BlockWidth );
m_blocks[3] = new Block( 0, 2 * Block.BlockWidth );
m_type = ShapeType.Zag;
}
/// <summary>Rotates the shape</summary>
public override void Rotate()
{
switch ( m_state )
{
case 0:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y += 2 * Block.BlockWidth;
m_blocks[1].Y += Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[3].Y -= Block.BlockWidth;
m_state++;
break;
case 1:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y -= 2 * Block.BlockWidth;
m_blocks[1].Y -= Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[3].Y += Block.BlockWidth;
m_state–;
break;
default:
break;
}
}
}
/// <summary>L shape</summary>
class ShapeL : Shape
{
/// <summary>Initializes the blocks of the shape</summary>
public ShapeL()
m_blocks[0] = new Block( 0, 0 );
m_blocks[1] = new Block( 0, Block.BlockWidth );
m_blocks[2] = new Block( 0, 2 *Block.BlockWidth );
m_blocks[3] = new Block( Block.BlockWidth, 2 * Block.BlockWidth );
m_type = ShapeType.L;
}
/// <summary>Rotates the shape</summary>
public override void Rotate()
switch ( m_state )
{
case 0:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y += Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[2].Y -= Block.BlockWidth;
m_blocks[3].X -= 2 * Block.BlockWidth;
m_state++;
break;
case 1:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y += Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[2].Y -= Block.BlockWidth;
m_blocks[3].Y -= 2 * Block.BlockWidth;
m_state++;
break;
case 2:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y -= Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[2].Y += Block.BlockWidth;
m_blocks[3].X += 2 * Block.BlockWidth;
m_state++;
break;
case 3:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y -= Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[2].Y += Block.BlockWidth;
m_blocks[3].Y += 2 * Block.BlockWidth;
m_state = 0;
break;
default:
break;
}
}
}
/// <summary>J shape</summary>
class ShapeJ : Shape
{
/// <summary>Initializes the blocks of the shape</summary>
public ShapeJ()
{
m_blocks[0] = new Block( Block.BlockWidth, 0 );
m_blocks[1] = new Block( Block.BlockWidth, Block.BlockWidth );
m_blocks[2] = new Block( Block.BlockWidth, 2 * Block.BlockWidth );
m_blocks[3] = new Block( 0, 2 * Block.BlockWidth );
m_type = ShapeType.J;
}
/// <summary>Rotates the shape</summary>
public override void Rotate()
{
switch ( m_state )
{
case 0:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y += Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[2].Y -= Block.BlockWidth;
m_blocks[3].Y -= 2 * Block.BlockWidth;
m_state++;
break;
case 1:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y += Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[2].Y -= Block.BlockWidth;
m_blocks[3].X += 2 * Block.BlockWidth;
m_state++;
break;
case 2:
m_blocks[0].X -= Block.BlockWidth;
m_blocks[0].Y -= Block.BlockWidth;
m_blocks[2].X += Block.BlockWidth;
m_blocks[2].Y += Block.BlockWidth;
m_blocks[3].Y += 2 * Block.BlockWidth;
m_state++;
break;
case 3:
m_blocks[0].X += Block.BlockWidth;
m_blocks[0].Y -= Block.BlockWidth;
m_blocks[2].X -= Block.BlockWidth;
m_blocks[2].Y += Block.BlockWidth;
m_blocks[3].X -= 2 * Block.BlockWidth;
m_state = 0;
break;
default:
break;
}
}
}
}
These are the different types of Shapes found in Tetris. The only difference between them is the layout of their Blocks. Since they all have different Block layouts, they all need to be rotated differently. This took some quick pen-and-paper work to figure out the rotations.
namespace CUnit
{
/// <summary>
/// Used to generate random Shapes
/// </summary>
class ShapeGenerator
{
private Random m_random;
/// <summary>Creates a new ShapeGenerator</summary>
public ShapeGenerator()
{
m_random = new Random( DateTime.Now.Millisecond );
}
/// <summary>Gets a random new Shape</summary>
/// <returns>The random Shape</returns>
public Shape GetNextShape()
{
int value = m_random.Next( 7 );
switch ( value )
{
case 0:
return new ShapeStraight();
case 1:
return new ShapeBox();
case 2:
return new ShapeT();
case 3:
return new ShapeZig();
case 4:
return new ShapeZag();
case 5:
return new ShapeL();
case 6:
return new ShapeJ();
default:
return new ShapeBox();
}
}
}
}
The ShapeGenerator class is used to generate random Shapes. This is what determines which Shape we are currently playing with. All we do is use the Random class to pick a Shape.
using System.Collections;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using DirectInput = Microsoft.DirectX.DirectInput;
namespace CUnit
{
class FourBlocksOfFun
{
private float m_timePassed;
private float m_shapeFallInterval;
private float m_minFallInterval;
private int m_level;
private ShapeGenerator m_shapeGenerator;
private Shape m_shape;
private ArrayList m_settledShapes;
private Sprite m_sprite = null;
private Surface m_background = null;
private Font m_font = null;
private int m_bottomBorder;
private int m_leftBorder;
private int m_rightBorder;
private int m_numColumns;
private int m_numRows;
private int m_score;
private bool m_endGame;
/// <summary>Initialize the game.</summary>
public FourBlocksOfFun()
m_shapeGenerator = new ShapeGenerator();
m_shape = m_shapeGenerator.GetNextShape();
CenterShape( m_shape );
m_timePassed = 0.0f;
m_score = 0;
m_level = 1;
m_shapeFallInterval = 1.0f;
m_minFallInterval = 0.05f;
m_bottomBorder = 480 - Block.BlockWidth;
m_leftBorder = 13 * Block.BlockWidth;
m_rightBorder = 640 - 14 * Block.BlockWidth;
m_numColumns = ( m_rightBorder - m_leftBorder ) / Block.BlockWidth + 1;
m_numRows = 480 / Block.BlockWidth;
m_endGame = false;
m_gameTimer = new Timer();
m_gameTimer.Start();
}
/// <summary>Create resources</summary>
/// <param name=”device”>D3D Device</param>
public void OnCreateDevice( Device device )
Block.BlockTexture = TextureLoader.FromFile( device, Utility.GetMediaFile( “block.dds” ) );
// Create Sprite
m_sprite = new Sprite( device );
// Create font
if ( m_font != null )
{
}
m_font = new Font( device, “Arial”, 18, true, false );
}
/// <summary>Release default resources.</summary>
public void OnLostDevice()
{
}
if ( m_font != null )
{
}
if ( m_background != null )
{
m_background = null;
}
}
/// <summary>Create default resources.</summary>
/// <param name=”device”>D3D Device</param>
/// <param name=”framework”>Framework</param>
public void OnResetDevice( Device device, Framework framework )
{
}
if ( m_sprite != null )
{
}
// Create background surface
m_background = device.CreateOffscreenPlainSurface( 640, 480,
framework.CurrentSettings.AdapterFormat, Pool.Default );
SurfaceLoader.FromFile( m_background, Utility.GetMediaFile( “background.jpg” ), Filter.None, 0 );
}
/// <summary>Release managed resources.</summary>
public void OnDestroyDevice()
{
{
Block.BlockTexture = null;
}
if ( m_font != null )
{
m_font = null;
}
if ( m_sprite != null )
{
m_sprite = null;
}
}
/// <summary>Update the game.</summary>
public void Update()
{
}
m_gameTimer.Update();
m_timePassed += m_gameTimer.ElapsedTime;
if ( m_timePassed >= m_shapeFallInterval )
{
if ( m_shape.TouchingBottom( m_bottomBorder, m_settledShapes ) )
{
foreach ( Block b in m_shape.Blocks )
{
{
break;
}
}
AddToScore( 1 );
m_settledShapes.Add( m_shape );
m_shape = m_shapeGenerator.GetNextShape();
CenterShape( m_shape );
// Check for row completion
CheckRowCompletion();
}
else
{
}
}
}
/// <summary>Render the game.</summary>
/// <param name=”device”>D3D Device</param>
public void Render( Device device )
Surface backbuffer = device.GetBackBuffer( 0, 0, BackBufferType.Mono );
device.StretchRectangle( m_background, new Rectangle( 0, 0, 640, 480 ), backbuffer,
new Rectangle( 0, 0, backbuffer.Description.Width, backbuffer.Description.Height ),
TextureFilter.None );
backbuffer.Dispose();
m_sprite.Begin( SpriteFlags.AlphaBlend | SpriteFlags.SortTexture );
m_shape.Render( m_sprite );
foreach ( Shape s in m_settledShapes )
{
}
// Score
m_font.Print( m_sprite, “Level:”, 440, 80, 0, 0, Color.Black.ToArgb(), Font.Alignment.Left );
m_font.Print( m_sprite, m_level.ToString(), 500, 80, 130, 0, Color.Black.ToArgb(), Font.Alignment.Right );
m_font.Print( m_sprite, “Score:”, 440, 100, 0, 0, Color.Black.ToArgb(), Font.Alignment.Left );
m_font.Print( m_sprite, m_score.ToString(), 500, 100, 130, 0, Color.Black.ToArgb(), Font.Alignment.Right );
if ( m_endGame )
{
}
m_sprite.End();
}
/// <summary>Process input.</summary>
/// <param name=”pressedKeys”>Array of pressed keys</param>
/// <param name=”framework”>Framework so we can lock keys</param>
public void ProcessInput( DirectInput.Key[] pressedKeys, Framework framework )
{
if ( m_endGame )
{
return;
}
foreach ( DirectInput.Key k in pressedKeys )
{
switch ( k )
{
case DirectInput.Key.Left:
framework.LockKey( DirectInput.Key.Left );
// Stop at collision
if ( !m_shape.TouchingLeft( m_leftBorder, m_settledShapes ) )
{
m_shape.MoveLeft();
}
break;
case DirectInput.Key.Right:
framework.LockKey( DirectInput.Key.Right );
// Stop at collision
if ( !m_shape.TouchingRight( m_rightBorder, m_settledShapes ) )
{
m_shape.MoveRight();
}
break;
case DirectInput.Key.Down:
framework.LockKey( DirectInput.Key.Down );
// Stop at collision
if ( !m_shape.TouchingBottom( m_bottomBorder, m_settledShapes ) )
{
m_shape.MoveDown();
if ( m_shape.TouchingBottom( m_bottomBorder, m_settledShapes ) )
{
AddToScore( 1 );
// Ready for next shape
m_settledShapes.Add( m_shape );
m_shape = m_shapeGenerator.GetNextShape();
CenterShape( m_shape );
m_timePassed = 0.0f;
// Check for row completion
CheckRowCompletion();
}
}
break;
case DirectInput.Key.Up:
framework.LockKey( DirectInput.Key.Up );
Shape tempShape = (Shape)m_shape.Clone();
tempShape.Rotate();
if ( !tempShape.IntersectingShapesOrBorders( m_settledShapes, m_leftBorder,
m_rightBorder, m_bottomBorder ) )
{
m_shape.Rotate();
}
break;
}
}
}
/// <summary>Moves a shape over to the center.</summary>
/// <param name=”shape”>Shape to center</param>
public void CenterShape( Shape shape )
{
}
}
/// <summary>Checks if a row has been filled up and deactivates blocks on a complete row.</summary>
public void CheckRowCompletion()
{
int rowPosition = 0;
ArrayList rowsToRemove = new ArrayList();
int scoreMultiplier = 1;
// See if a row is completely filled up.
for ( int row = 0; row < m_numRows; row++ )
{
foreach ( Shape s in m_settledShapes )
{
{
{
if ( columnsFilled == m_numColumns )
{
rowsToRemove.Add( rowPosition );
scoreMultiplier *= 2;
columnsFilled = 0;
}
}
}
}
columnsFilled = 0;
}
// Update score
if ( scoreMultiplier != 1 )
{
}
// Remove the blocks from the rows that are marked for removal
ArrayList shapesToRemove = new ArrayList();
foreach ( int row in rowsToRemove )
{
{
( (Shape)m_settledShapes[i] ).MoveBlocksAboveRowDown( row );
if ( ( (Shape)m_settledShapes[i] ).AllInactive() )
{
// iterating through it.
shapesToRemove.Add( m_settledShapes[i] );
}
}
}
// Remove Shapes that are totally inactive from ArrayList
for ( int i = 0; i < shapesToRemove.Count; i++ )
{
}
}
/// <summary>Add to score and increment level if score threshold is met.</summary>
/// <param name=”amount”>Amount to add to score.</param>
public void AddToScore( int amount )
m_score += amount;
int scoreModAfter = m_score % 100;
// Increment level every 100 points
if ( scoreModBefore > scoreModAfter )
{
if ( m_shapeFallInterval - 0.1f >= m_minFallInterval )
{
}
}
}
}
}
This is the main control point of our game, Four Blocks of Fun. Since we’ll be plugging this into our GameApp class, it has the usual On*Device methods to handle resource allocation and deallocation. It also has methods to update_ and render the frame.
The main challenge of Tetris is the speed at which the Shapes fall. This will be determined with the m_shapeFallInterval variable. As the level increases, this value will decrease making the Shapes fall faster. We also have m_minFallInterval so the Shapes don’t keep falling faster and faster forever, which would be way too hard. Our game time will be managed by the Timer class that we wrote earlier.
In OnCreateDevice, we allocate our Block Texture, Sprite and game Font using the Font class we wrote earlier. In OnResetDevice, we create_ the off-screen surface to display the background, which we learned about in the previous tutorial. The OnLostDevice and OnDestroyDevice methods deallocate our game resources.
The Update method updates our game state. It updates the timer and see’s if enough time has passed to move the Shape down one unit. If enough time has passed, we first check to see if the Shape is touching anything on its bottom with Shape.TouchingBottom. If it isn’t touching anything we move the Shape down. If it is touching something, we first check if the game should be over. The game is over when the Shape is both touching a Shape below and touching the top of the screen. If the game isn’t over, it means that the Shape has landed. When the Shape lands, we increase the score with AddToScore, add the shape to our ArrayList of settled Shapes, and generate a new shape with the ShapeGenerator class. Once the Shape has been added to the ArrayList of settled Shapes, we check if any of the rows are complete with CheckRowCompletion.
The Render method simply renders everything to the screen. Since the FourBlocksOfFun class is plugged into the GameApp class, we don’t need to call Device.BeginScene or Device.EndScene, since those are called in GameApp.
The ProcessInput method processes user input. The Down, Left, and Right arrow keys move the Shape. When we move the Shape we check for collisions so we know if we can move the Shape in that direction. When we move down, there’s a chance that the Shape has landed so we do the same thing we did in the Update method: add the Shape to the settled shapes ArrayList and generate a new Shape. When we rotate the Shape with the Up key we have to check if the rotated Shape intersects anything. As explained earlier, we Clone the Shape and see if the clone intersects anything by calling Shape.IntersectingShapesOrBorders. If it isn’t we can rotate the Shape.
Since Shapes are generated at coordinates (0,0), the CenterShape method simply moves it right a few times so it starts in the center.
The CheckRowCompletion method is called everytime a Shape lands. To see if a row is complete, we loop through each row and count the number of blocks on the row. If this number is equal to the total number of columns in a row, the row is completely filled up. When a row is completely filled up, we add the row to an ArrayList so we can deactivate it later on. We don’t deactivate here because we are currently iterating through the rows and deactivating the row would mess up the iteration. We also increase a score multiplier. The score multiplier doubles for each row completed on a given Shape-landing. So for a single row completion, we would get ten points, while a completion of four rows would give us 80 points. Once we’ve found all the rows that need deactivation, we loop through each Shape and deactivate the Blocks on the given row with Shape.DeactivateBlocks. We also move all the higher Blocks down one unit with Shape.MoveBlocksAboveRowDown. Once we deactivate some Blocks, we need to check if all the Blocks of a given Shape are deactivated with Shape.AllInactive. If all the Blocks are inactive, there is no point in keeping the Shape around so we remove the Shape from our settled Shapes ArrayList.
Finally, the AddToScore method adds a given value to the player’s score. Every 100 points we increment the level and lower the shape fall interval so the blocks fall faster.
And there you have it: Tetri…I mean Four Blocks of Fun.