EotB – Rendera väggar

Eye of the Beholder fortsätter att intressera oss. I tidigare artikel så försökte vi lista ut hur väggarna är uppbyggda. Vi kom en bit på vägen. I denna artikel ska vi lösa uppritningen av väggar och hela systemet för vyn i spelet.

Spelplanen

Som vi tidigare nämnt så är ”spelplanen”, alltså det fönster där världen ritas upp i, 22 x 15 block stort. Varje block är 8 x 8 pixlar vilket get 176 x 120 pixlars fönster.

Kartan som man vandrar runt i kan ses som ett 2D rutnät. När vi kikar år något av de fyra väderstrecken så är vår syn i kartan begränsad. Bilden nedan försöker illustrera de intilliggande rutorna som man kan se. Om det inte finns några väggar i vägen såklart.

Det blå fältet indikerar ungefärligt synfält. De gröna linjerna är potentiella väggar som kan ses. För varje väggtyp måste det finnas 25 olika versioner för att täcka in alla kombinationer av vinklar av samma väggtyp.

Innan vi går in på mer detaljer kring uppritning så måste vi göra en del korrigeringar i vår tolkning av .VMP-filerna.

VMP-filer

Vi gjorde ett fel i tidigare artiklar som måste revideras. Den ”padding” på 101 UInt16 som angavs stämmer inte. Det är inte utfyllnad utan data om hur blocken ska byggas ihop. Vi upptäckte detta då informationen vi hade sa att varje väggtyp beskrevs med 431 olika 8 x 8 block. Väljer man att läsa in filen med det filhuvud som angavs så kommer 330 UInt16 data att inte laddas in och en hel väggtyp kommer att missas. Det korrekta filhuvudet är

Datatyp Namn Kommentar
UInt16 Header Oklart vad den används till. Troligtvis hur många väggtyper som finns i filen.
UInt16 [22,15] BackgroundTiles Alla block som ska användas till bakgrunden. Den är alltså 22×15 block stor.
UInt16[431]*NrOfWallTypes WallType… Block för att beskriva en sorts vägg.

NrOfWallTypes kan beräknas med formeln (filstorlek – 2) / (431 * 2). En korrekt kodfil för inläsning blir:

VmpFile.cs
namespace PakExtract
{
    class VmpFile
    {
        public int Header { get; set; }
        public int[,] BackgroundTiles { get; set; }

        public int NrOfWallTypes { get; set; }
        public int[,] WallTiles { get; set; }

        private VmpFile(byte[] data)
        {
            int index = 0;
            Header = data[index++] + (data[index++] << 8);
            BackgroundTiles = new int[22, 15];

            for(int y=0; y<15; y++)
                for(int x=0; x<22; x++)
                    BackgroundTiles[x, y] = data[index++] + (data[index++] << 8);

            NrOfWallTypes = (data.Length - 2) / (431 * 2);
            WallTiles = new int[NrOfWallTypes, 431];

            for(int i =0; i<NrOfWallTypes; i++)
                for(int c=0; c<431; c++)
                    WallTiles[i, c] = data[index++] + (data[index++] << 8);
        }

        public static VmpFile FromData(byte[] data)
        {
            return new VmpFile(data);
        }
    }
}

Hjälp vid uppritning

För att lista ut vilka block i VMP-filen som hör till vilka väggar så behöver vi hjälp. Vi behöver data över varje position och dess tillhörande block. Som tur är så har vi hittat, felsökt och rättat en datastruktur som vi väljer att kalla WallRenderData. Alla 25 olika positioner kan vi beskriva med följande fil:

WallRenderData.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace PakExtract
{
    struct WallRenderData
    {
        public short BaseOffset;
        public short OffsetInViewPort;
        public short VisibleHeightInBlocks;
        public short VisibleWidthInBlocks;
        public short SkipValue;
        public short FlipFlag;

        public WallRenderData(short bo, short op, short vH, 
            short vW, short skip, short flip)
        {
            BaseOffset = bo;
            OffsetInViewPort = op;
            VisibleWidthInBlocks = vW;
            VisibleHeightInBlocks = vH;
            SkipValue = skip;
            FlipFlag = flip;
        }

        public static readonly IList<WallRenderData> Data = 
            new ReadOnlyCollection<WallRenderData>(new[]
            {
                new WallRenderData(104, 66, 5, 1, 2, 0),/* A-east */
                new WallRenderData(102, 68, 5, 3, 0, 0),/* B-east */
                new WallRenderData(97, 74, 5, 1, 0, 0), /* C-east */
                new WallRenderData(97, 79, 5, 1, 0, 1), /* E-west */
                new WallRenderData(102, 83, 5, 3, 0, 1),/* F-west */
                new WallRenderData(104, 87, 5, 1, 2, 1),/* G-west */
                new WallRenderData(133, 66, 5, 2, 4, 0),/* B-south */
                new WallRenderData(129, 68, 5, 6, 0, 0),/* C-south */
                new WallRenderData(129, 74, 5, 6, 0, 0),/* D-south */
                new WallRenderData(129, 80, 5, 6, 0, 0),/* E-south */
                new WallRenderData(129, 86, 5, 2, 4, 0),/* F-south */
                new WallRenderData(117, 66, 6, 2, 0, 0),/* H-east */
                new WallRenderData(81, 50, 8, 2, 0, 0), /* I-east */
                new WallRenderData(81, 58, 8, 2, 0, 1), /* K-west */
                new WallRenderData(117, 86, 6, 2, 0, 1),/* L-west */
                new WallRenderData(163, 44, 8, 6, 4, 0),/* I-south */
                new WallRenderData(159, 50, 8,10, 0, 0),/* J-south */
                new WallRenderData(159, 60, 8, 6, 4, 0),/* K-south */
                new WallRenderData(45, 25,12, 3, 0, 0), /* M-east */
                new WallRenderData(45, 38,12, 3, 0, 1), /* O-west */
                new WallRenderData(252, 22,12, 3,13, 0),/* M-south */
                new WallRenderData(239, 41,12, 3,13, 0),/* O-south */
                new WallRenderData(239, 25,12,16, 0, 0),/* N-south */
                new WallRenderData(0,  0,15, 3, 0, 0),  /* P-east */
                new WallRenderData(0, 19,15, 3, 0, 1),  /* Q-west */
            });
    }
}

Läs kommentarerna till varje position för att länka ihop med bilden över synliga rutor (A-Q).

BaseOffset anger hur många block in i VMP-filens WallTiles[,] vi hittar given position.
OffsetInViewPort anger hur många block in (både x- och y-led i samma siffra) fönstret som positionen börjar på.
VisibleWidthInBlocks anger antalet synliga block i x-led.
VisibleHeightInBlocks anger antalet synliga block i y-led.
SkipValue anger en extra offset som måste läggas på när man byter y-värde för positionens block vid uppritningen.
FlipFlag anger om blocket som ska ritas upp ska speglas horisontellt. Det är ett värde på 0x4000 i så fall.

Uppritningen av en given position blir alltså VisibleWidthInBlocks x VisibleHeightInBlocks stor i fönstret. Blocken i VMP-filen ligger i den ordningen (med viss korrigering för SkipValue ).

Uppritning

För att rita upp en korrekt vy så behöver vi en VMP-fil, tillhörande VCN-fil samt tillhörande PAL-fil. Vi behöver också veta hur vi ska pussla ihop dessa filer, vilket vi nu gör!

Det är dags att konstruera en uppritare som kan generera bakgrundsbild + en väggposition av en given väggtyp. Vi kommer att bespara er all diskussion om flesökning och testning som ledde fram till detta och enbart presentera resultatet, nämligen vår BlockBuilder.

BlockBuilder.cs
using System.Diagnostics;
using System.Drawing;

namespace PakExtract
{
    class BlockRenderer
    {
        public VcnFile Vcn { get; set; }
        public VmpFile Vmp { get; set; }
        public PaletteFile Palette { get; set; }

        public BlockRenderer(VcnFile vcn, VmpFile vmp, PaletteFile pal)
        {
            Vcn = vcn;
            Vmp = vmp;
            Palette = pal;
        }

