admin 发表于 2021-12-13 18:48:22

Mir2引擎之地图说明

Mir2的地图场景在逻辑上分三层,从里到外分别为:1,tiles,,smtiles构成的地面背景,,2,,objects构成的地图前景,3,,,随时活动着的人物
(实际上人物的绘制不是地图单元要实现的,,,而是在场景单元playscn中实现的,,所以上面的3也可以删去)

tiles,,大块的地面,,smtiles,,小块地面,smtiles填补tiles不能补充的地面部分,,如一些大块tiles拼接处绘制不到的缝隙,如小块草地(实际
上这也是mir2丰富地图效果的手段之一,,而不仅仅是一种容错的技术),玩过mir2的地图编辑器都会知道,tiles大小一般为96*64,smtiles一般大
小为48*32,,即1tiles=4个smtiles,,前景图就是objects.wil-objects*.wil里面所包含的图片,主要是一些mir2游戏世界中的景观如建筑物等,
客户端的mir.set定义了一些objects的组合,这些组合解释了一些常见的mir2景观(npc屋等,,石柱等,,树木),,它是一个ascii码文件,请自行理
解它的各字段的意义,,它解释了景观用到的objects序列,,跨越多少mappoint

注意到有些objects里面的图片也绘制了地面,如沙巴克门前破碎的石柱佛面像等,此外,一个.map文件不仅定义了用哪些tiles,smtiles,objects
来绘制它,,还包括了其它一些必需的信息,如哪里不可走,,哪里是过门点,这些属性都是用地图编辑器生成一个地图文件后由地图编辑器自动
生成的,,mir2的引擎之地图读取单元就是读取.map文件为程序所用,,,实现的源程序片断如下:

{------------------------------------------------------------------------------}
// 地图常量信息定义
{------------------------------------------------------------------------------}
const

// 屏幕大小 整个游戏世界场景区的大小,,也即DDraw在屏幕上开辟的的最大绘图面积,mir2res使用了ddraw的800*600*8的绘图引擎
CLIENT_WIDTH = 800;
CLIENT_HEIGHT = 600;

// 显示地图的大小 600-445的屏幕部分用于显于道具栏
MAPSURFACE_WIDTH = 800;
MAPSURFACE_HEIGHT = 445;

// 地图单位/背景图片尺寸 //这就是"地图格",,如比奇地图大小为700*700,它的单位是mapunit,,一些NPC屋的地图大小就小多了,,
700*700折合像素单位为 (700*48)*(700*32),,想像一下要走遍比奇,在水平方向上要卷屏700*48/800次,,也即:刚好42屏
MAPUNIT_WIDTH = 48;
MAPUNIT_HEIGHT = 32;

// 地图单位中心象素点
MAPCENTER_X = (MAPSURFACE_WIDTH - MAPUNIT_WIDTH) div 2;
MAPCENTER_Y = (MAPSURFACE_HEIGHT - MAPUNIT_HEIGHT) div 2;

// 逻辑地图单位 (估计是微量屏幕移动的最小值)
LOGICALMAPUNIT = 20;

LONGHEIGHT_IMAGE = 35;      // 地图上的任何一个前景图片最大的高度(以 MapPoint 为单位)

即任何一个包含在objects里面的图片高度都不能超过35,即35*32=1120,我们知道,,mappoint=48*32,,所以它超越了整个绘图区的高度600

{------------------------------------------------------------------------------}
// 地图文件结构定义
{------------------------------------------------------------------------------}
type
// 地图文件头结构 (52字节, 注意: 原文件头大小为56字节)
// 估计 UpdateDate 偏移有误
PMapHeader = ^TMapHeader;
TMapHeader = packed record
    Width      : Word;                      // 宽度      2
    Height   : Word;                      // 高度      2
    Title      : string;                // 标题      17
    UpdateDate : TDateTime;               // 更新日期8
    Reserved   : array of Char;      // 保留      23
end;

