Exclusion Zones

Introduction

In games there are objects such as walls, platforms, towers and maze hedges that must not be encroached by the player or enemies. With two rectangular shapes the intersect method of TRect provides an efficient way of testing whether or not a move should be allowed. With more complicated shapes we offer these possibilities.

  1. Represent the excluded zone as an array of TRect and test for overlap of the moving object with each stationary rectangle in the array. We demonstrate this method with the start of a maze game on the previous page.
  2. Test the foremost part of the advancing object to see if the attempted move would land it upon a colour of an excluded zone. We demonstrate this approach in GetPixelDemo below.
  3. Make up the excluded area as a path and test for inclusion of selected pixels of the moving object. We demonstrate this possibility in our second example.
  4. Divide the GameView into a grid and store each cell in a two-dimensional array. If each cell has the same size as the moving object then the code becomes rather similar to that of a console game. Our third example demonstrates a small grid.
  5. Probably the trickiest but most obvious method is to use many nested if and case statements such as (just for negotiating the two gaps below the top row in the maze used below):
    case Direction of
      Down: begin
              if PlayerY + PLAYER_SIZE < WALL_WIDTH + Gap then // top row
                begin
                  if (PlayerY + PLAYER_SIZE + MoveDist < WALL_WIDTH + Gap) then
                    PlayerY += MoveDist //cannot hit lower wall
                  else // Must be directly above first gap ...
                    if ((PlayerY + PLAYER_SIZE + MoveDist >= WALL_WIDTH + Gap) and
                        (PlayerX > GameView.Width div 3) and
                        ((PlayerX + PLAYER_SIZE) < (GameView.Width div 3 + Gap))) or
                       // or directly above right gap
                       (PlayerX > (GameView.Width - WALL_WIDTH - Gap))
                  then
                    PlayerY += MoveDist;    
    

GetPixel Demo

Use the wasd keys to navigate through part of a maze. If GetPixelDemo does not run in your current browser after clicking on the display, please try another (such as Chrome). If you see no display at school, the security system might have blocked it. You can try instead this direct link to the program running on its own page.

GetPixelDemo

The Code

unit Unit1;

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics, System.Colors;
type
  TDirection = (Up, Down, Left, Right);
  TCanvasProject = class(TW3CustomGameApplication)
  private
    const PLAYER_SIZE = 5;
    const WALL_WIDTH = 4;
    MoveAttempt: Boolean;
    Direction: TDirection;
    MoveDist = 2;
    Gap, Moves, PlayerX, PlayerY: integer;
  protected
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
    procedure KeyDownEvent(mCode: integer);
  end;

implementation

procedure TCanvasProject.KeyDownEvent(mCode: integer);
begin
  MoveAttempt := True;
  case mCode of
    27: Application.Terminate;
    37, 65: Direction := Left;
    38, 87: Direction := Up;
    39, 68: Direction := Right;
    40, 83: Direction := Down;
    0: MoveAttempt := False; //Initialization
  end;
end;

procedure TCanvasProject.ApplicationStarting;
begin
  inherited;
   asm
    window.onkeydown=function(e)
    {
    TCanvasProject.KeyDownEvent(Self,e.keyCode);
    }
  end;
  KeyDownEvent(0);
  Gap := 3 * PLAYER_SIZE;
  PlayerX := 0;
  PlayerY := WALL_WIDTH;
  GameView.Delay := 20;
  GameView.StartSession(false);
end;

procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
var
  PixelData1, PixelData2: TW3RGBA;