        public void DrawWall(Bitmap bmp, int wallType, int wallPosition, int offsetX, int offsetY)
        {
            int offset = WallRenderData.Data[wallPosition].BaseOffset;

            short visibleHeightInBlocks = WallRenderData.Data[wallPosition].VisibleHeightInBlocks;
            short visibleWidthInBlocks = WallRenderData.Data[wallPosition].VisibleWidthInBlocks;
            int flipX = WallRenderData.Data[wallPosition].FlipFlag;

            for (int y = 0; y < visibleHeightInBlocks; y++)
            {    
                for (int x = 0; x < visibleWidthInBlocks; x++)
                {
                    int blockIndex;
                    if (flipX == 0)
                        blockIndex = x + y * 22 + WallRenderData.Data[wallPosition].OffsetInViewPort;
                    else
                        blockIndex = WallRenderData.Data[wallPosition].OffsetInViewPort +
                                   visibleWidthInBlocks - 1 - x + y * 22;

                    int xpos = blockIndex % 22;
                    int ypos = blockIndex / 22;
                    int tile = Vmp.WallTiles[wallType, offset];

                    /* xor with wall flip-x to make block flip and wall flip cancel each other out. */
                    int blockFlip = (tile & 0x4000) ^ flipX;
                    blockIndex = tile & 0x3fff;

                    DrawBlock(bmp, xpos * 8, ypos * 8, blockIndex, blockFlip > 0, offsetX, offsetY);

                    offset++;
                }
                offset += WallRenderData.Data[wallPosition].SkipValue;
            }
        }

        private void DrawBlock(Bitmap bmp, int xpos, int ypos, int blockIndex, 
            bool isFlipped, int offsetX = 0, int offsetY = 0)
        {
            var blockData = Vcn.Blocks[blockIndex];

            for (int y = 0; y < 8; y++)
                for (int x = 0; x < 8; x++)
                {
                    var val = (isFlipped) ? blockData[7- x, y] : blockData[x, y];
                    if (val == 0)
                        continue;
                    var col = Palette.Colors[Vcn.WallLookup[val]];
                    bmp.SetPixel(xpos + x + offsetX, ypos + y + offsetY, col);
                }
        }

        public Bitmap DrawBackdrop()
        {
            Bitmap bmp = new Bitmap(176, 120);
            DrawBackdrop(bmp, 0, 0);
            return bmp;
        }
        public void DrawBackdrop(Bitmap bmp, int offsetX, int offsetY)
        {
            for (int y = 0; y < 15; y++)
                for (int x = 0; x < 22; x++)
                {
                    var tile = Vmp.BackgroundTiles[x, y];
                    bool isLimit = (tile & 0b10000000_00000000) > 0;
                    bool isFlipped = (tile & 0b01000000_00000000) > 0;

                    if (isLimit || isFlipped)
                        Debug.WriteLine(isLimit + " " + isFlipped);

                    var block = Vcn.Blocks[tile & 0x3fff];
                    for (int y2 = 0; y2 < 8; y2++)
                        for (int x2 = 0; x2 < 8; x2++)
                        {
                            var val = block[x2, y2];
                            var col = Palette.Colors[Vcn.BackdropLookup[val]];
                            bmp.SetPixel(x * 8 + x2 + offsetX, y * 8 + y2 + offsetY, col);
                        }
                }
        }
    }
}

Bakgrunden ritas upp med en egen palett utan genomskinlighet. Väggarna ritas upp med en annan palett och med färg 0 som genomskinlig. Även om koden är en aning omfattande så gör den helt enkelt jobbet med att rita upp en väggposition med hjälp av data från WallRenderData. DrawBackDrop har vi diskuterat i tidigare artikel. Den metoden som är intressant här är DrawWall.

Nu har vi knåpat ihop en liten loop som ritar ut väggtyp 0 från BRICK.VMP i alla 25 olika positioner, namngivna med text så du kan koppla ihop positionerna med den inledande bilden.

Väggtyper

Nu kan vi också undersöka vilka olika väggtyper det faktiskt finns. Det visar sig att det endast finns 6 olika väggtyper i BRICK.VMP.

De två första väggtyperna är ”vanlig” vägg i två olika varianter. Sedan har vi dörrarna följt av stegar upp och ned (?). Sist så finns portalen.

Avslutning

Nu har vi verkligen kommit en bra bit på vägen. Allt du ser är återskapat utifrån original-filerna. Nästa steg kommer att vara att få igång en variant av själva ”motorn” så vi kan gå runt. Steget efter det är att ladda banorna från spelet. Vi avslutar som vanligt med att bifoga källkoden.

Scroll to top