Landscaping is the technique by Mike Singleton, used to
such good affect in The Lords of Midnight, Doomdark's Revenge and later in Sorderons
Shadow. All three of these games used the technique a little differently but are all
fundamentally based on the same system - even code. All
thress games were grid based using 1 tile to represent 1 location. Each location was then
stored in either 1 or 2 bytes to identify the location type and other information. The
first thing that landscaping had to do was to select the locations that would be needed
for the display system.
fig 1.0 - the locations
|
If we take the current location and draw a circle
with a diameter of 13 locations, this would give us all the possible locations to consider
depending on in which direction we are looking. fig 1.0 shows this, the white location in
the middle is the current location.
We then need to consider which direction we are actually
looking in. From the center point we can draw a cone which is our view point. All
locations that fall within this cone are then considered to be drawn to screen. This cone
is then rotated around the circle depending on which compass direction we are looking in.
The circle is therefore split into 8 seperate sections to be used depending on which way
we are looking. In the following images the blue locations are the locations that would be
drawn to the screen.
|
|
|
|
north |
north east |
east |
south east |
|
|
|
|
south |
south west |
west |
north west |
After each location has been selected the
graphic of the required terrain types needs to be scaled and drawn to the screen in the
appropriate place.
When looking northwest, for example, the layout of the locations on the screen would
look something like fig 1.1. looking north would give you fig 1.2 NOTE:
only these two positions are required for all the other directions.
fig 1.1 - locations translated to the screen - northwest
fig 1.2 - locations translated to the screen - north
If we then populate these positions with the
graphic of the actual terrain, at the appropriate size, drawing from back to front the
screen would look something like fig 1.3
fig 1.3 - final locations translated to the screen -
northwest
The above affect could be calculated with a little 3d
oriented maths nowadays, but back when this technique was developed I doubt the
spectrum could have coped with the maths quick enough ( but don't hold me to that! ). So
the entire process was precalculated and a resulting table used to do the work at run
time.
The graphics is the games are prescaled and are stored in a draw-format and not a pixel
format. this allows storage to be small. that aside in LOM the graphics account for 16,618
bytes. or 16.2kb
In DDR they were 16180bytes or 15.8kb - the memory requirement was smaller but there is an
extra terrain type - mist, and some of the locations are more complex. However Mike had to
decrease the distance the player could see forward, to allow the lose of the two smallest
of each of the terrain graphics. See the draw
format for a breakdown description of the terrain graphic format.
Follows is some source code which describes in detail the
process used in LOM to achieve the effect required for landscaping.
typedef struct {
s8 dy; // location y adjuster
s8 dx; // location x adjuster
s16 xadj; // screen x adjuster
u8 y; // screen y position
u8 size; // scale
} landscape_t;
// Landscaping Location Calculation Table!
landscape_t landscape[] = {
{ 5, 4, 86, 64, 7 },
{ 4, 5, 114, 64, 7 },
{ 6, 2, 41, 64, 7 },
{ 6, 1, 21, 64, 7 },
{ 6,-1, -21, 64, 7 },
{ 6, 0, 0, 64, 7 },
{ 5, 3, 69, 63, 7 },
{ 3, 5, 131, 63, 7 },
{ 4, 4, 100, 63, 7 },
{ 5, 2, 48, 63, 6 },
{ 5, 1, 25, 63, 6 },
{ 5,-1, -25, 63, 6 },
{ 5, 0, 0, 62, 6 },
{ 4, 3, 82, 62, 6 },
{ 3, 4, 118, 62, 6 },
{ 4, 2, 59, 62, 6 },
{ 3, 3, 100, 61, 5 },
{ 4, 1, 31, 61, 5 },
{ 4,-1, -31, 61, 5 },
{ 4, 0, 0, 60, 5 },
{ 3, 2, 75, 59, 5 },
{ 2, 3, 125, 59, 5 },
{ 3, 1, 41, 58, 4 },
{ 3,-1, -41, 58, 4 },
{ 3, 0, 0, 57, 4 },
{ 2, 2, 100, 57, 4 },
{ 2, 1, 59, 53, 3 },
{ 1, 2, 141, 53, 3 },
{ 2, 0, 0, 51, 2 },
{ 1, 1, 100, 43, 1 },
{ 1, 0, 0, 32, 0 },
};
// Landscaping Direction Adjustment tables
u8 xAdjustments[] = { 3,2,0,0,2,3,1,1,3 };
u8 yAdjustments[] = { 1,1,3,2,0,0,2,3,1 };
short terrain[15][8] = {
{ 0x0000,0x023B,0x03C5,0x04DD,0x05CA,0x0674,0x06DE,0x072F }, // Mountain
{ 0x076E,0x0A82,0x0C76,0x0D93,0x0E6E,0x0F00,0x0F5D,0x0FAB }, // Citadel
{ 0x0FD8,0x120F,0x1384,0x1476,0x153F,0x15D1,0x1632,0x1677 }, // Forest
{ 0x16AA,0x1819,0x18F7,0x1994,0x1A1C,0x1A6E,0x1AA4,0x1AC9 }, // Henge
{ 0x1AE1,0x1C42,0x1D2D,0x1DCC,0x1E4E,0x1EB1,0x1EF8,0x1F2F }, // Tower
{ 0x1F5B,0x204B,0x20E0,0x213E,0x2191,0x21C1,0x21E6,0x21FC }, // Village
{ 0x220B,0x22D1,0x235C,0x23BF,0x2419,0x245D,0x248D,0x24B2 }, // Downs
{ 0x24C7,0x25CE,0x2692,0x26F5,0x2744,0x2773,0x2797,0x27AE }, // Keep
{ 0x27C2,0x2835,0x2884,0x28B7,0x28DD,0x28F5,0x290A,0x291B }, // SnowHall
{ 0x2926,0x295A,0x2981,0x299E,0x29AE,0x29BB,0x29C5,0x29CC }, // lake
{ 0x29D0,0x2BEB,0x2D50,0x2E46,0x2F14,0x2FA8,0x3001,0x303F }, // Wastes
{ 0x3073,0x317F,0x3231,0x32A5,0x330D,0x3359,0x338D,0x33B0 }, // Ruin
{ 0x33CC,0x3567,0x3688,0x3753,0x3807,0x388B,0x38EC,0x3930 }, // lith
{ 0x395C,0x39E0,0x3A3E,0x3A83,0x3ABF,0x3AE8,0x3B09,0x3B24 }, // Cavern
{ 0x3B35,0x3D22,0x3E4A,0x3F11,0x3FB8,0x403E,0x408C,0x40C2 }, // Army
};
void DrawPanoramic ( int dir )
{
int HowLftScr;
int HowRhtScr;
// looking along a straight line
HowLftScr = 1 ;
HowRhtScr = 0 ;
// adjust for diagonal
if ( dir&1 ) {
HowLftScr = 2 ;
HowRhtScr = 3 ;
}
// step through all the entries in the landscaping table
for ( int ii=0; ii<31; ii++ ) {
// draw location on the left of the screen
DrawLocation ( ii,
xAdjustments[dir],
yAdjustments[dir],
HowLftScr );
// draw locations on the right of the screen
DrawLocation ( ii,
xAdjustments[dir+1],
yAdjustments[dir+1],
HowRhtScr );
}
}
void DrawLocation ( int distance, int xtype, int ytype, int scrtype )
{
int locx,locy;
int x;
// work out the real y coordinate
locy = AdjustMapPos ( HIBYTE(g_CurrentLocation),
ytype,
landscape[distance].dx,
landscape[distance].dy );
// work out the real x coordinate
locx = AdjustMapPos ( LOBYTE(g_CurrentLocation),
xtype,
landscape[distance].dx,
landscape[distance].dy );
// actually get the location data
GetLocation ( locx, locy ) ;
// work out where on screen, x coordinate this item should be
x = 320;
switch ( scrtype ) {
case 0: x += landscape[distance].xadj ;
break;
case 1: x -= landscape[distance].xadj ;
break;
case 2: x = (x+landscape[distance].xadj)-100
; break;
case 3: x = (x-landscape[distance].xadj)+100
; break;
}
// and draw it
DrawFeature ( g_Feature,
x,
landscape[distance].y,
landscape[distance].size );
}
int AdjustMapPos ( int pos, int type, int dx, int dy )
{
switch ( type ) {
case 0: pos = pos + dy; break;
case 1: pos = pos - dy; break;
case 2: pos = pos + dx; break;
case 3: pos = pos - dx; break;
}
return pos ;
}
void DrawSet ( int y, int x, int inkpaper)
{
if ( x<0 ) return ;
if ( x>256) return;
LPPIXEL pDst = g_pDst + ((y*2)*g_Pitch)+(x*2) ;
pDst[0] = g_inks[inkpaper] ;
pDst[1] = g_inks[inkpaper] ;
pDst[g_Pitch+0] = g_inks[inkpaper] ;
pDst[g_Pitch+1] = g_inks[inkpaper] ;
}
void DrawLine ( int y, int x1, int x2, int inkpaper )
{
if ( x1<0 ) x1=0;
if ( x2>256) x2=256;
LPPIXEL pDst = g_pDst + ((y*2)*g_Pitch) ;
for ( int x=(x1*2); x<=(x2*2); x++ ) {
pDst[x] = g_inks[inkpaper] ;
pDst[x+1] = g_inks[inkpaper] ;
pDst[x+g_Pitch] = g_inks[inkpaper] ;
pDst[x+g_Pitch+1] = g_inks[inkpaper] ;
}
}
void DrawFeature ( int feature, int x, int y, int size )
{
u8* pData;
u8 flags ;
int MoreOnThisLine;
int InkOrPaper;
int OperationCount;
int height;
s8 x1,x2;
// draws the terrain pointer to by
// Offset as X,Y
// where X,Y is the bottom of the feature
// and Screen Y = 0 is at the bottom of the screen
if ( feature == 15 )
return;
g_SurfaceWork.Lock();
g_Pitch = g_SurfaceWork.Pitch() / g_ScreenDepth;
g_pDst = (LPPIXEL)g_SurfaceWork.Image();
g_pDst+= 64;
g_pDst+= 48*g_Pitch;
// offset is the terrain to draw
// and the scale
// 0 = Big, 7 = SMALL
pData = g_pTerrainGfx + terrain[feature][size] ;
y = (192-16) - y ;
x -= 256;
// terraingfx
// comes from the file terrain.bin
// or memory address 0x9000
height = *pData++;
while ( height-- ) {
while ( TRUE ) {
flags = *pData++ ;
// bit 8 = More than 1 group of drawing operations
// for this scan line
// bit 7 = Should we draw in ink or paper
// bits 1 - 6 = No of drawing instructions to follow
MoreOnThisLine = (
flags & 0x80 ) ? 1 : 0;
InkOrPaper = ( flags
& 0x40 ) ? 0 : 1 ;
OperationCount = flags
& 0x3f;
// draw a horizontal line from x1 to x2
x1 = *pData++ ;
x2 = *pData++ ;
DrawLine( y, x+x1,
x+x2, InkOrPaper );
// draw on the edges in Ink Always
DrawSet( y, x+x1, 1 );
DrawSet( y, x+x2, 1 );
// reverse the last color
InkOrPaper =
(InkOrPaper+1)&1;
while (
OperationCount-- ) {
// bit 8 = 1 = Draw A Line from
// bits 1 - 7 = x coordinate
flags = *pData++ ;
x1 = flags & 0x7f ;
if ( flags&0x80 ) {
x2 = *pData++ ;
DrawLine( y, x+x1, x+x2, InkOrPaper );
}else{
DrawSet( y, x+x1, InkOrPaper );
}
}
if ( !MoreOnThisLine )
break;
}
y--;
}
g_SurfaceWork.Unlock();
}
|