begin
  // Clear background
  Canvas.FillStyle := 'lightgray';
  Canvas.FillRectF(0, 0, GameView.Width, GameView.Height);
  Canvas.FillStyle := 'rgb(255, 110, 110)';

  //Draw top, bottom, left and right boundaries
  Canvas.FillBounds(0, 0, GameView.Width, WALL_WIDTH);
  Canvas.FillBounds(0, GameView.Height - WALL_WIDTH,
                   GameView.Width - WALL_WIDTH - 3 * PLAYER_SIZE, GameView.Height);
  Canvas.FillBounds(0, WALL_WIDTH + 3 * PLAYER_SIZE, WALL_WIDTH, GameView.Height - WALL_WIDTH);
  Canvas.FillBounds(GameView.Width - WALL_WIDTH, WALL_WIDTH,
                    GameView.Width, GameView.Height - WALL_WIDTH);
  // Draw 2 horizontal walls with two gaps
  Canvas.FillBounds(WALL_WIDTH, WALL_WIDTH + Gap, GameView.Width div 3, 2 * WALL_WIDTH + Gap);
  Canvas.FillBounds(GameView.Width div 3 + Gap, WALL_WIDTH + Gap,
                    GameView.Width - Gap - WALL_WIDTH, 2 * WALL_WIDTH + Gap);
  Canvas.FillBounds(WALL_WIDTH + Gap, 2 * (WALL_WIDTH + Gap), GameView.Width div 2,
                    3 * WALL_WIDTH + 2 * Gap);
  Canvas.FillBounds(GameView.Width div 2 + Gap, 2 * (WALL_WIDTH + Gap),
                    GameView.Width - WALL_WIDTH, 3 * WALL_WIDTH + 2 * Gap);
  // Draw vertical wall with one gap
  Canvas.FillBounds(GameView.Width div 2 - WALL_WIDTH, 2 * WALL_WIDTH + Gap,
                    GameView.Width div 2, GameView.Height - WALL_WIDTH - Gap);
  if MoveAttempt = True then  //Key pressed
    begin
      inc(Moves);
      // Test each approaching vertex for collision.
      case Direction of
        Up: begin
              PixelData1 := Canvas.GetImageData(PlayerX, PlayerY - MoveDist, 1, 1).GetPixel(0, 0);
              PixelData2 := Canvas.GetImageData(PlayerX + PLAYER_SIZE - 1, PlayerY - MoveDist, 1, 1).GetPixel(0, 0);
              if (PixelData1.R <> 255) and (PixelData2.R <> 255) then // Only the walls have max red component of colour
                PlayerY -= MoveDist;
            end;
        Left: begin
                PixelData1 := Canvas.GetImageData(PlayerX - MoveDist, PlayerY, 1, 1).GetPixel(0, 0);
                PixelData2 := Canvas.GetImageData(PlayerX - MoveDist, PlayerY + PLAYER_SIZE - 1, 1, 1).GetPixel(0, 0);
                if (PixelData1.R <> 255) and (PixelData2.R <> 255) then
                  PlayerX -= MoveDist;
              end;
        Right: begin
                 PixelData1 := Canvas.GetImageData(PlayerX + MoveDist + PLAYER_SIZE, PlayerY, 1, 1).GetPixel(0, 0);
                 PixelData2 := Canvas.GetImageData(PlayerX + MoveDist + PLAYER_SIZE - 1, PlayerY + PLAYER_SIZE - 1, 1, 1).GetPixel(0, 0);
                 if (PixelData1.R <> 255) and (PixelData2.R <> 255) then
                   PlayerX += MoveDist;
               end;
        Down: begin
                PixelData1 := Canvas.GetImageData(PlayerX, PlayerY + MoveDist + PLAYER_SIZE, 1, 1).GetPixel(0, 0);
                PixelData2 := Canvas.GetImageData(PlayerX + PLAYER_SIZE - 1, PlayerY + MoveDist + PLAYER_SIZE, 1, 1).GetPixel(0, 0);
                if (PixelData1.R <> 255) and (PixelData2.R <> 255) then
                  PlayerY += MoveDist;
              end;
      end; //end of case
    end; //end of if MoveAttempt
  Canvas.FillStyle := 'blue';
  Canvas.FillRectF(PlayerX, PlayerY, PLAYER_SIZE, PLAYER_SIZE);
  if PlayerY > GameView.Height - WALL_WIDTH then
    begin
      ShowMessage('Moves: ' + IntToStr(Moves));
      Application.Terminate;
    end;
  MoveAttempt := False;
end;

end.
You might find it easier to draw thick lines instead of rectangles. For example, this code snippet draws the top line:
Canvas.StrokeStyle := 'rgb(255, 110, 110)';
Canvas.LineWidth := WALL_WIDTH;
Canvas.BeginPath;
Canvas.MoveToF(0, 0);
Canvas.LineToF(GameView.Width, 0);
Canvas.Stroke;

