Search Tips Advanced Search |
| Delphi Graphics and Game Programming Exposed with DirectX 7.0 by John Ayres Wordware Publishing, Inc. ISBN: 1556226373 Pub Date: 12/01/99
Search this book:
Previous | Table of Contents | Next
Listing 2-3: The Shoot ’em example game
    unit ShootEmU;

    {******************************************************************************

        Shoot Example Application

        Author: John Ayres

        Like most examples in the book, this one takes advantage of the baseline

        DirectX application code.  It implements a very simplistic game in order

        to demonstrate basic game architecture. The architecture of a game is

        arbitrary and will in part be determined by the type of game being

        created. However, most games share similar tasks in order to generate

        a frame of animation, and this example should serve to familiarize

        the user with some of the more common gaming application tasks.

     ******************************************************************************

        Copyright © 1999 by John Ayres, All Rights Reserved

        This example utilizes sprites from the freeware sprite library

        SpriteLib by Ari Feldman.

        Sprite lib sprites Copyright (c) 1998 by Ari Feldman. Check out

        http://www.chromewave.com

        This code is freeware and can be used for any personal or commercial

        purposes without royalty or license.  Use at your own risk.

     ******************************************************************************}

    interface

    uses

      Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,

      DDraw;

    {these constants are used to modify specific attributes of the DirectX

     application, such as the color of the main form, the requested resolution,

     etc.}

    const

      DXWIDTH      = 640;

      DXHEIGHT     = 480;

      DXCOLORDEPTH = 8;

      BUFFERCOUNT  = 1;

      COOPERATIVELEVEL = DDSCL_FULLSCREEN or DDSCL_ALLOWREBOOT or

                         DDSCL_ALLOWMODEX or DDSCL_EXCLUSIVE;

      SURFACETYPE      = DDSCAPS_COMPLEX or DDSCAPS_FLIP or DDSCAPS_PRIMARYSURFACE;

    const

      {this user-defined message is used to start the flipping loop}

      WM_DIRECTXACTIVATE = WM_USER + 200;

      {the player’s ship index}

      PLAYERSHIPIDX = 0;

      {the total number of ships}

      NUMSHIPS = 5;

      {the total number of bullets}

      NUMBULLETS = 19;

      {the total number of explosions}

      NUMEXPLOSIONS = 5;

      {the total number of stars}

      NUMSTARS = 100;

    type

      {tracks the overall game state}

      TGameState = (gsDemo, gsPlaying, gsIntermission);

      {our base sprite class}

      TSprite = class

      public

        XPos, YPos,

        XVel, YVel,

        NumFrames,

        CurFrame,

        FrameWidth, FrameHeight,

        XFrameStartOffset, YFrameStartOffset: Integer;

        BoundBox,

        CollisionBox: TRect;

        Living: Boolean;

        CurAnimTime,

        AnimThreshold: LongWord;

        procedure Move; virtual;

        procedure Draw; virtual;

      end;

      {our star class, descended from sprites}

      TStar = class(TSprite)

      public

        Color: Byte;

        procedure Move; override;

        procedure Draw; override;

      end;

      {the explosion class, also descended from sprites}

      TExplosion = class(TSprite)

      public

        procedure Move; override;

      end;

      {the form class}

      TfrmDXAppMain = class(TForm)

        procedure FormCreate(Sender: TObject);

        procedure FormDestroy(Sender: TObject);

        procedure FormKeyDown(Sender: TObject; var Key: Word;

          Shift: TShiftState);

        procedure FormActivate(Sender: TObject);

      private

        { Private declarations }

        {flips back to the GDI surface to display the exception error message}

        procedure ExceptionHandler(Sender: TObject; ExceptionObj: Exception);

        {the main rendering loop}

        procedure AppIdle(Sender: TObject; var Done: Boolean);

        {intercepts certain messages to provide appropriate functionality}

        procedure AppMessage(var Msg: TMsg; var Handled: Boolean);

        {flips the DirectX surfaces}

        procedure FlipSurfaces;

        {restores any lost surfaces}

        procedure RestoreSurfaces;

        {draws the contents of surfaces}

        procedure DrawSurfaces;

      public

        {tracks the overall game state}

        GameState: TGameState;

        {tracks the level and score, number of ships left to destroy before

         advancing to the next level, and the number of lives the player has left}

        Level,

        Score,

        NumShipsToNextLevel,

        LivesLeft: Integer;

        {timing variable for pausing in the intermission mode}

        TimeAccum: Longint;

        {indicates if the player can shoot or not}

        CanShoot: Boolean;

        {tracks all of the living ships}

        Ships: array[0..NUMSHIPS] of TSprite;

        {tracks all of the living player bullets}

        PlayerBullets: array[0..NUMBULLETS] of TSprite;

        {tracks all of the living enemy bullets}

        EnemyBullets: array[0..NUMBULLETS] of TSprite;

        {tracks all of the living explosions}

        Explosions: array[0..NUMEXPLOSIONS] of TExplosion;

        {tracks all of the living stars}

        Stars: array[0..NUMSTARS] of TStar;

        {these are various procedures that initialize certain variables and

         generalize control of certain elements of the game}

        procedure StartEnemy;

        procedure StartExplosion(XPos, YPos: Integer);

        procedure StartBullet(XPos,YPos,XVel,YVel: Integer;PlayerBullet: Boolean);

        procedure ResetPlayer;

        procedure ResetAll;

        procedure PlayerShipKilled;

        procedure NewGame;

      end;

    var

      frmDXAppMain: TfrmDXAppMain;

      {the main DirectDraw interface}

      FDirectDraw: IDirectDraw4;

      {the interfaces for the primary and back buffer surfaces and bitmap image}

      FPrimarySurface,

      FBackBuffer,

      FGraphicsImage: IDirectDrawSurface4;

      {the palette interface}

      FPalette: IDirectDrawPalette;

    implementation

    uses

      DXTools, DDUtil, MMSystem, gensuppt;

    {$R *.DFM}

    { *—->>  BASELINE APPLICATION CODE <<—-* }

    { — the callback function used to ensure that the selected graphics mode —

      — is supported by DirectX                                              — }

    function EnumModesCallback(const EnumSurfaceDesc: TDDSurfaceDesc2;

                               Information: Pointer): HResult; stdcall;

    begin

      {if the height, width, and color depth match those specified in the

       constants, then indicate that the desired graphics mode is supported}

      if (EnumSurfaceDesc.dwHeight = DXHEIGHT) and

         (EnumSurfaceDesc.dwWidth = DXWIDTH) and

         (EnumSurfaceDesc.ddpfPixelFormat.dwRGBBitCount = DXCOLORDEPTH) then

        Boolean(Information^) := TRUE;

      Result := DDENUMRET_OK;

    end;

    { —> Events Hooked to the Application <— }

    { — this event is called when an exception occurs, and simply flips back —

      — to the GDI surface so that the exception dialog box can be read      — }

    procedure TfrmDXAppMain.ExceptionHandler(Sender: TObject;

                                             ExceptionObj: Exception);

    begin

      {disconnect the OnIdle event to shut off the rendering loop}

      Application.OnIdle := nil;

      {if the DirectDraw object was successfully created, flip to the GDI surface}

      if Assigned(FDirectDraw) then

        FDirectDraw.FlipToGDISurface;

      {display the exception message}

      MessageDlg(ExceptionObj.Message, mtError, [mbOK], 0);

      {reconnect the OnIdle event to reenter the rendering loop}

      Application.OnIdle := AppIdle;

    end;

    { — handles certain messages that are required to make DirectX function —

      — properly within Delphi                                              — }

    procedure TfrmDXAppMain.AppMessage(var Msg: TMsg; var Handled: Boolean);

    begin

      case Msg.Message of

        WM_ACTIVATEAPP:

          {unhook the OnIdle event when the application is being deactivated.

           this will stop all rendering}

          if not Boolean(Msg.wParam) then

             Application.OnIdle := nil

          else

            {upon activating the application, send ourselves the user-defined

             message}

            PostMessage(Application.Handle, WM_DIRECTXACTIVATE, 0, 0);

        WM_DIRECTXACTIVATE:

          begin

            {upon activating, restore all surfaces (reloading their memory as

             necessary), hook up the OnIdle event, and redraw the contents of

             all surfaces}

            RestoreSurfaces;

            Application.OnIdle := AppIdle;

            DrawSurfaces;

          end;

        WM_SYSCOMMAND:

          begin

            {do not allow a screen saver to kick in}

            Handled := (Msg.wParam = SC_SCREENSAVE);

          end;

      end;

    end;

    { —> Form Events <— }

    { — initialize essential form properties — }

    procedure TfrmDXAppMain.FormCreate(Sender: TObject);

    var

      iCount: Integer;

    begin

      Randomize;

      {set up the application exception handler}

      Application.OnException := ExceptionHandler;

      {initialize form properties;  note that the FormStyle property must be

       set to fsStayOnTop}

      BorderStyle := bsNone;

      BorderIcons := [];

      FormStyle   := fsStayOnTop;

      Color       := clBlack;

      Cursor      := crNone;

      {initialize the game state to demo}

      GameState := gsDemo;

      {create all of the star objects}

      for iCount := 0 to NUMSTARS do

      begin

        {create the star}

        Stars[iCount] := TStar.Create;

        {set its position}

        Stars[iCount].XPos := Random(DXWIDTH);

        Stars[iCount].YPos := Random(DXHEIGHT);

        {set its velocity. stars are only going to move from the top of the screen

         to the bottom, so we’re only concerned with the horizontal velocity}

        Stars[iCount].XVel := 0;

        Stars[iCount].YVel := Random(5)+1;

        {set the color of the star based on its velocity}

        case Stars[iCount].YVel of

          1..2  : Stars[iCount].Color := 252;

          3..4  : Stars[iCount].Color := 251;

          5     : Stars[iCount].Color := 255;

        end;

      end;

    end;

    { — provides essential cleanup functionality — }

    procedure TfrmDXAppMain.FormDestroy(Sender: TObject);

    var

      iCount: Integer;

    begin

      {disengage our custom exception handler}

      Application.OnException := nil;

      {remember, we do not have to explicitly free the DirectDraw objects, as they

       will free themselves when they go out of context (such as when the

       application is closed)}

      for iCount := 0 to NUMSTARS do

        Stars[iCount].Free;

    end;

    { — this method initializes DirectX and creates all necessary objects — }

    procedure TfrmDXAppMain.FormActivate(Sender: TObject);

    var

      {we can only get a DirectDraw4 interface from the DirectDraw interface, so we

       need a temporary interface}

      TempDirectDraw: IDirectDraw;

      {structures required for various methods}

      DDSurface: TDDSurfaceDesc2;

      DDSCaps: TDDSCaps2;

      {flag used to determine if the desired graphics mode is supported}

      SupportedMode: Boolean;

    begin

      {if DirectDraw has already been initialized, exit}

      if Assigned(FDirectDraw) then exit;

      {create a temporary DirectDraw object. this is used to create the

     desired DirectDraw4 object}

      DXCheck( DirectDrawCreate(nil, TempDirectDraw, nil) );

      try

        {we can only get a DirectDraw4 interface through the QueryInterface

         method of the DirectDraw object}

        DXCheck( TempDirectDraw.QueryInterface(IID_IDirectDraw4, FDirectDraw) );

      finally

        {now that we have the DirectDraw4 object, the temporary DirectDraw

         object is no longer needed}

        TempDirectDraw := nil;

      end;

      {set the cooperative level to that defined in the constants}

      DXCheck( FDirectDraw.SetCooperativeLevel(Handle, COOPERATIVELEVEL) );

      {hook up the application message handler}

      Application.OnMessage := AppMessage;

      {call EnumDisplayModes and verify that the desired graphics mode is

       indeed supported}

      FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0);

      DDSurface.dwSize   := SizeOf(TDDSurfaceDesc2);

      DDSurface.dwFlags  := DDSD_HEIGHT or DDSD_WIDTH or DDSD_PIXELFORMAT;

      DDSurface.dwHeight := DXHEIGHT;

      DDSurface.dwWidth  := DXWIDTH;

      DDSurface.ddpfPixelFormat.dwSize := SizeOf(TDDPixelFormat_DX6);

      DDSurface.ddpfPixelFormat.dwRGBBitCount := DXCOLORDEPTH;

      SupportedMode := FALSE;

      DXCheck( FDirectDraw.EnumDisplayModes(0, @DDSurface, @SupportedMode,

                                            EnumModesCallback) );

      {if the desired graphics mode is not supported by the DirectX drivers,

       display an error message and shut down the application}

      if not SupportedMode then

      begin

        MessageBox(Handle, PChar(The installed DirectX drivers do not support a +

                                 display mode of: +IntToStr(DXWIDTH)+’ X +

                                 IntToStr(DXHEIGHT)+’, +IntToStr(DXCOLORDEPTH)+

                                  bit color’), Unsupported Display Mode Error’,

                                 MB_ICONERROR or MB_OK);

        Close;

        Exit;

      end;

      {set the display resolution and color depth to that defined in the constants}

      DXCheck( FDirectDraw.SetDisplayMode(DXWIDTH, DXHEIGHT, DXCOLORDEPTH, 0, 0) );

      {initialize the DDSurface structure to indicate that we will be creating a

       complex flipping surface with one backbuffer}

      FillChar(DDSurface, SizeOf(TDDSurfaceDesc2), 0);

      DDSurface.dwSize  := SizeOf(TDDSurfaceDesc2);

      DDSurface.dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;

      DDSurface.ddsCaps.dwCaps := SURFACETYPE;

      DDSurface.dwBackBufferCount := BUFFERCOUNT;

      {create the primary surface object}

      DXCheck( FDirectDraw.CreateSurface(DDSurface, FPrimarySurface, nil) );

      {indicate that we want to retrieve a pointer to the backbuffer (the surface

       immediately behind the primary surface in the flipping chain) }

      FillChar(DDSCaps, SizeOf(TDDSCaps2), 0);

      DDSCaps.dwCaps := DDSCAPS_BACKBUFFER;

      {retrieve the surface}

      DXCheck( FPrimarySurface.GetAttachedSurface(DDSCaps, FBackBuffer) );

      {load the palette from the bitmap to be used and attach it to the

       primary surface}

      FPalette := DDLoadPalette(FDirectDraw, ExtractFilePath(ParamStr(0))+

      SpriteImgs.bmp’);

      DXCheck( FPrimarySurface.SetPalette(FPalette) );

      {load the bitmap image containing the background and animation frames}

      FGraphicsImage := DDLoadBitmap(FDirectDraw, ExtractFilePath(ParamStr(0))+

                        SpriteImgs.bmp’);

      {indicate which color in the bitmap is transparent}

      DXCheck( DDSetColorKey(FGraphicsImage, $00000000) );

      {initialize the surfaces with the images}

      DrawSurfaces;

      {post a message that will hook up the OnIdle event and start the main

       rendering loop}

      PostMessage(Handle, WM_ACTIVATEAPP, 1, 0);

    end;

    { —> Form Methods <— }

    { — this method is called in order to flip the surfaces — }

    procedure TfrmDXAppMain.FlipSurfaces;

    var

      DXResult: HResult;

    begin

      {perform the page flip. note that the DDFLIP_WAIT flag has been used,

       indicating that the function will not return until the page flip has been

       performed. this could be removed, allowing the application to perform other

       processing until the page flip occurs. however, the application will need to

       continuously call the Flip method to ensure that the page flip takes place}

      elseDXResult := FPrimarySurface.Flip(nil, DDFLIP_WAIT);

      {if the surfaces were lost, restore them. on any other error, raise an

       exception}

      if DXResult = DDERR_SURFACELOST then

        RestoreSurfaces

      else if DXResult <> DD_OK then

        DXCheck(DXResult);

    end;

    { — this method is called when the surface memory is lost  —

      — and must be restored.  surfaces in video memory that   —

      — contain bitmaps must be reinitialized in this function — }

    procedure TfrmDXAppMain.RestoreSurfaces;

    begin

      {restore the primary surface, which in turn restores any implicit surfaces}

      FPrimarySurface._Restore;

      {restore the bitmap image’s surface}

      FGraphicsImage._Restore;

      {reload the bitmap into the surface}

      DXCheck( DDReLoadBitmap(FGraphicsImage, ExtractFilePath(ParamStr(0))+

                              SpriteImgs.bmp’) );

    end;

    { — this method is continuously called by the application, and provides —

      — the main rendering loop.  this could be replaced by a custom        —

      — while..do loop                                                      — }

    procedure TfrmDXAppMain.AppIdle(Sender: TObject; var Done: Boolean);

    begin

      {indicates that the application should continuously call this method}

      Done := FALSE;

      {if DirectDraw has not been initialized, exit}

      if not Assigned(FDirectDraw) then Exit;

      {draw surface content and flip the surfaces}

      DrawSurfaces;

      FlipSurfaces;

    end;

    { — this method is called when the contents of the surfaces need to be  —

      — drawn. it will be continuously called by the AppIdle method, so any  —

      — rendering or animation could be done within this method             — }

    procedure TfrmDXAppMain.DrawSurfaces;

    var

      SourceRect,

      ISectRect: TRect;

      iCount, iCount2: Integer;

      SurfaceDC: HDC;

      WorkCanvas: TCanvas;

    begin

      {erase the last frame of animation}

      ColorFill(FBackBuffer, $00000000, nil);

      {draw all of the background stars. the stars are a background animation

       that are unaffected by any game state, so we always draw them}

      for iCount := 0 to NUMSTARS do

      begin

       Stars[iCount].Move;

       Stars[iCount].Draw;

      end;

      {perform various actions based on the current game state}

      case GameState of

        gsDemo : begin

                   {in the demo state, we simply want to draw some text on the screen}

                   WorkCanvas := TCanvas.Create;

                   FBackBuffer.GetDC(SurfaceDC);

                   {draw the game’s title and instructions}

                   try

                     WorkCanvas.Handle := SurfaceDC;

                     WorkCanvas.Font.Size := 52;

                     WorkCanvas.Font.Name := Arial’;

                     WorkCanvas.Font.Style := [fsBold, fsItalic];

                     WorkCanvas.Font.Color := clRed;

                     WorkCanvas.Brush.Style := bsClear;

                     WorkCanvas.TextOut(DXWIDTH div 2 -

                                       (WorkCanvas.TextWidth(Shoot ’em’) div 2),

                                       DXHEIGHT div 2 -

                                       (WorkCanvas.TextHeight(Shoot ’em’) div 2),

                                       Shoot ’em’);

                     WorkCanvas.Font.Size := 14;

                     WorkCanvas.Font.Name := Arial’;

                     WorkCanvas.Font.Style := [fsBold];

                     WorkCanvas.Font.Color := clWhite;

                     WorkCanvas.Brush.Style := bsClear;

                     WorkCanvas.TextOut(DXWIDTH div 2 - (WorkCanvas.

                                        TextWidth(Press Enter to begin’) div 2),

                                        DXHEIGHT - (WorkCanvas.

                                        TextHeight(Press Enter to begin’)) - 15,

                                        Press Enter to begin’);

                   finally

                     WorkCanvas.Handle := 0;

                     FBackBuffer.ReleaseDC(SurfaceDC);

                     WorkCanvas.Free;

                   end;

                   {when the user presses the Enter key, initialize a new

                    game, and change the game state}

                   if (GetAsyncKeyState(VK_RETURN) and $8000) = $8000 then

                     NewGame;

                 end;

        gsIntermission : begin

                           {in the intermission state, we want to draw the current

                            level on the screen for only a few seconds before

                            changing the game state}

                           {record the current time}

                           if TimeAccum = -1 then

                             TimeAccum := timeGetTime;

                           {draw the current level on the screen}

                           WorkCanvas := TCanvas.Create;

                           FBackBuffer.GetDC(SurfaceDC);

                           try

                             WorkCanvas.Handle := SurfaceDC;

                             WorkCanvas.Font.Size := 52;

                             WorkCanvas.Font.Name := Arial’;

                             WorkCanvas.Font.Style := [fsBold, fsItalic];

                             WorkCanvas.Font.Color := clRed;

                             WorkCanvas.Brush.Style := bsClear;

                             WorkCanvas.TextOut(DXWIDTH div 2 - (WorkCanvas.

                                       TextWidth(Level +IntToStr(Level)) div 2),

                                       DXHEIGHT div 2 - (WorkCanvas.

                                       TextHeight(Level +IntToStr(Level)) div 2),

                                       Level +IntToStr(Level));

                           finally

                             WorkCanvas.Handle := 0;

                             FBackBuffer.ReleaseDC(SurfaceDC);

                             WorkCanvas.Free;

                           end;

                           {if we have been displaying this for more than 3 seconds,

                            reset certain variables and change the game state}

                           if timeGetTime-TimeAccum>3000 then

                           begin

                             ResetPlayer;

                             TimeAccum := -1;

                             GameState := gsPlaying;

                           end;

                         end;

        gsPlaying : begin

                      {in the playing state, the user is actively playing the

                       game. this section will contain the game logic for

                       moving the player and performing enemy AI}                  {retrieve user input}

                      if Ships[PLAYERSHIPIDX] <> nil then

                      begin

                        {move left or right when cursor keys are pressed}

                        if (GetAsyncKeyState(VK_RIGHT) and $8000) = $8000 then

                          Ships[PLAYERSHIPIDX].XPos := Ships[PLAYERSHIPIDX].XPos+4;

                        if (GetAsyncKeyState(VK_LEFT) and $8000) = $8000 then

                          Ships[PLAYERSHIPIDX].XPos := Ships[PLAYERSHIPIDX].XPos-4;

                        {make sure the player does not go beyond the

                         screen boundaries}

                        if Ships[PLAYERSHIPIDX].XPos < 0 then

                          Ships[PLAYERSHIPIDX].XPos := 0;

                        if Ships[PLAYERSHIPIDX].XPos>DXWIDTH-Ships[PLAYERSHIPIDX].

                                                     FrameWidth then

                          Ships[PLAYERSHIPIDX].XPos:=DXWIDTH-Ships[PLAYERSHIPIDX].

                                                     FrameWidth;

                        {fire a shot when the spacebar is pressed. the CanShoot

                         variable is used here to force the player to release the

                         spacebar before another shot can be fired}

                        if ((GetAsyncKeyState(32) and $8000) = $8000) and

                           (CanShoot) then

                        begin

                          StartBullet(Ships[PLAYERSHIPIDX].XPos+

                                      (Ships[PLAYERSHIPIDX].FrameWidth div 2)-5,

                                      Ships[PLAYERSHIPIDX].YPos, 0, -8, TRUE);

                          CanShoot := FALSE;

                        end

                        else

                        if (GetAsyncKeyState(32) and $8000) = 0 then

                          CanShoot := TRUE;

                      end;

                      {some enemy AI. this is incredibly simplistic, but it

                       simply creates an enemy ship at a random interval. the

                       frequency of enemy ship generation increases as the

                       level increases, making the game harder}

                      if Random(1000) < 5 * Level then

                        StartEnemy;

                      {move all of the bullets}

                      for iCount := 0 to NUMBULLETS do

                      begin

                        {move player bullets}

                        if PlayerBullets[iCount] <> nil then

                        begin

                          {update the bullet based on velocity}

                          PlayerBullets[iCount].Move;

                          {collision detection. we must determine if this

                           bullet has collided with an enemy ship}

                          for iCount2 := PLAYERSHIPIDX+1 to NUMSHIPS do

                            if (Ships[iCount2] <> nil) and

                               (IntersectRect(ISectRect,

                                              PlayerBullets[iCount].CollisionBox,

                                              Ships[iCount2].CollisionBox)) then

                            begin

                              {indeed, the bullet did strike an enemy ship.

                               we should create an explosion, increment the

                               player’s score, and destroy both the bullet and

                               the enemy ship}

                              StartExplosion(Ships[iCount2].XPos,

                                             Ships[iCount2].YPos);

                              Inc(Score, Level * 5);

                              PlayerBullets[iCount].Living := FALSE;

                              Ships[iCount2].Living := FALSE;

                              {the level increases when the player has destroyed

                               a certain number of enemy ships, so we must

                               record this kill}

                              Dec(NumShipsToNextLevel);

                            end;

                          {if the bullet is still alive, draw it; otherwise

                           destroy it}

                          if PlayerBullets[iCount].Living then

                            PlayerBullets[iCount].Draw

                          else

                          begin

                           PlayerBullets[iCount].Free;

                           PlayerBullets[iCount] := nil;

                          end;

                        end;

                        {now, we move enemy bullets}

                        if EnemyBullets[iCount] <> nil then

                        begin

                          {update the bullet based on velocity}

                          EnemyBullets[iCount].Move;

                          {as with the player bullets, we must determine if an

                           enemy bullet has collided with the player’s ship}

                          if (Ships[PLAYERSHIPIDX] <> nil) and

                             (IntersectRect(ISectRect,

                                            EnemyBullets[iCount].CollisionBox,

                                            Ships[PLAYERSHIPIDX].CollisionBox))then

                          begin

                            {indeed, the enemy bullet has collided with the

                             player’s ship, destroying it. we should create an

                             explosion and destroy both the enemy bullet and the

                             player’s ship}

           StartExplosion(Ships[PLAYERSHIPIDX].XPos,

                                           Ships[PLAYERSHIPIDX].YPos);

                            EnemyBullets[iCount].Living := FALSE;

                            Ships[PLAYERSHIPIDX].Living := FALSE;

                            {we want the player to see the explosion, so we must

                             wait an appropriate amount of time before changing

                             the game state}

                            TimeAccum := timeGetTime;

                          end;

                          {if the enemy bullet is still alive, draw it; otherwise

                           destroy it}

                          if EnemyBullets[iCount].Living then

                            EnemyBullets[iCount].Draw

                          else

                          begin

                           EnemyBullets[iCount].Free;

                           EnemyBullets[iCount] := nil;

                          end;

                        end;

                        {game state AI.  again, this is very simplistic, but if the

                         user has destroyed a certain amount of enemy ships

                         (based on the level), the level increases. as the level

                         increases, the number of enemy ships that the user is

                         required to destroy also increases. this results in the

                         game getting harder and harder as the player progresses}

                        if NumShipsToNextLevel <= 0 then

                        begin

                          Inc(Level);

                          NumShipsToNextLevel := 5 * Level;

                          ResetAll;

                          {since we’ve increased the game level, we should go to

                           intermission to inform the player of what has happened}

                          GameState := gsIntermission;

                          Break;

                        end;

                      end;

                      {update and draw any explosions}

                      for icount := 0 to NUMEXPLOSIONS do

                        if Explosions[iCount] <> nil then

                        begin

                          {although explosions don’t really move, they

                           animate, and in the interest of similarity,

                           they have a Move method like other sprites}

                          Explosions[iCount].Move;

                          {explosions are short lived. if the explosion is

                           still living (and thus animating), draw it; otherwise

                           destroy it}

                          if Explosions[iCount].Living then

                            Explosions[iCount].Draw

                          else

                          begin

                            Explosions[iCount].Free;

                            Explosions[iCount] := nil;

                          end;

                        end;

                      {now move all of the ships}

                      for iCount := 0 to NUMSHIPS do

                        if Ships[iCount] <> nil then

                        begin

                          {some enemy specific code...}

                          if iCount > PLAYERSHIPIDX then

                          begin

                            {we should also detect if an enemy ship has collided

                             with the player’s ship...}

                            if (Ships[PLAYERSHIPIDX] <> nil) and

                               (IntersectRect(ISectRect,Ships[iCount].CollisionBox,

                                Ships[PLAYERSHIPIDX].CollisionBox)) then

                            begin

                              {if indeed the player and enemy ships have collided,

                               we should start two explosions and kill both ships}

                              StartExplosion(Ships[PLAYERSHIPIDX].XPos,

                                             Ships[PLAYERSHIPIDX].YPos);

                              StartExplosion(Ships[iCount].XPos,

                                             Ships[iCount].YPos);

                              Ships[iCount].Living := FALSE;

                              Ships[PLAYERSHIPIDX].Living := FALSE;

                              {this kill should count towards the total amount

                               required to advance a level}

                              Dec(NumShipsToNextLevel);

                              {as before, we need to determine if the player has

                               increased to the next level.  however, the player

                               has just died, so we don’t want to immediately

                               change the game state}

                              if NumShipsToNextLevel <= 0 then

                              begin

                                Inc(Level);

                                NumShipsToNextLevel := 5 * Level;

                              end;

                              {we want the player to see the explosion, so we must

                               wait an appropriate amount of time before changing

                               the game state}

                              TimeAccum := timeGetTime;

                            end

                            else

                            begin

                              {there was no collision with the player’s ship,

                               so perform some very simplistic AI. this will

                               randomly change the horizontal velocity of the

                               ship, and randomly fire a bullet at the player}

                              if Random(1000) < 10 then

                                Ships[iCount].XVel := 0-Ships[iCount].XVel;

                              if Random(1000) < 50 then

                                StartBullet(Ships[iCount].XPos+

                                            (Ships[iCount].FrameWidth div 2)-5,

                                            Ships[iCount].YPos, 0, 8, FALSE);

                            end;

                          end;

                          {update the ship’s position based on velocity}

                          Ships[iCount].Move;

                          {if the ship is still living, draw it, otherwise

                           destroy it}

                          if Ships[iCount].Living then

                            Ships[iCount].Draw

                          else

                          begin

                            Ships[iCount].Free;

                            Ships[iCount] := nil;

                          end;

                        end;

                        {display the number of ships left}

                        for iCount := 0 to LivesLeft-1 do

                        begin

                          SourceRect := Rect(392, 48, 422, 70);

                          FBackBuffer.BltFast(500+(iCount*30)+5, 455,

                                              FGraphicsImage, SourceRect,

                                              DDBLTFAST_SRCCOLORKEY or

                                              DDBLTFAST_WAIT);

                        end;

                        {display the score}

                        WorkCanvas := TCanvas.Create;

                        FBackBuffer.GetDC(SurfaceDC);

                        try

                          WorkCanvas.Handle := SurfaceDC;

                          WorkCanvas.Font.Size := 10;

                          WorkCanvas.Font.Name := Arial’;

                          WorkCanvas.Font.Color := clWhite;

                          WorkCanvas.Brush.Style := bsClear;

                          WorkCanvas.TextOut(5, 465, Score: +IntToStr(Score));

                        finally

                          WorkCanvas.Handle := 0;

                          FBackBuffer.ReleaseDC(SurfaceDC);

                          WorkCanvas.Free;

                        end;

                        {if our timing variable has been initialized, the player

                         has been killed. thus, we check to see if an appropriate

                         amount of time has passed before resetting certain

                         variables and reinitializing the player. this allows

                         players to see the ship exploding when it collides with

                         a missile or an enemy ship}

                        if (TimeAccum > -1) and (timeGetTime-TimeAccum>2000) then

                        begin

                          PlayerShipKilled;

                        end;

                    end;

      end;

    end;

    { —> Deletable Events <— }

    ///

    { — as a matter of convenience this framework will terminate when the —

      — Escape key is pressed, but this should probably be deleted and    —

      — replaced with your own terminate methods                          — }

    procedure TfrmDXAppMain.FormKeyDown(Sender: TObject; var Key: Word;

      Shift: TShiftState);

    begin

      if Key = VK_ESCAPE then

        Close;

    end;

    ///

    { *—->>  END BASELINE APPLICATION CODE <<—-* }

    { TSprite }

    procedure TSprite.Draw;

    var

      SourceRect: TRect;

      DestY: Integer;

    begin

      {if it is time to increment the current frame...}

      if timeGetTime - CurAnimTime > AnimThreshold then

      begin

        {increment the frame of animation}

        Inc(CurFrame);

        {roll it over if necessary}

        if CurFrame>=NumFrames then

          CurFrame := 0;

        {record the time}

        CurAnimTime := timeGetTime;

      end;

      {draw the next animation frame into the backbuffer}

      SourceRect := Rect(XFrameStartOffset+(FrameWidth*CurFrame),

                         YFrameStartOffset, XFrameStartOffset+

                         (FrameWidth*CurFrame)+FrameWidth,

                         YFrameStartOffset+FrameHeight);

      {if the sprite is fully on screen...}

      if (YPos >= 0) and (YPos <= DXHEIGHT-FrameHeight) then

        {draw it to the screen}

        FBackBuffer.BltFast(XPos, YPos, FGraphicsImage, SourceRect,

                            DDBLTFAST_SRCCOLORKEY OR DDBLTFAST_WAIT)

      else

      begin

        {...otherwise, it is partially hidden at the top or bottom, so determine

         the rectangular portion of the sprite that is visible}

        if YPos < 0 then

        begin

          DestY := 0;

          SourceRect := Rect(SourceRect.Left, SourceRect.Top-YPos,

                             SourceRect.Right, SourceRect.Bottom);

        end

        else

        begin

          DestY := YPos;

          SourceRect := Rect(SourceRect.Left, SourceRect.Top, SourceRect.Right,

                             SourceRect.Top+(DXHEIGHT-YPos));

        end;

        {copy the image portion to the screen}

        FBackBuffer.BltFast(XPos, DestY, FGraphicsImage, SourceRect,

                            DDBLTFAST_SRCCOLORKEY OR DDBLTFAST_WAIT);

      end;

    end;

    procedure TSprite.Move;

    begin

      {move the sprite according to its horizontal velocity}

      XPos := XPos + XVel;

      YPos := YPos + YVel;

      {clip the sprite to the boundaries of the screen}

      if XPos > DXWIDTH-FrameWidth then

      begin

        XPos := DXWIDTH-FrameWidth;

        XVel := 0-XVel;

      end;

      if XPos < 0 then

      begin

        XPos := 0;

        XVel := 0-XVel;

      end;

      {update its bounding box}

      CollisionBox := Rect(XPos+BoundBox.Left, YPos+BoundBox.Top,

                           XPos+BoundBox.Right, YPos+BoundBox.Bottom);

      {if the sprite has gone off the top or bottom of the screen, it is dead}

      if (YPos < 0-FrameHeight) or (YPos > DXHEIGHT) then

        Living := FALSE;

    end;

    { TExplosion }

    procedure TExplosion.Move;

    begin

      {explosions don’t actually move, but an explosion’s lifespan is over when

       it has played all of its frames}

      if CurFrame+1 >= NumFrames then

        Living := FALSE;

    end;

    { TStar }

    procedure TStar.Move;

    begin

      {stars only move vertically, so update their position}

      YPos := YPos + YVel;

      {clip them to the boundaries of the screen, wrapping around if necessary}

      if YPos < 0 then

        YPos := DXHEIGHT;

      if YPos >= DXHEIGHT then

        YPos := 0;

    end;

    procedure TStar.Draw;

    var

      SurfaceDesc: TDDSurfaceDesc2;

    begin

      {lock the backbuffer}

      SurfaceDesc.dwSize := SizeOf(TDDSurfaceDesc2);

      FBackBuffer.Lock(nil, SurfaceDesc, DDLOCK_SURFACEMEMORYPTR or DDLOCK_WAIT,0);

      {color the appropriate pixel in the backbuffer using the star’s color. this

       is a very fast way to plot individual pixels}

      try

        Byte(Pointer(Longword(SurfaceDesc.lpSurface)+

                    (YPos*SurfaceDesc.lPitch+XPos))^) := Color;

      finally

        {the buffer must be unlocked before it can be flipped}

        FBackBuffer.Unlock(nil);

      end;

    end;

    procedure TfrmDXAppMain.NewGame;

    begin

      {initialize game variables}

      Level := 1;

      Score := 0;

      LivesLeft := 3;

      NumShipsToNextLevel := 5;

      {reset all sprites}

      ResetAll;

      {go to intermission}

      GameState := gsIntermission;

    end;

    procedure TfrmDXAppMain.StartBullet(XPos, YPos, XVel, YVel: Integer;

      PlayerBullet: Boolean);

    var

      iCount: Integer;

    begin

      {if we’re starting a player bullet}

      if PlayerBullet then

      begin

        {find an open slot in the bullets array}

        for iCount := 0 to NUMBULLETS do

          if PlayerBullets[iCount] = nil then

          begin

            {create a new bullet}

            PlayerBullets[iCount] := TSprite.Create;

            {initialize the bullet sprite’s properties}

            PlayerBullets[iCount].NumFrames := 1;

            PlayerBullets[iCount].CurFrame := 0;

            PlayerBullets[iCount].FrameWidth := 10;

            PlayerBullets[iCount].FrameHeight := 17;

            PlayerBullets[iCount].XFrameStartOffset := 372;

            PlayerBullets[iCount].YFrameStartOffset := 48;

            PlayerBullets[iCount].BoundBox := Rect(0, 0, 10, 17);

            PlayerBullets[iCount].Living := TRUE;

            PlayerBullets[iCount].XPos := XPos;

            PlayerBullets[iCount].YPos := YPos;

            PlayerBullets[iCount].XVel := XVel;

            PlayerBullets[iCount].YVel := YVel;

            PlayerBullets[iCount].CurAnimTime := timeGetTime;

            PlayerBullets[iCount].AnimThreshold := 10000;

            {this bullet has been created, we can exit}

            Break;

          end;

      end

      else

      begin

        {otherwise, search for an open slot in the enemy bullets array}

        for iCount := 0 to NUMBULLETS do

          if EnemyBullets[iCount] = nil then

          begin

            {create the bullet}

            EnemyBullets[iCount] := TSprite.Create;

            {initialize the bullet sprite’s properties}

            EnemyBullets[iCount].NumFrames := 1;

            EnemyBullets[iCount].CurFrame := 0;

            EnemyBullets[iCount].FrameWidth := 10;

            EnemyBullets[iCount].FrameHeight := 10;

            EnemyBullets[iCount].XFrameStartOffset := 382;

            EnemyBullets[iCount].YFrameStartOffset := 51;

            EnemyBullets[iCount].BoundBox := Rect(0, 0, 10, 10);

            EnemyBullets[iCount].Living := TRUE;

            EnemyBullets[iCount].XPos := XPos;

            EnemyBullets[iCount].YPos := YPos;

            EnemyBullets[iCount].XVel := XVel;

            EnemyBullets[iCount].YVel := YVel;

            EnemyBullets[iCount].CurAnimTime := timeGetTime;

            EnemyBullets[iCount].AnimThreshold := 10000;

            {this bullet has been created, we can exit}

            Break;

          end;

      end;

    end;

    procedure TfrmDXAppMain.StartEnemy;

    var

      iCount: Integer;

    begin

      {search for an open slot in the ships array. notice that the player’s ship

       is always located at the first index, so we skip it}

      for iCount := PLAYERSHIPIDX+1 to NUMSHIPS do

        if Ships[iCount] = nil then

        begin

          {create the sprite}

          Ships[iCount] := TSprite.Create;

          {initialize the sprite properties}

          Ships[iCount].NumFrames := 3;

          Ships[iCount].CurFrame := 0;

          Ships[iCount].FrameWidth := 62;

          Ships[iCount].FrameHeight := 36;

          Ships[iCount].XFrameStartOffset := 186;

          Ships[iCount].YFrameStartOffset := 0;

          Ships[iCount].BoundBox := Rect(3, 3, 59, 33);

          Ships[iCount].Living := TRUE;

          Ships[iCount].XPos := Random(DXWIDTH - 61)+1;

          Ships[iCount].YPos := -36;

          Ships[iCount].XVel := Random(6)-3;

          Ships[iCount].YVel := Random(4)+1;

          Ships[iCount].CurAnimTime := timeGetTime;

          Ships[iCount].AnimThreshold := 100;

          {the sprite was created, we can exit}

          Break;

        end;

    end;

    procedure TfrmDXAppMain.StartExplosion(XPos, YPos: Integer);

    var

      iCount: Integer;

    begin

      {search for an open slot in the explosions array}

      for iCount := 0 to NUMEXPLOSIONS do

        if Explosions[iCount] = nil then

        begin

          {create the explosion sprite}

          Explosions[iCount] := TExplosion.Create;

          {initialize the sprite properties}

          Explosions[iCount].NumFrames := 6;

          Explosions[iCount].CurFrame := 0;

          Explosions[iCount].FrameWidth := 62;

          Explosions[iCount].FrameHeight := 47;

          Explosions[iCount].XFrameStartOffset := 0;

          Explosions[iCount].YFrameStartOffset := 48;

          Explosions[iCount].Living := TRUE;

          Explosions[iCount].XPos := XPos;

          Explosions[iCount].YPos := YPos;

          Explosions[iCount].CurAnimTime := timeGetTime;

          Explosions[iCount].AnimThreshold := 100;

     {the sprite was created, we can exit}

          Break;

        end;

    end;

    procedure TfrmDXAppMain.ResetAll;

    var

      iCount: Integer;

    begin

      {free all of the ships}

      for iCount := 0 to NUMSHIPS do

      begin

        Ships[iCount].Free;

        Ships[iCount] := nil;

      end;

      {free all of the bullets}

      for iCount := 0 to NUMBULLETS do

      begin

        PlayerBullets[iCount].Free;

        PlayerBullets[iCount] := nil;

        EnemyBullets[iCount].Free;

        EnemyBullets[iCount] := nil;

      end;

      {free all of the explosions}

      for iCount := 0 to NUMEXPLOSIONS do

      begin

        Explosions[iCount].Free;

        Explosions[iCount] := nil;

      end;

      {reset the pause timer}

      TimeAccum := -1;

    end;

    procedure TfrmDXAppMain.ResetPlayer;

    begin

      {make sure the player ship is created}

      if not Assigned(Ships[PLAYERSHIPIDX]) then

        Ships[PLAYERSHIPIDX] := TSprite.Create;

      {initialize the player ship sprite properties}

      Ships[PLAYERSHIPIDX].NumFrames := 3;

      Ships[PLAYERSHIPIDX].CurFrame := 0;

      Ships[PLAYERSHIPIDX].FrameWidth := 62;

      Ships[PLAYERSHIPIDX].FrameHeight := 48;

      Ships[PLAYERSHIPIDX].XFrameStartOffset := 0;

      Ships[PLAYERSHIPIDX].YFrameStartOffset := 0;

      Ships[PLAYERSHIPIDX].BoundBox := Rect(3, 3, 59, 45);

      Ships[PLAYERSHIPIDX].Living := TRUE;

      Ships[PLAYERSHIPIDX].XPos := DXWIDTH div 2;

      Ships[PLAYERSHIPIDX].YPos := DXHEIGHT-Ships[PLAYERSHIPIDX].FrameHeight-20;

      Ships[PLAYERSHIPIDX].XVel := 0;

      Ships[PLAYERSHIPIDX].YVel := 0;

      Ships[PLAYERSHIPIDX].CurAnimTime := timeGetTime;

      Ships[PLAYERSHIPIDX].AnimThreshold := 100;

      {indicate that the player can now shoot}

      CanShoot := TRUE;

    end;

    procedure TfrmDXAppMain.PlayerShipKilled;

    begin

      {decrement the number of lives}

      Dec(LivesLeft);

      {reset all of the sprites}

      ResetAll;

      {if the player has no more lives left, go to demo; otherwise go to

       intermission}

      if LivesLeft < 0 then

        GameState := gsDemo

      else

        GameState := gsIntermission;

    end;

    end.