// 地图点数据结构
PMapPoint = ^TMapPoint;
TMapPoint = packed record
    BackImg   : Word;   // 背景图片索引(BackImg-1), 图片在 Tile.wil 中
    MiddImg   : Word;   // 背景小图索引(MiddImg-1), 图片在 SmTile.wil 中
    ForeImg   : Word;   // 前景
    DoorIndex   : Byte;   //    $80 (巩娄), 巩狼 侥喊 牢郸胶
    DoorOffset: Byte;   //    摧腮 巩狼 弊覆狼 惑措 困摹, $80 (凯覆/摧塞(扁夯))
    AniFrame    : Byte;   //    $80(Draw Alpha) +橇贰烙 荐
    AniTick   : Byte;
    Area      : Byte;   //    瘤开 沥焊
    Light       : Byte;   //    0..1..4 堡盔 瓤苞
end;

"地图点"是一个重要的概念:我们把tiles,,smtiles构成的地面层称为背景,,把objects构成的地图层称为前景,,地图点的大小为mapunit定义
的像素大小,,,比奇有700*700个地图点数据,,现在来看一下它的结构,,前三个好理解,,doorindex,,dooroffset,,aniframe,,anitick,我们将
在下面的函数过程中求得它的实际意义,,,area为该地图点使用的是哪个objects文件(1.76版的mir2为0~6好像),,light,,天气文件,,lig0a~f

type

{------------------------------------------------------------------------------}
// TMirMap class
{------------------------------------------------------------------------------}
TMirMap = class(TObject)
private
    FFileName: string;
    FFileHandle: THandle;   // WIN32 文件句柄 指向硬盘文件
    FFileMapping: THandle;    // 内存映射文件句柄 指向内存镜像文件,,也称内存"图像"文件,,这里的图像并非指图片
    FFilePointer: Pointer;    // 内存映射指针
    FHeight: Word;
    FWidth: Word;
    FTitle: string;
    FUpdateDate: TDateTime;

{    FCenterX: Integer;
    FCenterY: Integer;
    FShiftX: Integer; //我们分析过actor,,谈到shiftx,,,它就在这里发挥作用
    FShiftY: Integer;}
    FClientWidth: Integer;
    FClientHeight: Integer;

    procedure SetFileName(const value: string);
    function GetPoint(X, Y: Word): PMapPoint;
protected

public
    AniTick: Cardinal;
    AniCount: Integer;
   
    constructor Create(AClientWidth, AClientHeight: Integer);

    destructor Destroy; override;

    function CanMove(X, Y: Word): Boolean;

    function CanFly(X, Y: Word): Boolean;

    procedure BitBlt(DC: HDC; X, Y, AWidth, AHeight: Word);

    procedure DrawBackground(Surface: IDirectDrawSurface7;
      CenterX, CenterY, ShiftX, ShiftY: Integer);

    procedure DrawForeground(Surface: IDirectDrawSurface7;
      CenterX, CenterY, ShiftX, ShiftY: Integer; FirstStep: Boolean);

    // 地图文件名, 指定为空串将关闭地图
    property FileName: string read FFileName write SetFileName;

    // 地图宽度
    property Width: Word read FWidth; //注意delphi的这个机制
    // 地图高度
    property Height: Word read FHeight;
    // 指定地图点的信息 (返回为 TMapPoint 指针, 直接指向地图文件)
    property Point: PMapPoint read GetPoint;

    // 地图标题
    property Title: string read FTitle;
    // 地图更新日期(可能有误)
    property UpdateDate: TDateTime read FUpdateDate;
end;


{ TMirMap }
implementation

procedure TMirMap.BitBlt(DC: HDC; X, Y, AWidth, AHeight: Word); //读取.map行列属性,根据一定规则,,进行实际的游戏世界中的贴图工作,,,图片资源当然在tiles,,,smtiles和objects中了:)
var
Pt: PMapPoint;
I, J: Word;
ImageIndex, AniIndex: Word;
AniCount: Integer;
// TODO: 本函数中的乘法运算可以优化为加法,CPU做加法比做乘法快
begin