For an impressive implementation of this technique, see Matthew's MazeOnCube.

Excluded Path Demo

Use the wasd keys to attempt unsuccessfully to enter the jagged exclusion zone outlined in red. If ExcludedPathDemo does not run in your current browser after clicking on the display, please try another (such as Chrome). If you see no display at school, the security system might have blocked it. You can try instead this direct link to the program running on its own page.

ExcludedPathDemo

The Code

unit Unit1;

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics;
type
  TDirection = (Up, Down, Left, Right);
  TCanvasProject = class(TW3CustomGameApplication)
  private
    const PLAYER_SIZE = 5;
    MoveAttempt: Boolean;
    Direction: TDirection;
    MoveDist = 1;
    PlayerX, PlayerY: integer;
  protected
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
    procedure KeyDownEvent(mCode: integer);
  end;

implementation

procedure TCanvasProject.KeyDownEvent(mCode: integer);
begin
  MoveAttempt := True;
  case mCode of
    27: Application.Terminate;
    37, 65: Direction := Left;
    38, 87: Direction := Up;
    39, 68: Direction := Right;
    40, 83: Direction := Down;
    0: MoveAttempt := False; //Initialization
  end;
end;

procedure TCanvasProject.ApplicationStarting;
begin
  inherited;
   asm
    window.onkeydown=function(e)
    {
    TCanvasProject.KeyDownEvent(Self,e.keyCode);
    }
  end;
  KeyDownEvent(0);
  PlayerX := 0;
  PlayerY := 0;
  GameView.Delay := 20;
  GameView.StartSession(false);
end;

procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
begin
  function CheckPerimeter(Left, Top: integer): Boolean;
  var
    Collision: Boolean = False;
  begin
    for var x := Left to Left + PLAYER_SIZE do
      if Canvas.IsPointInPathF(x, Top) or Canvas.IsPointInPathF(x, Top + PLAYER_SIZE) then
        exit True;
    for var y := Top to Top + PLAYER_SIZE do
      if Canvas.IsPointInPathF(Left, y) or Canvas.IsPointInPathF(Left + PLAYER_SIZE, y)  then
        exit True;
    exit False;
  end;
  // Clear background
  Canvas.FillStyle := 'lightgray';
  Canvas.FillRectF(0, 0, GameView.Width, GameView.Height);
  Canvas.FillStyle := 'rgb(255, 110, 110)';
  // Draw jagged path
  Canvas.BeginPath;
  Canvas.MoveToF(20, 20);
  Canvas.LineToF(30, 40);
  Canvas.LineToF(40, 20);
  Canvas.LineToF(50, 80);
  Canvas.LineToF(60, 10);
  Canvas.LineToF(65, 100);
  Canvas.LineToF(65, 150);
  Canvas.LineToF(30, 100);
  Canvas.ClosePath;
  Canvas.StrokeStyle := 'red';
  Canvas.Stroke;

  if MoveAttempt = True then  //Key pressed
    begin
      case Direction of
        Up: if not CheckPerimeter(PlayerX, PlayerY - MoveDist) then
              PlayerY -= MoveDist;
        Left: if not CheckPerimeter(PlayerX - MoveDist, PlayerY) then
                PlayerX -= MoveDist;
        Right: if not CheckPerimeter(PlayerX + MoveDist, PlayerY) then
                 PlayerX += MoveDist;
        Down: if not CheckPerimeter(PlayerX, PlayerY + MoveDist ) then
                PlayerY += MoveDist;
      end;
    end;
  Canvas.FillStyle := 'blue';
  Canvas.FillRectF(PlayerX, PlayerY, PLAYER_SIZE, PLAYER_SIZE);
  MoveAttempt := False ;
end;

end.

You can extend the path to include other enclosed sections that are tested with the same code. Insert this code after Canvas.ClosePath to add an excluded triangle.

Canvas.MoveToF(125, 30);
Canvas.LineToF(130, 60);
Canvas.LineToF(100, 90);
Canvas.ClosePath;    

Using a Grid