### Summary In this chapter, we discussed the most important and most basic issues involved in designing game application software. When designing the architecture of game application software, it is important to keep these points in mind: • Every game, no matter how complex, can be broken down into three constituent parts: graphics, sound, and user input. • The specific architecture of a game will vary widely depending on the type of game and its inherent functionality. Even so, most games usually perform similar tasks in a similar order. From a high-level standpoint, game software can be thought of in terms of six elements that perform necessary tasks: Initialization, Game Introduction, Game Startup, the Game Loop, Game Ending, and Shutdown and Exit. • The game loop is usually the focal point of a game application, and is where all of the action takes place. It processes a series of instructions over and over as quickly as possible to produce the on-screen images that reflect the current state of the game. This processing continues with or without user interaction. It can be implemented in a variety of ways, and must perform several different tasks as rapidly as possible. Technically speaking, a game loop is responsible for three things: retrieving input, updating game objects, and rendering a new frame of animation. Additionally, it must perform collision detection, enemy AI, and other tasks commensurate with the specific type of game. • Games can be examined from the standpoint of their current mode of execution, or state. A state based gaming architecture allows the game to be controlled in a more logical and ordered manner. The state of a game can flow from one state to another as appropriate, changing between logical states and ultimately creating a circuit from game beginning through game play to game end and back again. In general, games can be considered to execute in five different states: Demo, Playing, Intermission, Paused, and Game Over.
Previous | Table of Contents | Next

Products | Contact Us | About Us | Privacy | Ad Info | Home Use of this site is subject to certain Terms & Conditions, Copyright © 1996-2000 EarthWeb Inc. All rights reserved. Reproduction whole or in part in any form or medium without express written permission of EarthWeb is prohibited. Read EarthWeb's privacy statement.