// TODO: 更新正确的 AniCount
AniCount := 1000;

// 画背景图
for J := Y to Y + AHeight do
begin
    // 如果纵坐标超出地图范围则终止
    // TODO: 这时 I, J 定义为 Word, 会永远为 False, 应该更正, 包括下面的函数
    if J >= FHeight then Break;

    for I := X to X + AWidth do
    begin
      // 如果横坐标超出地图范围则终止
      if I >= FWidth then Break;

      // 取坐标处的地图信息
      Pt := GetPoint(I, J);

      // 如果是偶数行, 则画大块背景, 背景图尺寸是 96 * 64
      if (J mod 2 = 0) and (I mod 2 = 0) then
      begin
      ImageIndex := Pt.BackImg and $7FFF;
      if ImageIndex > 0 then
          G_WilTile.BitBlt(ImageIndex - 1, DC, (I-X) * 48, (J-Y) * 32);
      end;

      // 画小图, 小图尺寸是 48 * 32 (小图用于填补一些大图画不到的边缘)
      ImageIndex := Pt.MiddImg;
      if ImageIndex > 0 then
      G_WilTileSm.BitBlt(ImageIndex - 1, DC, (I-X) * 48, (J-Y) * 32);
    end;
end;

// 画前景, 前景图尺寸是 48 * 32
for J := Y to Y + AHeight do
begin
    // 如果纵坐标超出地图范围则终止
    if J >= FHeight then Break;

    for I := X to X + AWidth do
    begin
      // 如果横坐标超出地图范围则终止
      if I >= FWidth then Break;

      // 取坐标处的地图信息
      Pt := GetPoint(I, J);

      ImageIndex := Pt.ForeImg and $7FFF; //与计算,,hex的7fff=b
      if ImageIndex > 0 then
      begin
      AniIndex := Pt.AniFrame;
      if (AniIndex and $80 > 0) then AniIndex := AniIndex and $7F;
      if AniIndex > 0 then
          ImageIndex := ImageIndex + (AniCount mod (AniIndex * (Pt.AniTick + 1)))
            div (Pt.AniTick + 1);
      if (Pt.DoorOffset and $80 > 0) and (Pt.DoorIndex and $7F > 0) then
          Inc(ImageIndex, Pt.DoorIndex and $7F);

      // TODO: check value
      if Pt.Area > 6 then
          raise Exception.Create('err');

      G_WilObjects.BitBlt(ImageIndex - 1, DC, (I-X) * 48, (J-Y) * 32);
      end;
    end;
end;
end;

constructor TMirMap.Create(AClientWidth, AClientHeight: Integer); //creat是一个对象的默认初始工作过程
begin
FClientWidth := AClientWidth;
FClientHeight := AClientHeight;
end;

destructor TMirMap.Destroy; //destory是一个对象的默认销毁过程,把向系统申请过的资源还给系统
begin
// 关闭已打开的文件句柄等资源
FileName := '';

inherited;
end;

function TMirMap.GetPoint(X, Y: Word): PMapPoint;
begin
Result := IncPointer(FFilePointer, SizeOf(TMapHeader) + //指针按一定的步长前进,,以取得当前.map文件x,ymappoint坐标处的那个地图格信息(它是一个结构,,拥有前面定义的全部字段如for,,mid,,backimg,,aniframe)
    SizeOf(TMapPoint) * (FHeight * X + Y));

//注意, Mir 的地址存放似乎与一般地图方向不同
//Result := IncPointer(FFilePointer, SizeOf(TMapHeader) +
//    SizeOf(TMapPoint) * (FWidth * Y + X));
end;