Use the wasd keys to navigate through the tiny maze. If GridDemo does not run in your current browser after clicking on the display, please try another (such as Chrome). If you see no display at school, the security system might have blocked it. You can try instead this direct link to the program running on its own page.

GridDemo

The Code

unit Unit1;

interface

uses
  System.Types, SmartCL.System, SmartCL.Components, SmartCL.Application,
  SmartCL.Game, SmartCL.GameApp, SmartCL.Graphics;
const WIDTH = 20;
const HEIGHT = 10;

type
  TDirection = (Up, Down, Left, Right);
  TCanvasProject = class(TW3CustomGameApplication)
  private
    const CELL_SIZE = 10;
    Grid: array[1..HEIGHT, 1..WIDTH] of integer =
    [
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
      [0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1],
      [1,0,1,1,1,0,0,0,0,1,0,0,0,0,0,0,0,1,0,1],
      [1,0,1,0,1,1,0,0,0,1,0,0,0,0,0,1,0,0,0,1],
      [1,0,0,0,0,1,0,0,0,1,1,1,1,1,1,1,0,1,1,1],
      [1,0,1,0,0,1,1,1,0,1,0,0,0,0,0,0,0,0,0,1],
      [1,0,1,0,0,0,0,0,0,1,0,0,0,1,1,1,0,0,0,1],
      [1,0,1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,0,0,1],
      [1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0],
      [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
    ];
    MoveAttempt: Boolean;
    Direction: TDirection;
    PlayerX, PlayerY: integer;
  protected
    procedure ApplicationStarting; override;
    procedure ApplicationClosing; override;
    procedure PaintView(Canvas: TW3Canvas); override;
    procedure KeyDownEvent(mCode: integer);
  end;

implementation

procedure TCanvasProject.KeyDownEvent(mCode: integer);
begin
  MoveAttempt := True;
  case mCode of
    27: Application.Terminate;
    37, 65: Direction := Left;
    38, 87: Direction := Up;
    39, 68: Direction := Right;
    40, 83: Direction := Down;
    0: MoveAttempt := False; //Initialization
  end;
end;

procedure TCanvasProject.ApplicationStarting;
begin
  inherited;
   asm
    window.onkeydown=function(e)
    {
    TCanvasProject.KeyDownEvent(Self,e.keyCode);
    }
  end;
  KeyDownEvent(0);
  PlayerX := 1;
  PlayerY := 2;
  GameView.Delay := 20;
  GameView.StartSession(False);
end;

procedure TCanvasProject.ApplicationClosing;
begin
  GameView.EndSession;
  inherited;
end;

procedure TCanvasProject.PaintView(Canvas: TW3Canvas);
begin
  procedure FillCell(x, y: integer);
  begin
    Canvas.FillStyle := 'red';
    if Grid[y, x] = 1 then
      Canvas.FillRectF((x - 1) * CELL_SIZE, (y - 1) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
  end;

  // Clear background
  Canvas.FillStyle := 'lightgray';
  Canvas.FillRectF(0, 0, GameView.Width, GameView.Height);
  Canvas.FillStyle := 'rgb(255, 110, 110)';
  for var X := 1 to WIDTH do
    for var Y := 1 to HEIGHT do
      FillCell(X, Y);

  if MoveAttempt = True then  //Key pressed
    begin
      case Direction of
        Up: if Grid[PlayerY -1, PlayerX] <> 1 then
              PlayerY -= 1;
        Left: if Grid[PlayerY, PlayerX - 1] <> 1 then
                PlayerX -= 1;
        Right: if Grid[PlayerY, PlayerX + 1] <> 1 then
                 PlayerX += 1;
        Down: if Grid[PlayerY +1, PlayerX ] <> 1 then
                PlayerY += 1;
      end;
    end;
  Canvas.FillStyle := 'blue';
  Canvas.FillRectF((PlayerX - 1) * CELL_SIZE, (PlayerY - 1) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
  MoveAttempt := False;
end;

end.
Programming - a skill for life!

Useful for games, with drawing routines (including transforms and text fonts), images, sprites and WebGL 3D graphics. Now includes Box2D physics and rendering by Pixi.js.