c#

位置:IT落伍者 >> c# >> 浏览文章

Visual C#编写3D游戏框架示例


发布日期:2018年04月12日
 
Visual C#编写3D游戏框架示例
你可能对实际地编写游戏代码期待已久了由于DirectX SDK 年夏季更新包含了一个牢固的示例框架组件并且它被设计成能在你自己的代码中直接使用同时还为你处理了很多事务所以你只要简单的使用它就可以节省大量的时间和精力

本文中的例子使用的就是这个示例框架组件在本文中你将学习到的内容有

· 如何建立自己的项目

· 如何使用示例框架组件来列举设备

建立项目

在本文中我假定你的所有开发工作都将使用Visual  来完成如果你不希望使用这个环境可以使用命令行编译代码它允许你使用任意的文本编辑器或集成开发环境(IDE)

启动Visual Studio NET 并点击起始页面中的新建项目按钮如果你没有使用起始页面可以点击文件菜单下的新建子菜单中的项目菜单项或者使用Ctrl+Shift+N选择Visual C#项目区域中的Windows项目数据项把这个项目命名为Blockers这是游戏的名称

在你查看自动生成的代码之前首先把示例框架组件添加到你的项目中一般情况下我会在解决方案浏览器中建立一个新文件夹并把这些文件放入一个这个独立的文件夹中(把这个文件夹的名字取为Framework)右键点击这个新建的文件夹添加菜单中选择添加已有的项导航到DirectX SDK文件夹你会发现该示例框架文件位于Samples\Managed\Common文件夹中选择每个文件并添加到你的项目中