procedure TMirMap.SetFileName(const value: string); //动态加载.map文件,,进行前一个已经加载的.map文件与当前正要加载的.map文件之间在内存里的动态切换
begin
// 如果文件名相同则退出
if FFileName = value then Exit; //在这行执行时,,filename当然会有一个先前的值,,代表前一次加载的地图文件名,,在下面(// 保存地图文件名FFileName := value;)处

// 如果已经打开过地图文件, 则先释放先前的文件句柄
if FFileName <> '' then
begin
    UnmapViewOfFile(FFilePointer); //取消文件从硬盘到内存的文件映射过程,,即CreateFileMapping的逆过程
    CloseHandle(FFileMapping); //释放文件在内存的文件指针
    CloseHandle(FFileHandle);//释放文件硬盘文件指针
end;

// 如果文件名为空则退出
if value = '' then Exit;

// 创建文件句柄
FFileHandle := CreateFile(PChar(value), GENERIC_READ, FILE_SHARE_READ, nil,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL or FILE_FLAG_RANDOM_ACCESS, 0);

if FFileHandle = INVALID_HANDLE_value then
    raise Exception.CreateFmt('打开 "%s" 失败!', );

// 创建文件映射
FFileMapping := CreateFileMapping(FFileHandle, nil, PAGE_READONLY, 0, 0, nil);

if FFileMapping = 0 then
begin
    CloseHandle(FFileHandle);
    raise Exception.CreateFmt('创建文件映射 "%s" 失败!', );
end;

// 进行文件映射
FFilePointer := MapViewOfFile(FFileMapping, FILE_MAP_READ, 0, 0, 0);

if FFilePointer = nil then
begin
    CloseHandle(FFileMapping);
    CloseHandle(FFileHandle);
    raise Exception.CreateFmt('映射文件 "%s" 失败!', );
end;

// 读出地图头信息
FWidth := PMapHeader(FFilePointer)^.Width;
FHeight := PMapHeader(FFilePointer)^.Height;
FTitle := PMapHeader(FFilePointer)^.Title;
FUpdateDate := PMapHeader(FFilePointer)^.UpdateDate;

// 保存地图文件名
FFileName := value;
end;

//从setfilename()函数中我们可以提取到:一个.map文件映射入内存的3个步骤前二个步骤是建立文件指针,,第三个步骤也就是最终步骤是实际的映射过程,,,至此,,一个硬盘中的.map文件被映射入内存为程序暂时所用,,,直到下一次的setfilename()下一个.map文件映射入内存

所以释放.map文件的过程也应该先释放二个指针再来一次实际映射的逆过程并且这三个过程跟建立时的顺序是完全颠倒的.

当然在setfilename()中还有一些附加处理过程如保存地图文件名,,读取地图头信息为上下程序所用

procedure TMirMap.DrawBackground(Surface: IDirectDrawSurface7; //画背景即画tiles和smtiles...
CenterX, CenterY, ShiftX, ShiftY: Integer);
var
MapRect: TRect;               // 需要绘制的 MAP 坐标范围
OffsetX, OffsetY: Integer;    // X, Y 左上角偏移
AdjustX, AdjustY: Integer;    // 在绘背景时是否需要调整最左/上行(由于BkImg以偶行/列方式绘制)
I, J: Integer;
Pt: PMapPoint;
ImageIndex: Word;
begin
// 自地图中间至最左/最上的宽度(象素)
I := (FClientWidth - MAPUNIT_WIDTH) div 2 - ShiftX;
J := (FClientHeight - MAPUNIT_HEIGHT) div 2 - ShiftY;

// 计算需要绘制的地图点范围(TMapPoint)
MapRect.Left := Max(0, CenterX - Ceil(I / MAPUNIT_WIDTH));
MapRect.Right := Min(FWidth, CenterX + Ceil((FClientWidth - I) / MAPUNIT_WIDTH));
MapRect.Top := Max(0, CenterY - Ceil(J / MAPUNIT_HEIGHT));
MapRect.Bottom := Min(FHeight, CenterY + Ceil((FClientHeight - J) / MAPUNIT_HEIGHT));

