THE ART OF LANDSCAPING

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
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.2 - final locations translated to the screen
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();

}

 

Tower of the Moon