Well, what ends up happening is that the sprite wraps around from the right edge onto the left edge. This is a very bad thing to happen, because it destroys it looks ugly, and it can be confusing for the user of the program. Another case is when the sprite falls off of the bottom of the screen. Here, it goes on to overwrite portions of memory... this is really bad. One thing that can become of this is that some of your data is corrupted. Another WORSE thing that can happen is that some of your code gets corrupted. This could (and probably will) lead to a crash.
Clipping the top is easily done. All that we have to do is skip over the sprite data that we don't need. Then we adjust the sprite height to compensate for those lines lost, and move the Y value down to a position that is inside of the clipping area. Here is the code prior to the modification:
The complete code is below, but most of it turns out to be the silly initialization code. This code is very fast, and the inner loop is almost as fast as the non-clipped version of the code. Here it is:
Wow. Yet another installment on the topic of sprites. Here, we will discuss what is involved in the clipping of sprites. All that we will worry about clipping is the DrawSprite routine; it doesn't make much sense to clip the GetSprite routine. If you don't understand what a clipping DrawSprite routine is good for, then you must consider the edges of the viewable area of the screen. What happens to a sprite that is half way off of the right edge of the screen for example?
So now that we are ready and determined to get the job done, how do we do it? Well, it turns out that clipping these rectangular sprites is really pretty easy. First, we will start by clipping the top and bottom of the sprite, and then we will move on to clip the left and right.
PROCEDURE DrawSprite(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
Index, XI, YI : INTEGER;
BEGIN
Index := 0;
FOR YI := 0 TO Sprite.Height-1 DO
FOR XI := 0 TO Sprite.Width-1 DO
BEGIN
SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
INC(Index);
END;
END;
Here it is with the changes to make it clip the top:
PROCEDURE DrawSpriteC(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
Height,
Index, XI, YI : INTEGER;
BEGIN
Index := 0;
Height := Sprite.Height; { Make a local copy of this }
IF Screen.Clip.Y1 > Y THEN
BEGIN
XI := Screen.Clip.Y1-Y; { Calculate the difference }
DEC(Height, XI); { Lower the height by the diff }
INC(Y, XI); { Increase the Y by the diff }
INC(Index, XI*Sprite.Width); { Increase the pointer to the }
END; { sprite data appropriately }
FOR YI := 0 TO Height-1 DO
FOR XI := 0 TO Sprite.Width-1 DO
...
As you can see above, although this added code, it did not add code to the inner loop of the draw routine. In fact, it did not change the looping code at all except the value which it loops to. The bottom portion of the sprite is even easier to clip than the top. To clip the top of the sprite, we had to modify three variables. To clip the bottom, we only have to change one, the Height variable.
Height := Sprite.Height; { Make a local copy of this }
IF Screen.Clip.Y2 < Y+Sprite.Height-1 THEN { Clip the bottom }
DEC(Height, Y+Sprite.Height-1-Screen.Clip.Y2);
IF Screen.Clip.Y1 > Y THEN
Believe it or not, that is all that there is! Only two lines needed to be added to clip the bottom. Whoa! Is it possible that we are already half way done? Amazing! This isn't as hard as we thought. Now if we could just get the left and right sides to clip...
To clip the sides of the sprite, we do have to make a small change to the loop of our routine. Luckily, it doesn't change the inner loop of the routine, but it will slow us down slightly. Here is the new inner loop:
FOR YI := 0 TO Height-1 DO
BEGIN
FOR XI := 0 TO Width-1 DO
BEGIN
SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
INC(Index);
END;
INC(Index, SkippedPixels);
END;
What this does, is to skip over a number of pixels (in the sprite) at the end of the scanline. If we needed to clip four pixels off the right edge, for example, we would set SkippedPixels to 4, and Width to Sprite.Width-4. All this variable does is specify how many pixels to skip over. If the left and right sides are not clipped, then it is set to zero. Luckily, it is a very simple statement, and will translate into only one instruction in assembly. Here is the code to clip the left side:
Height := Sprite.Height; { Make a local copy of these }
Width := Sprite.Width;
IF Screen.Clip.X1 > X THEN
BEGIN
XI := Screen.Clip.X1-X; { Calculate the difference }
INC(Index, XI); { Increase the sprite offset }
INC(SkippedPixels, XI); { Make sure we skip these }
DEC(Width, XI); { The sprite is now narrower }
INC(X, XI); { Start it at the clip boundary}
END;
IF Screen.Clip.Y2 < Y+Sprite.Height-1 THEN { Clip the bottom }
Just like the bottom, the right is also easy to clip. In this case, we only have to modify two variables, instead of the four that were required for the left clipper. We have to make the sprite narrower (the Width variable) and increase the number of SkippedPixels to make sure that we only display the pixels we want. Here's all of the code!:
PROCEDURE DrawSpriteC(VAR Sprite : SpriteType; X, Y : INTEGER);
VAR
Height, Width,
SkippedPixels,
Index, XI, YI : INTEGER;
BEGIN
Index := 0; SkippedPixels := 0;
Height := Sprite.Height; { Make a local copy of these }
Width := Sprite.Width;
IF Screen.Clip.X2 < X+Width-1 THEN { Clip the Right side }
BEGIN
XI := X+Width-1-Screen.Clip.X2; { Calculate the difference }
DEC(Width, XI); { Reduce the width }
INC(SkippedPixels, XI); { Increase the number skipped }
END;
IF Screen.Clip.X1 > X THEN { Clip the Left side }
BEGIN
XI := Screen.Clip.X1-X; { Calculate the difference }
INC(Index, XI); { Increase the sprite offset }
INC(SkippedPixels, XI); { Make sure we skip these }
DEC(Width, XI); { The sprite is now narrower }
INC(X, XI); { Start it at the clip boundary}
END;
IF Screen.Clip.Y2 < Y+Height-1 THEN { Clip the bottom }
DEC(Height, Y+Height-1-Screen.Clip.Y2);{Reduce the height }
IF Screen.Clip.Y1 > Y THEN { clip the Top }
BEGIN
XI := Screen.Clip.Y1-Y; { Calculate the difference }
DEC(Height, XI); { Lower the height by the diff }
INC(Y, XI); { Increase the Y by the diff }
INC(Index, XI*Sprite.Width); { Increase the pointer to the }
END; { sprite data appropriately }
FOR YI := 0 TO Height-1 DO
BEGIN
FOR XI := 0 TO Width-1 DO
BEGIN
SetPixel(XI+X, YI+Y, Sprite.Data^[Index]);
INC(Index);
END;
INC(Index, SkippedPixels); { Skip invisible pixels }
END;
END;
This piece of code turns out to be an excellent candidate for assemblerization. The inner loop turns into the following assembly code:
@MainLoop: { FOR YI := 0 TO Height-1 DO }
mov CX, AX { FOR XI := 0 TO Width-1 DO }
rep movsb { SetPixel(XI+X, YI+Y, Sprite.Data^[Index])}
{ INC(Index) }
add SI, BX { INC(Index, SkippedPixels) }
add DI, DX { Skip invisible pixels }
dec YI
jnz @MainLoop
One optimization we can make is to expand it to write to memory a word at a time... like this:
@MainLoop: { FOR YI := 0 TO Height-1 DO }
mov CX, AX { FOR XI := 0 TO Width-1 DO }
shr CX, 1
rep movsw { SetPixel(XI+X, YI+Y, Sprite.Data^[Index])}
{ INC(Index) }
adc CX, CX
rep movsb
add SI, BX { INC(Index, SkippedPixels) }
add DI, DX { Skip invisible pixels }
dec YI
jnz @MainLoop
As you can see above, the code uses rep movsw for as much of the sprite as possible, and cleans up with a movsb if neccesary.
PROCEDURE DrawSpriteC(VAR Sprite : SpriteType; X, Y : INTEGER); ASSEMBLER;
VAR
Height, Width,
SkippedPixels,
Index, YI : INTEGER;
ASM
xor AX, AX
mov Index, AX
mov SkippedPixels, AX
mov CX, X
mov DX, Y
cmp CX, Screen.Clip.X2
jg @TrivialReject
cmp DX, Screen.Clip.Y2
jg @TrivialReject
les DI, Sprite
mov AX, ES:[DI+SpriteType.Height] { Make a local copy of these }
mov BX, ES:[DI+SpriteType.Width]
mov Height, AX
mov Width, BX
add CX, BX
add DX, AX
cmp CX, Screen.Clip.X1
jl @TrivialReject
cmp DX, Screen.Clip.Y1
jl @TrivialReject
mov AX, Screen.Clip.Y1 { Clip the Top }
cmp AX, Y { IF Screen.Clip.Y1 > Y THEN }
jle @NoTopClip { Calculate the difference }
sub AX, Y { XI := Screen.Clip.Y1-Y }
sub Height, AX { DEC(Height, XI) }
add Y, AX { INC(Y, XI) }
mul Width { Increase the pointer to the }
add Index, AX { sprite data appropriately }
@NoTopClip: { INC(Index, XI*Sprite.Width) }
mov AX, X { Clip the Right side }
add AX, Width
dec AX
cmp Screen.Clip.X2, AX { IF Screen.Clip.X2 < X+Width-1 THEN }
jge @NoRightClip { BEGIN }
sub AX, Screen.Clip.X2 { XI := X+Width-1-Screen.Clip.X2 }
sub Width, AX { Reduce the width }
add SkippedPixels, AX { Increase the number skipped }
@NoRightClip: { END; }
mov AX, Screen.Clip.X1 { Clip the Left side }
cmp AX, X { IF Screen.Clip.X1 > X THEN }
jle @NoLeftClip { BEGIN }
sub AX, X { XI := Screen.Clip.X1-X }
add Index, AX { Increase the sprite offset }
add SkippedPixels, AX { Make sure we skip these }
sub Width, AX { The sprite is now narrower }
add X, AX { Start it at the clip boundary}
@NoLeftClip:
mov AX, Y
add AX, Height
dec AX { Clip the bottom }
cmp Screen.Clip.Y2, AX { IF Screen.Clip.Y2 < Y+Height-1 THEN }
jge @NoBottomClip
sub AX, Screen.Clip.Y2 { Reduce the height }
sub Height, AX { DEC(Height, Y+Height-1-Screen.Clip.Y2) }
@NoBottomClip:
les DI, Screen.Buffer
add DI, X
mov BX, Y { Y is an index into the ScreenY array }
add BX, BX { Multipy by two because each entry is a word }
add DI, DS:[BX+OFFSET Screen.YTable]
push DS
mov DX, Screen.Width
lds SI, Sprite
mov AX, Height
lds SI, DS:[SI+SpriteType.Data]
mov YI, AX
mov AX, Width
mov BX, SkippedPixels
add SI, Index
sub DX, AX
@MainLoop: { FOR YI := 0 TO Height-1 DO }
mov CX, AX { FOR XI := 0 TO Width-1 DO }
shr CX, 1
rep movsw { SetPixel(XI+X, YI+Y, Sprite.Data^[Index])}
{ INC(Index) }
adc CX, CX
rep movsb
add SI, BX { INC(Index, SkippedPixels) }
add DI, DX { Skip invisible pixels }
dec YI
jnz @MainLoop
pop DS
@TrivialReject:
END;
Notice how nasty and ugly looking the initialization code is, but now nice and clean the inner loops are. It's a good thing that all of the processor time is spent in the loops, huh? Anyway, the moral of the story is that this code is not as bad as it looks.
Wow, now that we have a super fast clipped sprite drawer, what are we going to do with it? The following program displays a bunch of clipped sprites. It moves them around so that various parts of the sprite get clipped, and there are some command line parameters. Just play with it a little bit and have some fun.
PROGRAM SpriteClippingExample;
USES CRT, GraphPro, Sprite, FPS;
CONST
ClipX1 = 50;
ClipY1 = 40;
ClipX2 = 270;
ClipY2 = 160;
VAR
Size,
I : INTEGER;
Clip : BOOLEAN;
Font : FontType;
Spr : SpriteType;
X, Y : INTEGER;
Radius, Theta : REAL;
Flag : STRING;
BEGIN
Size := 63;
Clip := TRUE;
Randomize;
FOR X := 1 TO ParamCount DO
BEGIN
Flag := ParamStr(X);
FOR Y := 1 TO Length(Flag) DO
Flag[Y] := Upcase(Flag[Y]);
IF Flag = '-C' THEN
Clip := FALSE
ELSE
BEGIN
Val(Flag, I, Y);
IF Y = 0 THEN Size := I;
IF Size < 19 THEN Size := 19;
END;
END;
InitGraph; { Initialize the graphics }
LoadBIOSFont(Font, 16); { Get a 16 point font }
ClrScr(0);
FOR I := 0 TO (Size SHR 1)-9 DO { Create a dounut ball sprite }
BEGIN
Line(Size+1+Size SHR 1 , I, Size+1+I, Size SHR 1, 40-I);
Line(Size+1+Size SHR 1+1, I, Size+1+Size-I, Size SHR 1, 40-I);
Line(Size+1+Size SHR 1 , Size-I, Size+1+I, Size SHR 1+1, 40-I);
Line(Size+1+Size SHR 1+1, Size-I, Size+1+Size-I, Size SHR 1+1, 40-I);
END;
GetSprite(Spr, Size+1, 0, Size+Size+1, Size);
ClrScr(0);
SetClipBoundary(ClipX1, ClipY1, ClipX2, ClipY2);{ Set the clipping boundary }
Line(ClipX1, ClipY1, ClipX2, ClipY1, 15); { Draw the clipping boundary }
Line(ClipX1, ClipY1, ClipX1, ClipY2, 15);
Line(ClipX2, ClipY1, ClipX2, ClipY2, 15);
Line(ClipX1, ClipY2, ClipX2, ClipY2, 15);
WriteG1(Font, 1, 1, 'Press almost any key to continue', 7);
WriteG1(Font, 0, 0, 'Press almost any key to continue', 15);
DrawSprite(Spr, 160-Size DIV 2, 0);
DrawSprite(Spr, 160-Size DIV 2, 200-Size);
Theta := 0.0;
StartFPS;
WHILE NOT KeyPressed DO
BEGIN
Radius := Cos(Theta);
X := Round(Cos(Theta)*110)+160 - Size DIV 2;
Y := Round(Sin(Theta)*Radius*120)+100 - Size DIV 2;
Theta := Theta + 0.02;
IF Clip THEN
DrawSpriteC(Spr, X, Y)
ELSE
DrawSprite(Spr, X, Y);
INC(Frames);
END;
EndFPS;
CloseGraph; { Deinit graphics }
Writeln('RLE Sprite Clipping Demo - (Frames = Sprites below...)');
ShowFPS('1996');
WRITELN;
WRITELN('Format of command: RLEDemo [-C] [XXX]');
WRITELN(' -C turns clipping off');
WRITELN(' XXX changes the size of the sprite from the default of 63');
WRITELN(' (Odd values work best, the minimum is 19)');
WRITELN;
WRITELN('Example: RLEDemo -C 31')
END.