在示例框架组件被添加到项目中以后你就可以去掉自动生成的代码了这些代码中的大部分都是用于建立别致的Windows窗体应用程序的因此它与我们编写游戏的代码是无关的用列表中的代码替换已有的代码和类(Form

列表空的框架组件

using System;

using SystemConfiguration;

using MicrosoftDirectX;

using MicrosoftDirectXDirectD;

using MicrosoftSamplesDirectXUtilityToolkit;

public class GameEngine : IDeviceCreation

{

///程序入口初始化所有部分并进入一个消息处理循环用空闲时间显示场景

static int Main()

{

using(Framework sampleFramework = new Framework())

{

return sampleFrameworkExitCode;

}

}

}

这段新代码中有三个地方比较突出首先你可能注意到了除了静态的main方法之外删除了所有东西而且main方法也被修改过剩余的代码是Windows窗体设计器的支撑代码由于这个应用程序不需要使用该设计器因此这些代码就没有用了可以被删除其次这段代码不能编译因为游戏引擎希望实现的两个接口还未实现再次这段代码实际上没有做任何事务

在你开始解决后面两个问题之前你必须添加一些引用由于你准备在这个项目中显示奇特的D图像你就必须给项目添加能执行这样的显示操作的组件的引用本文采用受控DirectX来执行这种操作因此你需要在项目菜单中选择添加引用显示了弹出的对话框

添加引用对话框

如果你安装了DirectX SDK 年夏季更新你会发现有多个版本的受控DirectX组件可供使用请选择最新的版本(版本)对于这个项目来说你需要添加三个不同的组件引用

· MicrosoftDirectX

· MicrosoftDirectXDirectD

· MicrosoftDirectXDirectDX

DirectX根(root)组件包含了辅助显示计算的数学结构其它两个组件相应地包含了DirectD和DDX的功能添加这些引用之后你可以简单地查看一下列表中添加的using语句以确保名字空间被正确地引用了这个步骤可以确保你不需要完整地限定类型例如如果不添加using语句那么声明一个DirectD设备变量就必须使用下面的语句

MicrosoftDirectXDirectDDevice device = null;

Using语句可以减少很多输入内容(没有人希望在声明一个变量时输入全部的内容)由于你已经增加了using语句就可以使用如下所示的声明语句了

private Device device = null;

你可以看到用这种方式声明变量简单多了节省了大量的输入在了解这些信息之后你可以开始修补应用程序编译过程中的错误了并准备好编写第一个D游戏了你现在必须实现的唯一一个接口是IDeviceCreation它控制着设备的列举和建立

你可能会想列举设备做什么?我只有一个监视器!尽管一般情况下是这样的但是现在的显卡实际上是支持多个监视器的即使你只有一个设备你仍然拥有多个可选择的模式显示器的格式可能不同(你可以在Windows桌面设置中看到这些种类例如位色和位色)全屏幕模式下的高度和宽度也可能有不同的值你甚至于还可以控制屏幕的刷新率总而言之还是有些事情需要解决

列表中的代码修补的应用程序中的编译错误

列表实现接口

/// 设备初始化的时候调用这段代码检查设备最小的性能

/// 如果没有通过检查就返回false

public bool IsDeviceAcceptable(Caps caps Format adapterFormat

Format backBufferFormat bool windowed)

{

if (!ManagerCheckDeviceFormat(capsAdapterOrdinal capsDeviceType

adapterFormat UsageQueryPostPixelShaderBlending

ResourceTypeTextures backBufferFormat))

return false;

if (capsMaxActiveLights == )

return false;

return true;

}

/// 在建立某个设备之前这个回调函数会被立即调用以允许应用程序修改设备的

/// 设置信息它提供的设置参数包含了框架组件为新设备挑选的设置 并且应用程序

/// 可以直接对这个结构进行任何需要的修改请注意示例框架没有纠正无效的

/// 设备设置信息因此必须小心地返回有效的设备设置否则建立设备就会失败

public void ModifyDeviceSettings(DeviceSettings settings Caps caps)

{

// 这个应用程序没有使用任何get方法它被设计成在一个纯设备上工作

// 因此如果受到支持并且使用HWVP就建立一个纯设备

if ( (capsDeviceCapsSupportsPureDevice) && ((settingsBehaviorFlags & CreateFlagsHardwareVertexProcessing) != ) )

settingsBehaviorFlags |= CreateFlagsPureDevice;

}

请你查看一下声明的IsDeviceAcceptable方法在示例框架忙着枚举系统中的设备的时候它会在每个找到的组合上调用该方法注意到该方法返回一个布尔型值吗?这使得你有权利告诉示例框架你认为某个设备是否符合需求但是在你仔细查看第一个方法中的代码的时候请先看一下声明的第二个方法(ModifyDeviceSettings)某个设备被建立之前示例框架会立即调用这个方法允许你加入任何希望的选项但是你要小心地使用参数选项因为它可能导致设备建立失败

现在我们回到第一个方法我们先看一下它的参数首先它带有一个Caps类型的参数它是设备性能的简单结构体该结构体包含了具体设备的巨量信息可以帮助你决定某个类型是不是你正在使用的设备类型其后的两个参数都是特定设备的格式参数一个是后台的缓沖区格式另一个是设备的格式

请注意

后台缓沖区是实际要显示的数据(象素)在发送给显卡处理并输出到屏幕之前所存储的地方后台缓沖区的格式决定了可以显示多少种色彩大多数格式遵循特定的命名习惯每个字符跟着一个数字例如ARGB字符所指定的构成部分拥有与其后面的数字相同数量的位(bit)在ARGB该格式可以包含位色彩信息alpharedgreen和blue各用最常见的构成是

A Alpha

R Red

G Green

B Blue

X Unused

你可以查看DirectX SDK文档得到更多关于格式的信息由于我们还需要知道该设备时候可以显示在窗体中所以这个方法还有一个参数(最后一个)尽管大多数游戏都运行在全屏模式下但是编写和调试在全屏模式下运行的游戏却很困难在调试过程中这个应用程序在窗体模式而不是全屏模式下显示

请注意

窗体模式是我们运行的大多数应用程序的显示方式其中大多数应用程序带有边框和控制菜单右上角带有最小化最大化和关闭按钮在全屏模式下应用程序覆盖了整个屏幕并且在大多数情况下没有边框如果全屏模式使用了另外的屏幕大小(你当前使用的桌面)你可以改变桌面的分辨率

你可能注意到了默认行为是接受该设备但是在接受之前进行了两项检查第一项检查确保了传递进来的设备可以进行alpha混合(游戏的用户界面需要这个)如果它不能够实现就返回false表明这个设备不能被接受接着它检查是否支持活动光源(active light)的能力没有光源的屏幕看起来是平面的是假的因此一般至少需要一个光源

还有一些代码在设备建立之前的确修改了该设备你可能想知道这段代码的作用能够执行处理过程的设备需要用多种方法来显示顶点要么在硬件中计算或软件中计算或者两者都使用如果处理过程完全在硬件中进行这就是另外一种模式就叫做纯硬件设备它潜在地提供了更高的性能这段代码检查你当前是否要建立一个硬件处理设备如果你正准备这样做并且该纯设备是可以使用的那么它就切换到这种模式中你不能建立纯设备(如果该设备是可用的)的唯一情形是你计划调用该设备上的某个get方法或属性由于在这个例子中你不需要这样操作所以你可以自由地使用能力更加强大的设备

示例框架中有一些不安全(unsafe)的代码因此你需要更新自己的项目并处理这些问题见图

允许不安全的代码列举所有设备选项

现在你可以让框架组件开始列举系统中的设备了首先为游戏引擎类声明一个构造函数并把main中建立的示例框架实例作为参数传递进去如列表所示

列表添加构造函数

private Framework sampleFramework = null; // 示例的框架组件

/// 建立该类的一个新的实例

public GameEngine(Framework f)

{

// 存储框架组件

sampleFramework = f;

}

该构造函数除了存储示例框架实例之外没有做其它的任何操作这是因为这个实例是游戏中其它的一切东西几乎都需要使用的在你调用示例框架之后它所做的第一件事情是试图列举系统中的所有设备在你的项目文件中你在Framework文件夹中可以看到dxmutenumcs文件这个文件包含了列举系统中所有设备所需要的全部代码由于理解如何列举和为什么列举设备是非常重要的所以请你打开这个文件

你首先应该注意到Enumeration类自身是不能被创建的并且每个可用的成员和方法都是用static(静态的)关键字声明的由于在正常情况下(最少是现在)应用程序在运行的时候你的图形硬件是不会改变的因此这些列举代码只需要在应用程序开头运行一次

列举工作是从Enumerate方法开始的该方法在设备建立之前被示例框架调用请注意这个方法的唯一参数是你自己在游戏引擎类中所实现的接口这个接口被保存下来因为随后随着设备组合的列举会调用IsDeviceAcceptable方法来决定某个设备是否应该添加到有效设备列表中

那么设备到底是怎样列举出来的呢?这些功能都位于受控DirectX的Manager类中如果你非常熟悉非受控的DirectX应用程序编程接口(API)那么我告诉你这个类映射了IDirectD组件对象模型(COM)接口请留意列表中的Enumerate方法中的第一个循环

列表列举设备

// 查找系统中的每个适配器

for each(AdapterInformation ai in ManagerAdapters)

{

EnumAdapterInformation adapterInfo = new EnumAdapterInformation();

// 存储一些信息

adapterInfoAdapterOrdinal = (uint)aiAdapter; // 序号

adapterInfoAdapterInformation = aiInformation; // 信息

// 获取这个适配器上的所有显示模式

// 建立一个所有显示适配器格式的临时列表

adapterFormatListClear();

// 现在检测支持哪种格式

for(int i = ; i < allowedFormatsLength; i++)

{

// 检查这种格式的每一种可支持的显示模式

for each(DisplayMode dm in aiSupportedDisplayModes[allowedFormats[i]])

{

if ( (dmWidth < minimumWidth) ||

(dmHeight < minimumHeight) ||

(dmWidth > maximumWidth) ||

(dmHeight > maximumHeight) ||

(dmRefreshRate < minimumRefresh) ||

(dmRefreshRate > maximumRefresh) )

{

continue; // 这种格式是无效的

}

// 添加到列表中

adapterInfodisplayModeListAdd(dm);

// 如果先前并不存在就把它添加到格式列表中

if (!adapterFormatListContains(dmFormat))

{

adapterFormatListAdd(dmFormat);

}

}

}

// 获取适配器显示模式

DisplayMode currentAdapterMode = aiCurrentDisplayMode;

// 检查这种格式是否在列表中

if (!adapterFormatListContains(currentAdapterModeFormat))

{

adapterFormatListAdd(currentAdapterModeFormat);

}

// 对显示模式列表进行排序

adapterInfodisplayModeListSort(sorter);

// 获取这个适配器上每个设备的信息

EnumerateDevices(adapterInfo adapterFormatList);

// 如果适配器上至少有一个设备并且它是兼容的就把它添加到列表中

if (adapterInfodeviceInfoListCount > )

{

adapterInformationListAdd(adapterInfo);

}

}

Manager类的Adapters(适配器)属性是一个包含了系统中所有适配器信息的集合适配器这个术语可能有点不恰当但是它的基本定义是指任何监视器可以连接到的东西例如假设你有一块ATI Radeon XT显卡虽然只有一块显卡但是可能把两个不同的监视器连接到它上面(通过视频图形适配器[VGA]端口和后面的数字视觉接口[DVI]端口)当用这两种监视器的时候这块显卡就有两个适配器因此是两种设备

请注意

这是一种通过把设备创建为适配器组的方式在不同的设备之间共享资源的方法这种方法受到了少许限制你可以查阅DirectX文档了解更多的信息

这个循环至少会迭代一次这依赖于你的系统在把当前活动的适配器的基本信息存储起来以后代码必须找到在全屏模式下这个适配器可以支持的所有显示模式你可能发现了受到支持的模式都可以直接从当前正在列举的适配器信息中直接列举出来代码也是这样做的

列举某个适配器模式的时候第一步是检查最小和最大的范围集合大多数设备支持很多模式但是其中很多我们现在不会使用了很多年前你可能见过在x全屏窗口中运行游戏但是现在不会发生这种情况(除非你正好在玩手持式游戏例如Gameboy Advance)示例框架选择的最小的大小为x窗体没有设置最大的尺寸

请注意

示例框架选择的最小尺寸为x并不意味着在全屏模式下它就会选择最小的尺寸在全屏模式下示例框架选择最好的可用尺寸它一般是当前桌面的大小(通常不是x的)

在符合框架组件需求的受到支持的模式被添加到列表中后当前的显示模式就会被添加进来因为这个模式肯定受到支持最后通过实现IComparer接口这些模式会被排序见列表

列表对显示模式进行排序

public class DisplayModeSorter : IComparer

{

/// 比较两种显示模式

public int Compare(object x object y)

{

DisplayMode d = (DisplayMode)x;

DisplayMode d = (DisplayMode)y;

if (dWidth > dWidth)

return +;

if (dWidth < dWidth)

return ;

if (dHeight > dHeight)

return +;

if (dHeight < dHeight)

return ;

if (dFormat > dFormat)

return +;

if (dFormat < dFormat)

return ;

if (dRefreshRate > dRefreshRate)

return +;

if (dRefreshRate < dRefreshRate)

return ;

// 它们一定相同所以返回

return ;

}

}

IComparer接口允许我们在数组或集合上执行简单的快速排序算法这个接口提供的唯一的方法是Compare它必须返回整型值也就是如果左边的数据项大于右边的就返回+如果左边的数据项小于右边的就返回如果相等就返回你可以看到在上面的实现中显示模式的宽度有最高的优先级接着是高度格式和刷新率这个次序规定了在比较两种模式(例如xx)的时候正确的操作方法

这些模式被排序之后就调用EnumerateDevices方法列表显示了这个方法

列表列举设备类型

private static void EnumerateDevices(EnumAdapterInformation adapterInfo

ArrayList adapterFormatList)

{

// 在查找设备类型的时候忽略任何异常

DirectXExceptionIgnoreExceptions();

// 列举每个DirectD设备类型

for(uint i = ; i < deviceTypeArrayLength; i++)

{

// 建立一个新设备信息对象

EnumDeviceInformation deviceInfo = new EnumDeviceInformation();

// 存储该类型

deviceInfoDeviceType = deviceTypeArray[i];

// 试图获取其性能

deviceInfoCaps = ManagerGetDeviceCaps((int)adapterInfoAdapterOrdinal deviceInfoDeviceType);

// 获取该设备上每个设备组合的信息

EnumerateDeviceCombos( adapterInfo deviceInfo adapterFormatList);

// 我们有设备组合吗?

if (deviceInfodeviceSettingsListCount > )

{

// 有把它添加到列表中

adapterInfodeviceInfoListAdd(deviceInfo);

}

}

// 打开异常处理开关

DirectXExceptionEnableExceptions();

}

查看这段代码的时候你必须注意两个非常重要的信息你能猜到是哪两个吗?如果你猜的是对DirectXException类的调用那就对了第一个调用关闭了受控DirectX部件内部任何异常的产生你可能会怀疑这样做的优点实际上这样做是出于性能的考虑捕捉和抛出异常是很昂贵的操作而这段代码可能产生大量的异常你可能希望尽快地执行列举过程因此过程中产生的任何异常都被简单地忽略了在这个 函数执行完之后就恢复正常的异常处理过程这段代码看起来相当简洁你可能会问这段代码为什么倾向于产生异常呢

我有一个很好的答案大多数情形是某种设备不支持DirectX 也许你没有升级显卡驱动程序或当前的显卡驱动程序所需要的必要代码路径不正确也可能是由于显卡本身太老了没有能力支持DirectX 有时候一些人通过包含不支持DirectX 的PCI显卡激活了系统中的多监视器模式

这个方法中的代码试图得到这个适配器的性能信息并列举出不同的组合方式并且它试图获取每个可用的设备的这些信息可能的设备类型包括

· 硬件(Hardware)建立的最常见的设备类型呈现过程由硬件(显卡)来完成

· 引用(Reference)这种设备不管硬件是否能够执行处理过程可以呈现DirectD运行时支持的任何设置所有的处理过程在软件中进行这意味着在游戏中这种设备类型很慢

· 软件(Software)除非你编写了光栅化程序(rasterizer)否则永远不会使用这个选项

假设在列举过程中找到了某些设备组合就把它存储到列表中列举类存储了少量的列表示例框架在以后可以使用它们列表是EnumerateDeviceCombos方法

列表列举设备组合

private static void EnumerateDeviceCombos(EnumAdapterInformation adapterInfo

EnumDeviceInformation deviceInfo ArrayList adapterFormatList)

{

// 查找这种设备支持哪种适配器格式

for each(Format adapterFormat in adapterFormatList)

{

for(int i = ; i < backbufferFormatsArrayLength; i++)

{

bool windowed = false;

do

{

if ((!windowed) && (adapterInfodisplayModeListCount == ))

continue;

if (!ManagerCheckDeviceType((int)adapterInfoAdapterOrdinal

deviceInfoDeviceType adapterFormat

backbufferFormatsArray[i] windowed))

continue; // 不支持的

// 我们需要加速象素阴影混合吗?

if (isPostPixelShaderBlendingRequired)

{

if (!ManagerCheckDeviceFormat(

(int)adapterInfoAdapterOrdinal

deviceInfoDeviceType adapterFormat

UsageQueryPostPixelShaderBlending

ResourceTypeTextures backbufferFormatsArray[i]))

continue; // 不支持的

}

// 如果提供了某个应用程序回调函数就要确保这个设备受到该应用程序的支持

if (deviceCreationInterface != null)

{

if (!deviceCreationInterfaceIsDeviceAcceptable(deviceInfoCaps

adapterFormat backbufferFormatsArray[i]windowed))

continue; // 应用程序不喜欢这个设备

}

EnumDeviceSettingsCombo deviceCombo = new EnumDeviceSettingsCombo();

// 存储信息

deviceComboAdapterOrdinal = adapterInfoAdapterOrdinal;

deviceComboDeviceType = deviceInfoDeviceType;

deviceComboAdapterFormat = adapterFormat;

deviceComboBackBufferFormat = backbufferFormatsArray[i];

deviceComboIsWindowed = windowed;

BuildDepthStencilFormatList(deviceCombo);

BuildMultiSampleTypeList(deviceCombo);

if (deviceCombomultiSampleTypeListCount == )

{

continue;

}

BuildConflictList(deviceCombo);

BuildPresentIntervalList(deviceInfo deviceCombo);

deviceComboadapterInformation = adapterInfo;

deviceCombodeviceInformation = deviceInfo;

// 把组合添加到设备列表中

deviceInfodeviceSettingsListAdd(deviceCombo);

windowed = !windowed;

}

while (windowed);

}

}

}

总结

在本文中你开始建立了第一个游戏项目并且看到了示例框架的一些内容你看到了大量的列举系统中可能支持的设备组合的代码这个示例框架是你在未来编写游戏的一个重要的出发点               

上一篇:ADO.NET3.5的高级特性简介

下一篇:Visual Basic.NET中组件的叠加使用