//MapRect.Left := MapRect.Left - MapRect.Left mod 2;
//MapRect.Top := MapRect.Top - MapRect.Top mod 2;

// 计算开始绘制时的偏移值(象素)
OffsetX := I - (CenterX - MapRect.Left) * MAPUNIT_WIDTH;
OffsetY := J - (CenterY - MapRect.Top) * MAPUNIT_HEIGHT;

// 绘制背景 (BkImg)
AdjustX := MapRect.Left mod 2;
AdjustY := MapRect.Top mod 2;
for I := MapRect.Left - AdjustX to MapRect.Right do
for J := MapRect.Top - AdjustY to MapRect.Bottom do
begin
    if (I mod 2 = 0) and (J mod 2 = 0) then
    begin
      Pt := GetPoint(I, J);
      ImageIndex := Pt.BackImg and $7FFF;
      if ImageIndex > 0 then
      begin
      G_WilTile.Draw(ImageIndex - 1, Surface,
          (I - MapRect.Left) * MAPUNIT_WIDTH + OffsetX,
          (J - MapRect.Top) * MAPUNIT_HEIGHT + OffsetY,
          FClientWidth, FClientHeight, False);
      end;
    end;
end;

// 绘制背景补充 (MidImg)
for I := MapRect.Left to MapRect.Right do
for J := MapRect.Top to MapRect.Bottom do
begin
    Pt := GetPoint(I, J);
    ImageIndex := Pt.MiddImg;
    if ImageIndex > 0 then
    begin
      G_WilTileSm.Draw(ImageIndex - 1, Surface,
      (I - MapRect.Left) * MAPUNIT_WIDTH + OffsetX,
      (J - MapRect.Top) * MAPUNIT_HEIGHT + OffsetY,
      FClientWidth, FClientHeight, False);
    end;
end;
end;

procedure TMirMap.DrawForeground(Surface: IDirectDrawSurface7; CenterX, //画前景即objects
//这里要注意在客户端objects.wil文件中的图片的属性,打开它们,你可以发现它们有的为48*32从到48*448不等,,这就是为什么要在开头定义最高为35,,,448=32*14,,,
CenterY, ShiftX, ShiftY: Integer; FirstStep: Boolean);
var
MapRect: TRect;               // 需要绘制的 MAP 坐标范围
OffsetX, OffsetY: Integer;    // X, Y 左上角偏移
I, J: Integer;
Pt: PMapPoint;
InfoPtr: PImageInfo;
ImageIndex: Word;
AniIndex: Byte;
IsBlend: Boolean; //是否需要blend绘制,,,可见objects这些静态显示的图片(相对hum,,mon可以构成动画效果的系列图片),,,有时也需要blend显示,,想像一下,,比如盟重安全区圣诞树上的彩带之类的
begin
// 自地图中间至最左/最上的宽度(象素)
I := (FClientWidth - MAPUNIT_WIDTH) div 2 - ShiftX;
J := (FClientHeight - MAPUNIT_HEIGHT) div 2 - ShiftY;

// 计算需要绘制的地图点范围(TMapPoint)
MapRect.Left := Max(0, CenterX - Ceil(I / MAPUNIT_WIDTH));
MapRect.Right := Min(FWidth, CenterX + Ceil((FClientWidth - I) / MAPUNIT_WIDTH));
MapRect.Top := Max(0, CenterY - Ceil(J / MAPUNIT_HEIGHT));
MapRect.Bottom := Min(FHeight, CenterY + Ceil((FClientHeight - J) / MAPUNIT_HEIGHT) + LONGHEIGHT_IMAGE);

// 计算开始绘制时的偏移值(象素)
OffsetX := I - (CenterX - MapRect.Left) * MAPUNIT_WIDTH;
OffsetY := J - (CenterY - MapRect.Top) * MAPUNIT_HEIGHT;

// 绘制前景 (FrImg)
for I := MapRect.Left to MapRect.Right do
for J := MapRect.Top to MapRect.Bottom do
begin
    Pt := GetPoint(I, J);
    ImageIndex := Pt.ForeImg and $7FFF;
    if ImageIndex > 0 then
    begin
      IsBlend := False;
      AniIndex := Pt.AniFrame;
      if AniIndex and $80 > 0 then
      begin
      IsBlend := True;
      AniIndex := AniIndex and $7F;
      end;
      if AniIndex > 0 then
      begin
      Inc(ImageIndex, (AniCount mod (AniIndex * (Pt.AniTick + 1))) div (Pt.AniTick + 1));
      end;
      if (Pt.DoorOffset and $80 > 0) and (Pt.DoorIndex and $7F > 0) then
      Inc(ImageIndex, Pt.DoorIndex and $7F);

      // TODO: check value
      if Pt.Area > 6 then
      raise Exception.Create('err');

      InfoPtr := G_WilObjects.ImageInfo;

      // 如果图片尺寸=48/32则按正常方式绘制
      if FirstStep then
      begin
      if (InfoPtr^.Width = 48) and (InfoPtr^.Height = 32) then
      begin
          G_WilObjects.Draw(ImageIndex - 1, Surface,
            (I - MapRect.Left) * MAPUNIT_WIDTH + OffsetX,
            (J - MapRect.Top) * MAPUNIT_HEIGHT + OffsetY,
            FClientWidth, FClientHeight, True);
      end
      end
      else begin
      // 如果不是混合方式
      if not IsBlend then
      begin
          if (InfoPtr^.Width <> 48) or (InfoPtr^.Height <> 32) then
            G_WilObjects.Draw(ImageIndex - 1, Surface,
            (I - MapRect.Left) * MAPUNIT_WIDTH + OffsetX,
            (J - MapRect.Top + 1) * MAPUNIT_HEIGHT + OffsetY - InfoPtr^.Height, // 要用减去图片高度
            FClientWidth, FClientHeight, True);
      end
      else
      // 否则, 是混合方式
{          G_WilObjects.Draw(ImageIndex - 1, Surface,
            (I - MapRect.Left) * MAPUNIT_WIDTH + OffsetX + InfoPtr^.PX - 2,
            (J - MapRect.Top) * MAPUNIT_HEIGHT + OffsetY + InfoPtr^.PY - 68,
            FClientWidth, FClientHeight, True)}
          DrawBlend(Surface,
            (I - MapRect.Left) * MAPUNIT_WIDTH + OffsetX + InfoPtr^.PX - 2,
            (J - MapRect.Top) * MAPUNIT_HEIGHT + OffsetY + InfoPtr^.PY - 68,
            FClientWidth, FClientHeight,
            G_WilObjects.Surfaces,
            InfoPtr^.Width, InfoPtr^.Height, 0);
      end;
    end;
end;
end;

function TMirMap.CanFly(X, Y: Word): Boolean;
var
Pt: PMapPoint;
begin
Result := False;

if X >= FWidth then Exit;
if Y >= FHeight then Exit;

Pt := Point;
Result := Pt.ForeImg and $8000 = 0;
if Result then
begin
    if (Pt.DoorIndex and $80 > 0) and (Pt.DoorOffset and $80 = 0) then
      Result := False;
end;
end;

function TMirMap.CanMove(X, Y: Word): Boolean;
var
Pt: PMapPoint;
begin
Result := False;

if X >= FWidth then Exit;
if Y >= FHeight then Exit;
   
Pt := Point;
Result := (Pt.BackImg and $8000 = 0) and (Pt.ForeImg and $8000 = 0);
if Result then
begin
    if (Pt.DoorIndex and $80 > 0) and (Pt.DoorOffset and $80 = 0) then
      Result := False;
end;
end;

A关爱口腔 发表于 2023-4-1 17:49:35

4155445454545
页: [1]
查看完整版本: Mir2引擎之地图说明