量子前锋(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/quantum-striker-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 46 分钟阅读 - 23045 个词 阅读量 0量子前锋(译文)
原文地址:https://www.codeproject.com/Articles/796242/Quantum-Striker
原文作者:Florian Rappl
译文由本站 robot-v1.0 翻译
前言
Architecture, design and implementation of a cross-platform Windows Desktop / Windows Store space shoot-em-up game.
跨平台Windows桌面/Windows Store空间射击游戏的体系结构,设计和实现.
目录(Table of Contents)
介绍(Introduction)
前段时间,我和一位同事参加了2013年英特尔应用创新大赛(AIC).尽管游戏类别不像其他类别那样可预测,并且依赖(经典)编程技能,但我们还是尽力克服了图形缺陷.最后,我们设法提供了一款有趣的游戏,使其至少进入了比赛的前五名.我们对成功感到非常高兴.(A while ago a colleague and I participated in the Intel App Innovation Contest (AIC) 2013. Even though the games category is not as predictable and depending on (classical) programming skills as other categories, we tried our best to overcome our graphical deficiencies. In the end we managed to deliver a fun game that made it at least to the Top-5 games of the competition. We’ve been quite happy about that success.) 从那时起,我们计划在CodeProject上发布游戏.显然花了一段时间,但这里是相关文章.在接下来的几段中,我将指导您完成游戏,逻辑,设计决策和分析.整个文章将在跨平台应用程序的上下文中进行讨论.当然我们现在有能力写(From that moment on we planned on releasing the game on the CodeProject. Obviously it took a while, but here is the associated article. During the next paragraphs I will guide you through the game, the logic, as well as design decisions and an analysis. The whole article will be discussed in the context of cross-platform applications. Of course we now have the ability to write)**普遍(universal)**应用,即支持我们开发者之间共享代码的应用,例如但是,要在Windows应用商店和Windows(WPF)之间跨平台运行Windows Phone 8和Windows 8应用仍然不是那么容易.(apps, i.e. applications that support us developers in sharing code between, e.g. Windows Phone 8 and Windows 8 apps, however, being cross-platform between Windows Store and Windows (WPF) is still not that easy.) 我们将看到如何使用可移植类库作为共享代码的坚实基础.本文还将演示使代码可通过.NET-Framework进行移植的有效方法(效率不高).最后,我希望展示一种可扩展,可移植且灵活的体系结构.(We will see how we can use portable class libraries as a solid foundation for sharing code. The article will also demonstrate efficient (and not-so-efficient) ways for making code portable with the .NET-Framework. Finally I hope to demonstrate an architecture that is extensible, portable and flexible.)
背景(Background)
开始为CodeProject撰写文章后不久,我创建了一个名为SpaceShoot的游戏.我在几个小时内将它作为演示项目编写,用于我在HTML5上的演讲.然而,事实证明这是一个非常有趣的项目,并在CodeProject读者,我的学生和朋友中广受欢迎.对于AIC 2013,我们决定制作一个翻拍游戏-值得继承的游戏.我们选择的平台不是HTML5,而是WPF.主要原因是与触摸层的交互.(Shortly after I started writing articles for the CodeProject I created a game called SpaceShoot. I wrote it in a couple of hours as a demo project for a lecture I was giving on HTML5. Nevertheless it turned out to be quite a fun project and has been popular among CodeProject readers, my students and friends. For the AIC 2013 we decided to create a remake - a game that is worth being a successor. Instead of HTML5 our platform of choice has been WPF. The main reason for this was the interaction with the touch layer.) 回想起来,这不是最好的决定.我们花了一个多星期才弄清楚如何使用WPF有效地进行多点触摸.原来这是一个古老的话题,有未解决的问题.其中一些问题来自WPF的设计选择,一些来自我们自己的盲目性.最终,我们有了一个可靠的解决方案,但是花了我们比预期更长的时间.但是,WPF的好处是,该游戏已放置在具有强大灵活性和可扩展性的真正可靠的代码库上.(In retrospect this was not the best decision. We lost more than a week figuring out how to do multi-touch efficiently with WPF. It turned out to be an old topic with unsolved issues. Some of those issues came from WPF design choices, some from our own blindness. In the end we had a solid solution, but it took us longer than expected. The good thing about WPF was, however, that the game has been placed on a really solid code base with great flexibility and extensibility.) 我们想要创建最终的小行星射手模拟.实施物理方法后不久,我们不得不实施辅助程序,因为事实证明游戏很难控制.每次激光射击,每次碰撞或加速都会产生一些非常现实的影响.应对这些影响的累积对于任何玩家而言都是艰巨的挑战.没有玩家的适当反应,对船的控制不可避免地会丢失.我们实施了惯性阻尼器,转向助力器及其他工具,只是为了像控制汽车一样控制太空船.这与原始游戏中的行为相同,但是我们更加现实,可以在其他几种情况下使用.(We wanted to create the ultimate asteroid shooter simulation. Shortly after we implemented the physics we had to implement helpers, since the game turned out to be very hard to control. Every laser shot, every collision or acceleration had some very realistic impacts. To counter the accumulation of those impacts turned out to be a tough challenge for any player. Without a proper reaction of the player the control over the ship was lost inevitably. We implemented inertia dampers, steering helpers and others, just to control the space ship as if it would be a car. This is the same behavior as in the original game, however, we are more realistic and that can be used on several other occasions.) 该船包含损坏模型,然后可以将其打开以禁用标准船用系统.这种系统的一个例子是所提到的惯性阻尼器.如果通过激光射击,碰撞或其他影响将其关闭,则惯性效应将不会受到抑制.在详细讨论设计的后果(优点和问题)之前,我们将首先了解应用程序的技术方面.(The ship contains a damage model, which then could be turned on to disable standard ship systems. An example of such a system would be the mentioned inertia dampers. If those are turned off by laser shots, collisions or other influences, the inertia effects won’t be suppressed. Before we discuss the consequences (advantages and problems) of our design in detail, we will first have a look at the technical side of our application.)
游戏引擎:量子引擎(The game engine: Quantum Engine)
每个游戏都需要某种游戏引擎.即使是非常简单的游戏,也可能具有一些可以视为游戏引擎的核心.对于实时游戏,这比其他类型的游戏更容易识别.(Every game requires some kind of game engine. Even very simple games may have some core that could be considered a game engine. For real time games this is much easier to identify than for other kind of games.) 基本上,游戏引擎包含两个重要的块:(Basically a game engine consists of two important blocks:)
-
逻辑管理器,通常按固定的时间间隔触发.(A logic manager, usually coupled to fire at fixed time intervals.)
-
资产管理-即绘制图形或播放一些声音.这是实时行为.(Asset management - i.e. drawing graphics or playing some sounds. This acts in real-time.) 保持这些组件尽可能地分离不仅是一个好习惯.这也将导致更加干净的游戏设计.我目睹了学生,他们开始混合使用这些组件,并最终陷入某种形式的编码地狱.如果您的逻辑取决于特定的图形状态,那么您基本上会迷路.例如:如果当前正在发射激光,则不应该取决于当前的绘制状态.我们是否已经显示了激光图像都没有关系.它是否被解雇.(It is not only a good practice to keep these components as decoupled as possible. It will also result in a much cleaner game design. I’ve witnessed students, who started mixing these components and ended up in some kind of coding hell. If you logic depends on a certain graphics state, you are basically lost. For instance: If a laser is currently shot should not depend on the current draw state. It does not matter if we already show an image of the laser. It is either fired or not.) 另一方面,当对象仍然被绘制时,它们可能已经超出了我们的逻辑.考虑很少的图形效果,例如灰尘,爆炸或其他效果.相应的对象已从逻辑中删除.它无法再进行交互.然而,它的遗骸仍然可见.(On the other side objects might already be out of our logic, when they are still drawn. Consider little graphic effects such as dust, explosions or other effects. The corresponding object has already been removed from the logic. It cannot interact any more. However, its remains are still visible.) 逻辑管理器与视觉动作分开也很重要.游戏始终希望尽可能多地重绘屏幕.也许由于某些节能的需求,希望降低帧速率,但这可以通过仅限制用于硬件加速图形的GPU或用于(部分)软件渲染的CPU来自然实现.不用说,这些决定不应对逻辑产生影响.因此,我们希望游戏在1 GHz Intel i3 CPU和3 GHz Intel i7 CPU上具有相同的逻辑.操作系统负责在固定时间范围内给我们回调.(It is also important that the logic manager acts separately from the visuals. Games always want to redraw the screen as often as possible. Maybe one wants to throttle down the frame-rate due to some energy saving desire, but that would be achieved naturally by just limiting the GPU for hardware accelerated graphics or the CPU for (partial) software rendering. Needless to say that these decisions should have no impact on the logic. Therefore we want the game to have the same logic on a 1 GHz Intel i3 CPU as well as on a 3 GHz Intel i7 CPU. The operating system is responsible for giving us a callback at fixed time spans.) 该逻辑负责各种任务:(The logic is responsible for a variety of tasks:)
-
管理称为实体的对象.这包括添加,删除或检查实体是否仍然存在.(Managing the objects, called entities. That includes adding, removing or checking if the entity is still alive.)
-
更新每个实体的逻辑.(Updating the logic of each entity.)
-
管理控制器,例如键盘和触摸屏.(Managing the controllers, e.g., keyboard and touch.)
-
检查实体之间的冲突.(Checking for collisions between entities.)
-
为资产管理提供桥梁,以实现分离.(Providing a bridge to the asset management, to enforce separation.) 最后一点听起来与资产管理中分离逻辑的一般概念相矛盾.但是,我们将看到这对于分离至关重要.(The last point sounds like a contradiction to the general concept of decoupling logic from asset management. However, we will see that this is crucial for the separation.) 仅当有通用管道将数据传递到独立的工作单元时,分离才起作用.但是在我们的情况下,这不太容易实现,因为这样的管道需要以渲染引擎或逻辑引擎的速度工作.(The separation works only if there is a general pipe that passes data to independent worker units. But in our case this is not so easily possible, since such a pipeline would be required to work either at the speed of the rendering engine, or the logic engine.) 如果此管道以渲染引擎的速度工作,它将一直触发.这样,我们将无法执行逻辑引擎(至少不是每次执行时).另一方面,如果管道将以逻辑引擎的速度工作,则将需要逻辑来等待渲染完成.另外,渲染将不会尽可能快,这可能会导致播放器的体验不令人满意.(If this pipeline would work at the speed of the rendering engine, it would fire all the time. We would then not be able to execute the logic engine (at least not at every execution). On the other side if the pipe would work at the speed of the logic engine, the logic would be required to wait for the rendering to complete. Additionally the rendering would not be as fast as possible, probably resulting in an unsatisfactory experience for the player.) 因此,不幸的是不可能采用整个管道方法.我们有两个管道-一个可以称为逻辑引擎,另一个可以称为图形或渲染引擎.这些引擎从何处获取数据?好吧,我们可以使其中一个自我维持.当然,我们选择逻辑引擎,因为我们更喜欢显示逻辑.(Therefore the whole pipeline approach is unfortunately not possible. We have two pipelines - one that may be called logic engine and another that may be called graphic or rendering engine. Where do these engines get the data from? Well, we can make one of them self-sustained. Of course we choose the logic engine, as we prefer logic to display.) 本质上,这种方法使渲染引擎依赖于逻辑引擎-但仅依赖于来自逻辑引擎的数据.我们可以使此依赖关系尽可能小,但它将一直存在.因此,我们的逻辑引擎还需要一种直接与图形引擎对话的方法.所有这些都已集成到量子引擎的设计中.(In essence such an approach makes the rendering engine dependent on the logic engine - but only on the data from the logic engine. We can make this dependency as small as possible, but it will be there. Therefore our logic engine also needs a way to directly speak to the graphic engine. All this has been integrated into the design of the quantum engine.)
游戏(The game)
游戏包含一组可用模式.原则上,添加另一种模式非常容易.最初,我们提供了以下模式集:(The game contained a set of available modes. In principle it was quite easy to add another mode. Initially we offered the following set of modes:)
- 单人战役(A single-player campaign)
- 多人游戏(如死亡竞赛)模式(A multi-player (like deathmatch) mode)
- 团队模式(A team mode)
竞选模式包含一个非线性的故事情节,其中某些行动和决定影响了即将执行的任务或改变了所采取的故事路径.即使设计这样的系统需要做更多的工作(需要更多的关卡,根据某些条件改变任务目标的逻辑,还必须执行更多的逻辑),但绝对可以从游戏的乐趣和灵活性上获得回报.(The campaign mode contained a non-linear story-line, where certain actions and decisions influenced the upcoming mission or changed the taken story path. Even though designing such a system is a lot more work (a lot more levels are required, logic for changing mission objectives depending on certain conditions and more has to be implemented), it definitely pays off in terms of fun and flexibility of the game.)
该运动还允许通过基于文本的简报介绍短期任务目标.所有这些情况都可以在引擎内进行处理,无需任何破解或变通方法.以下屏幕截图显示了这样的简报.(Also the campaign allowed to introduce short mission objectives with text based briefings. All these scenarios can be handled within the engine, there is no hack or work-around required. The following screenshot shows such a briefing in action.)
在<量子前锋>中,玩家所使用的舰只的健康点,护盾和弹药有限.该船可能装有可能用于引爆的炸弹.此外,弹药仅限于激光射击.可以升级主要(激光)武器.(In Quantum Striker the player is using a ship that has limited health points, shields and ammo. The ship may contain a set of bombs that might be used to detonate. Additionally the ammo is limited for laser shots. The primary (laser) weapon can be upgraded.)
- 有小行星和AI控制的敌舰.尽管可以避免使用小行星,但必须对AI控制的飞船进行射击.否则,它们很可能会对玩家的船只造成严重损害.(There are asteroids and AI controlled opponent ships. While asteroids might be avoided, AI controlled ships have to be shot. Otherwise they are very likely to perform serious damage to the player’s ship.)
规则,图形和声音(Rules, graphics and sounds)
一些图形直接基于XAML,但是大多数图形都存储为位图.这里有各种各样的船只.例如,在以下屏幕截图中,我们看到了无人驾驶飞机的母舰(右侧,基本上是所谓的博格立方体),球形无人机(看起来像小行星,但带有健康条)和标准无人机.(Some graphics are directly based on XAML, but most graphics are stored as bitmaps. There is a huge variety of ships included. For instance in the following screenshot we see a drone mothership (right side, basically what is known as a Borg cube), spherical drones (looking like asteroids, but with health bars) and standard drones.)
玩家的飞船只有位图图形可用.为了支持多个播放器,此位图图形以多种颜色(红色,绿色,黄色,蓝色等)存在.可以在XAML中完成所有操作,但是最后,位图的详细程度和简单性是我们坚持使用位图方法的原因.(The player’s ship is only available is a bitmap graphic. For supporting multiple players, this bitmap graphic exists in multiple colors (red, green, yellow, blue, …). It would have been possible to do it all in XAML, but in the end the level of detail and simplicity of a bitmap have been our reason for staying with the bitmap approach.) 不过,对于玩家的HUD(平视显示器),我们希望显示一个基本上能说明飞船的富有表现力的图形.在此图形中,我们显示当前的弹药,工作系统和可用炸弹.该图形应以播放器的颜色显示.(Nevertheless for the player’s HUD (heads up display) we wanted to show an expressive graphic basically illustrating the ship. In this graphic we display the current ammo, working systems and available bombs. This graphic should be shown in the player’s color.) 由于此图形基本上只是一条带有可变信息的线,因此选择XAML作为我们的实现语言是一个显而易见的选择.因此,HUD的定义如下:(Since this graphic is basically just a line with variable information, it was an obvious choice for choosing XAML as our language of implementation. The HUD therefore is defined as follows:)
<UserControl x:Class="QuantumStriker.Xaml.Hud"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="120"
Width="200">
<Viewbox>
<Canvas Height="150" Width="250">
<Path Data="M0,0 L50,0 L50,20 L70,35 L90,15 L160,15 L180,50 L230,50 L230,55 L250,55 L250,65 L230,65 L230,70 L180,70 L160,105 L90,105 L70,85 L50,100 L50,120 L0,120 L0,90 L20,90 L20,75 L0,75 L0,45 L20,45 L20,30 L0,30 Z"
x:Name="HullFrame"
Stroke="SteelBlue"
StrokeThickness="1"/>
<Path Data="M100,40 L110,30 L140,30 L150,50 L150,70 L140,90 L110,90 L100,80 Z"
x:Name="CoreFrame"
Stroke="DarkGray"
StrokeThickness="1"/>
<Path Data="M110,50 L140,50 L140,70 L110,70 Z"
x:Name="CockpitFrame"
Stroke="DarkGray"
StrokeThickness="1"/>
<Image x:Name="Battery"
Canvas.Left="109"
Canvas.Top="44"
Width="32"
Height="32" />
<Path Data="M60,55 L60,65 L35,80 L45,65 L45,55 L35,40 Z"
x:Name="BombFrame"
Stroke="LightGray"
StrokeThickness="1" />
<Path Data="M180,50 L230,50 L230,55 L250,55 L250,65 L230,65 L230,70 L180,70 L190,60 Z"
x:Name="LaserFrame"
StrokeThickness="0" />
<TextBlock Text="0"
FontFamily="../../Fonts/#Acknowledge TT BRK"
x:Name="BombText"
FontSize="28"
TextAlignment="Center"
Foreground="SteelBlue"
Width="30"
Canvas.Left="67"
Canvas.Top="45" />
<TextBlock Text="1000"
FontFamily="../../Fonts/#Acknowledge TT BRK"
x:Name="AmmoText"
FontSize="28"
TextAlignment="Right"
Width="60"
Foreground="SteelBlue"
Canvas.Left="190"
Canvas.Top="80" />
<TextBlock Text="The main reactor is broken."
x:Name="Message"
FontWeight="Light"
FontSize="16"
TextAlignment="Center"
Width="250"
Foreground="SteelBlue"
Canvas.Left="0"
Canvas.Top="125" />
</Canvas>
</Viewbox>
</UserControl>
基本上我们使用(Basically we use a) Viewbox
提供可伸缩性.的(to provide scalability. The) UserControl
然后使用(then uses a) Canvas
用于绘制将是船轮廓的路径.各种文本块和图像放置在有趣的位置.我们不使用任何绑定(所有内容都将在后面的代码中设置,并且我们的游戏引擎不了解WPF),并且该信息只能在更新步骤中进行更新.这将由游戏引擎完成,这就是我们完全规避MVVM绑定的原因.(for drawing a path that will be the outline of the ship. Various text blocks and images are placed on interesting positions. We do not use any binding (everything will be set in the code behind and our game engine does not know about WPF) and the information can only be updated in the update step. This will be done by the game engine, which is why we circumvent MVVM binding altogether.)
游戏的布局经过设计,可将控件显示在屏幕上.因此,为使用触摸屏的玩家提供了触摸控件-它们以半透明的方式呈现.同样,HUD显示为具有一定的透明度,以使其尽可能不引人注目.(The layout of the game has been designed in such a way, that the controls will be displayed on the screen. Therefore the touch controls are present for players that use the touch screen - and they are presented in a semi-transparent way. Also the HUD is shown with some transparency, to be as unobtrusive as possible.)
下一个屏幕截图说明了多人游戏的开始.在这里,我们选择一个有4个玩家的场景.这是当前的最大值,通常可以通过实现基于TCP/IP或网络连接的多人游戏模式来增加此数量.(The next screenshot illustrates the beginning of a multiplayer match. Here we choose a scenario with 4 players. This is currently the maximum, which might be increased by implementing a multi-player mode that is based on TCP/IP or network connections in general.)
我们看到标准的触摸控件包括两个触发按钮(左侧,左侧按钮触发激光,右侧按钮为弹出炸弹按钮)和某种位置字段.后者可以与手指一起使用.在这种情况下,它基本上代表转向/加速圆.通常,此面板将用于在其上放置特殊类型的触摸操纵杆.(We see that the standard touch control consists of two fire buttons (left side, with the left button firing the laser and the right one being the eject bomb button) and some kind of position field. The latter could be used together with the finger. In this case it basically represents something like a steering / accelerating circle. Usually this panel would be used to place a special kind of touch-joystick on it.) 此操纵杆面板以及其他触摸控件均使用纯XAML进行了重新设计.面板的XAML代码在下一个代码片段中给出.(This joystick panel, as well as the other touch controls, have been designed again with pure XAML. The XAML code for the panel is given in the next code snippet.)
<UserControl x:Class="QuantumStriker.Xaml.JoystickPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="100"
Width="100">
<UserControl.Resources>
<SolidColorBrush x:Key="Inner"
Color="#55CCCCCC" />
<SolidColorBrush x:Key="Outer"
Color="#AABBBBBB" />
<SolidColorBrush x:Key="Line"
Color="#AAFF0000" />
</UserControl.Resources>
<Grid>
<Ellipse Margin="0"
StrokeThickness="4"
Stroke="{StaticResource Outer}">
<Ellipse.Clip>
<GeometryGroup>
<PathGeometry>
<PathFigure StartPoint="10,0" IsClosed="True">
<LineSegment Point="50,50" />
<LineSegment Point="90, 0" />
</PathFigure>
</PathGeometry>
<PathGeometry>
<PathFigure StartPoint="0,10" IsClosed="True">
<LineSegment Point="50,50" />
<LineSegment Point="0, 90" />
</PathFigure>
</PathGeometry>
<PathGeometry>
<PathFigure StartPoint="10,100" IsClosed="True">
<LineSegment Point="50,50" />
<LineSegment Point="90, 100" />
</PathFigure>
</PathGeometry>
<PathGeometry>
<PathFigure StartPoint="100,10" IsClosed="True">
<LineSegment Point="50,50" />
<LineSegment Point="100, 90" />
</PathFigure>
</PathGeometry>
</GeometryGroup>
</Ellipse.Clip>
</Ellipse>
<Ellipse Margin="20"
Fill="{StaticResource Inner}" />
<Line X1="7" Y1="7"
X2="23" Y2="23"
x:Name="Line1"
Stroke="{StaticResource Line}" />
<Line X1="93" Y1="93"
X2="77" Y2="77"
x:Name="Line2"
Stroke="{StaticResource Line}" />
<Line X1="93" Y1="7"
X2="77" Y2="23"
x:Name="Line3"
Stroke="{StaticResource Line}" />
<Line X1="7" Y1="93"
X2="23" Y2="77"
x:Name="Line4"
Stroke="{StaticResource Line}" />
</Grid>
</UserControl>
声音有点复杂.基本上,我们在声音效果(例如激光镜头)和背景音乐之间有所不同.效果存储为波形文件时,背景声音存储为MP3文件.这是合乎逻辑的,因为效果很小,并且播放效果应该尽可能直接.任何解码工作都只是开销,特别是因为文件的大小不会有太大变化.对于背景声音,情况并非如此.此处的大小至少要高出10倍(取决于所选的比特率).(Sounds is a little bit more complicated. Basically we differ between sound effects (such as laser shots) and background music. While effects are stored as wave files, background sounds are stored as MP3 files. This is just logical, as effects are very small and playing them should be as direct as possible. Any decoding efforts would be just overhead, especially since the size of the file wouldn’t change much. This is not true for background sounds. Here the size would be at least a factor 10 higher (depending on the selected bitrate).)
播放声音/音乐的实现实际上是特定于平台的.因此,我们使用仅定义声音系统外观的抽象层.然后,特定于平台的层将创建此类系统的具体实现.(The implementation of playing the sounds / music is really platform specific. We therefore use an abstraction layer that just defines how the sound system looks like. Then the platform specific layer creates a concrete implementation of such a system.)
对于WPF,我们将声音系统实现基于著名的NAudio库.这是一个广泛的,几乎完整的库,用于播放,录制和处理声音.我们仅在解码(针对mp3)和播放(针对所有内容)声音流方面很有趣.(For WPF we based the sound system implementation on the well-known NAudio library. This is an extensive, nearly-complete library for playing, recording and manipulating sounds. We are only interesting in decoding (for mp3) and playing (for everything) sound streams.)
对于Windows Store应用程序,我们使用SharpDX.这为XAudio提供了一个很好的抽象层,它是Windows(8)API的一部分,可以通过DirectX进行访问.(For the Windows Store app we use SharpDX. This provides a nice abstraction layer to XAudio, which is a part of the Windows (8) API and could be accessed over DirectX.)
下图提供了有关游戏的更多信息.单击以原始分辨率打开屏幕截图.(More information about the game is available on the picture below. Click to open the screenshot in its original resolution.)
逻辑与物理(Logic and physics)
引擎已经定义了什么是实体,以及任何实体应包含哪些基本属性.但是,除了表示与(2D)游戏世界的基本连接并包含对象的当前状态(有效,无效)外,它实际上并没有表达与给定对象有关的任何特殊内容.(The engine already defines what an entity is, and what base properties any entity should contain. Nevertheless, besides representing a basic connection to the (2D) game world, and containing the current state (alive, dead) of the object, it does not really express anything special about the given object.)
因此,我们需要从实体中派生出定义所要处理的对象的类型.对我们来说幸运的是,引擎已经为继承提供了有趣的起点.例如一个(So we need to derive from the entity to define what kind of objects we are dealing with. Lucky for us the engine already offers interesting start points for inheritance. For instance an) OrientableObject
包含所有可定向的内容.(that contains everything to be orientable.)
如果我们对给定的类不满意,我们可能会从头开始.最后,只有实现正确的接口才重要.在我们的例子中,我们从继承(If we are not satisfied with the given classes, we might just start from scratch. In the end it only matters if we implemented the correct interfaces. In our case we started by inheriting from) OrientableObject
创建另一个重要的基类(to create yet another important base class called) MoveableObject
.这以一种速度扩展了方向.然后,此速度仅在每个更新步骤中被添加到该位置.(. This one extends the orientation with a kind of velocity. This velocity then just gets added to the location in every update step.)
此外,我们还包括(Additionally we also include) AngularVelocity
.在这里我们利用(. Here we make use of the) Orientation
财产,由(property, that is offered by the) OrientableObject
基类.(base class.)
abstract class MoveableObject : OrientableObject
{
// ...
public override void Update(IGame g)
{
base.Update(g);
Location += Velocity;
Orientation += AngularVelocity;
if (collisionCoolDown > 0)
collisionCoolDown--;
}
}
继续前进,我们可能会为我们的几个实体专门设置该基类.一个特殊的实体组是所有船只的组.在这里,我们还有其他专长.因此,对于该组也有一个通用的基类是有意义的.(Moving on we might specialize this base class for several of our entities. A special group of entities is the group of all ships. Here we have additional specializations. Therefore it makes sense to have a common base class for this group as well.)
我们称这个基类(We call this base class) ControlledShip
并实现几个接口,这些接口也可以用来装饰其他(非船用)实体.这些接口中的某些接口对于游戏引擎也特别有意义.例如(and implement several interfaces, that might be used to decorate other (non-ship) entities as well. Some of these interfaces are also of particular interest for the game engine. For instance the) IDamageable
告诉游戏引擎该对象可能会被实现该对象的其他对象击中(tells the game engine that the object might be hit by other objects implementing the) IDamaging
接口.(interface.)
这基本上是整个碰撞模型的基础,该模型在引擎中自动运行.船舶抽象的实现编码如下:(This basically is the basis for the whole collision model, that runs automatically in the engine. The implementation of the ship abstraction has been coded as follows:)
abstract class ControlledShip : MovableObject, IDamageable, IDamaging, IInterestedInStatistics
{
// ...
public override void Update(IGame g)
{
base.Update(g);
_laser.Update(g);
_alternative.Update(g);
_engineering.Update(g);
var effectiveDamperQuality = _damperQuality * _engineering.InertialDamper.Performance;
ExerciseControl(g, effectiveDamperQuality);
var vN = Velocity.Norm();
UseStabilisator(effectiveDamperQuality, vN);
ImposeSpeedLimit(vN);
if (HealthPoints <= 0)
{
g.PlayEffect(AudioDb.Effects.Explosion);
g.Register(ExplosionFactory.Create(this, ExplosionFactory.ExplosionType.Explosion2));
IsGarbage = true;
}
if (revengeTimer > 0)
revengeTimer--;
else if (revengeTimer == 0)
HitDirection = null;
}
}
每个专业,例如(Every specialization, such as) DroneShip
要么(or) PlayerShip
从给定的基类派生.正如我们在(derives from the given base class. As we can see in the specialization of the) Update
方法,我们正在使用更新步骤来使其他对象保持最新(例如,船舶的工程系统和武器).(method, we are using the update step to also keep other objects current (such as the ship’s engineering system and weapons).)
一个非常有趣的专业是企业.它比大多数其他舰船大得多,难以控制并且在战役中以AI控制的形式使用.它还提供了更强大的武器.(A very interesting specialization is the enterprise. It is much bigger than most other ships, hard to control and used in an AI controlled form within the campaign. It also provides much stronger weapons.)
因此,该船还具有一种特殊的图形,基本上就是您所期望的.(Hence the ship also features a special kind of graphic, which is basically what you would expect.)
紫色激光枪由专业模型发射.另一个专业是普通玩家飞船.此类还实现了一些对游戏很重要的接口(而不是游戏引擎).这里我们有以下代码:(The purple laser shots are fired by the specialized model. Another specialization is the general player ship. This class also implements some interfaces that are important for the game (not the game engine). Here we have the following code:)
sealed class PlayerShip : ControlledShip, IDroneTarget, IPlayerShip
{
public override void Update(IGame g)
{
base.Update(g);
if (HealthPoints <= 0)
{
PlayerStatistic.IncrementPlayersRecursively(LastCollisionObject, LastCollisionObject);
Statistic.Deaths++;
if (Alternative.Ammo > 0)
{
g.Register(new BombItem
{
Location = this.Location,
AngularVelocity = this.AngularVelocity
});
}
}
}
}
的(The) IDroneTarget
无人机AI使用该接口来确定应将哪种对象视为目标.的(interface is used by the drone AI to determine what kind of objects should be treated as targets. The) IPlayerShip
实际上,如果该类可以用于实例化一个完全由玩家控制的对象,则它实际上只是一个指示器.(is practically only an indicator if the class could be used to instantiate an object that is purely controlled by a player.)
出于同样的原因,另一个接口称为(With the same reasoning another interface called) IDroneShip
已经被创造了.因此,我们可以动态创建播放器和无人机工厂.这可以用于例如某种有趣的模式,其中每个玩家都可以选择要使用的船,并且/或者必须防御同一对手船的(随机)波浪.(has been created. So we could dynamically create player and drone ship factories. This could be used for, e.g., some funny mode, where each player either may select the ship to use, and / or (random) waves of the same opponent ships have to be defended.)
平台注意事项(Platform considerations)
让我们记住本文的内容.我们想要创建一个跨平台游戏.在这种情况下,这两个问题的平台是Windows(通过使用WPF)和Windows Store.对于这两种平台,我们都使用C#作为我们选择的编程语言.即使不需要在所有平台上使用相同的语言,也可以使共享代码变得更加容易.否则,我们可能会受到不同代码段之间的ABI的限制.但是,ABI高度特定于平台,并且可能是提供跨平台功能的最不喜欢的方式.(Let’s remember what this article is about. We wanted to create a cross platform game. The two platforms of matter in this case are Windows (by using WPF) and Windows Store. For both platforms we use C# as our programming language of choice. Even though it is not required to use the same language on all platforms, it makes sharing code a lot easier. Otherwise we may be limited with an ABI between the different code snippets. ABIs, however, are highly platform specific and may be the least favorite way of providing cross-platform capabilities.) 通常,我们希望能够在代码片段之间进行通信,并将所有内容(我们的整个代码)编译到特定平台.这样可以确保一致性,并最终以最简单,最可靠的方式创建跨平台项目.否则,我们总是会怀疑我们的更改是否会破坏某些东西.(Usually we want to be able to communicate between our code snippets and to compile everything (our whole code) to a specific platform. This ensures consistency and will eventually result in the easiest and most reliable way of creating cross-platform projects. Otherwise we would always be in doubt if our changes did break something.)
可以共享什么?(What can be shared?)
当然,共享代码有局限性.最后,两个平台的差异通常无关紧要.如果我们处理两个平台,我们将始终依靠包装器进行系统调用.该排除是与POSIX兼容的,但是由于我们正在处理.NET应用程序,因此我们可以忘记这种排除.另外,我们已经有了.NET-Framework的强大包装器.(Of course there are limitations to sharing code. In the end it usually does not matter how different two platforms are. If we deal with two platforms, we will always depend on wrappers for system calls. The exclusion is POSIX compatible, but since we are dealing with a .NET application, we can forget about such an exclusion. Plus we already have a powerful wrapper with the .NET-Framework.)
但是请稍等!如果.NET为我们做所有事情,为什么还要讨论共享代码呢?好吧,实际上那将是一个梦想.但是现实更像一场噩梦.基本的抽象(理论上)是共享的.例如,我们可以创建一个控制台应用程序,该应用程序使用来自(But wait a second! Why this long talk about sharing code if .NET does everything for us? Well, actually that would be a dream. But the reality is more close to a nightmare. Basic abstractions are (in theory) shared. For instance we can create a console application that uses classes from the) System.IO
用于读取和写入文件的名称空间.编译和运行此代码可在MacOS和使用Mono编译器的任何Linux发行版上运行.这就是我所说的可移植的.(namespace for reading and writing files. Compiling and running this code works on MacOS and any Linux distribution using the Mono compiler. This is what I would call portable.)
但是,一旦我们介绍了沙箱和专用环境的概念,即使给出的示例也可能会停止工作.在某些平台上,没有直接访问文件系统的方法.因此,这样的抽象可能不存在.这是一个概念(However, once we introduce the concept of a sandbox and specialized environments even the given example will probably stop working. On some platforms there is no direct way of accessing the file system. Therefore such an abstraction might not exist. This is the concept of a)可移植类库(Portable Class Library).它基本上确定了适合给定数量平台的最低公分母.因此,如果我们选择4个平台,但其中一个不包含任何直接访问文件的方式,则PCL项目中也不会存在这种可能性.(. It basically determines the lowest common denominator that fits a given number of platforms. So if we pick 4 platforms and one does not contain any direct way of accessing files, we will also not have this possibility in the PCL project.)
这个概念是普遍的.它基本上确定了可以共享的内容.一组给定平台上通用的所有内容都可以共享.其他所有内容都必须通过下一节中提到的技术进行介绍.(This concept is universal. It basically determines what can be shared. Everything that is common on a given set of platforms can be shared. Everything else has to be introduced by the techniques mentioned in the following section.)
当然,我们可以尝试引入分类.在我看来,有三个部分决定了可以共享多少代码.(Of course we can try to introduce a classification. In my opinion there are three parts that determine how much code might be shared.)
UI可能是最常见的部分.即使框架为一组平台提供了相同的抽象,从长远来看,避免共享UI代码通常也是明智的决定.平台不仅在外观上有所不同,而且在感觉上也有所不同.因此,任何决定都会导致不同平台具有相同的外观和感觉,最终将导致针对提供平台特定解决方案的其他应用程序的失败.(UI is probably the most common part. Even if a framework provides the same abstraction for a set of platforms, it is usually a wise decision to avoid sharing UI code (in the long run). Platforms differ not only by look, but also by feel. Therefore any decision that will result in the same look and feel for different platforms, will ultimately fail against other applications, that provide a platform specific solution.)
IO也可能很难共享.有时没有抽象,甚至没有可用的UI功能.然后,我们也可能无法共享任何代码.即使我们可以共享代码,我们也应该在一个共同的基础上工作,即只能处理(IO might be hard to share as well. Sometimes there are no abstraction or not even accessible UI capabilities available. Then we might also not be able to share any code. Even if we can share code, we should work on a common base, i.e. only handle) FileStream
要么(or) Stream
一般来说.根据平台,输入可以更具体.(in general. The input can then be more specific, according to the platform.)
最后是一般的库和给定的框架.这通常是最简单的部分.例如,如果给定的平台支持.NET 4.5,我们可以使用(Finally libraries and the given framework in general. This is usually the easiest part. For instance if the given platform supports .NET 4.5, we might use) async
/(/) await
.如果平台甚至不支持.NET 4.0,我们将无法使用(. If the platform does not even support .NET 4.0, we cannot use) Task
完全没有有变体,排除项,子集等等.最后,我们必须确定我们要使用的东西以及必须使用的东西.有时(重新)创建这些类并不难.有时有可用的NuGet软件包.这里的重点是,始终要有一个粗略的计划,这将需要什么.(at all. There are variants, exclusions, subsets and much more. In the end we have to determine what we want to use, and what we have to use. Sometimes (re-) creating these classes is not hard. Sometimes there are NuGet packages available. The important point here is, to always have a rough plan what will be required.)
代码共享技术(Code sharing techniques)
有许多共享技术. Visual Studio与C#/.NET的结合为代码共享提供了极好的基础.考虑到C ++代码,我们已经处于相当舒适的区域.(There are a number of sharing techniques. The combination of Visual Studio together with C# / .NET provides an excellent basis for code sharing. Considering for instance C++ code, we are already in quite comfortable zone.) 我们将首先更仔细地研究PCL的概念.这个概念为通用应用程序等解决方案提供了强大的动力,同时也帮助我们在WPF和Windows Store之间建立了共享基础.(We will start by looking a little bit more closely at the concept of PCL. This concept is what powers solutions like universal apps, but also helps us in creating a shared basis between WPF and Windows Store.) 在下文中,我们将从纯代码共享开始.然后我们将讨论越来越多的技术,这些技术在纯代码共享(即无法在所有平台中完全支持给定代码段)的情况下有用.(In the following we will start by pure code sharing. Then we will discuss more and more techniques that are useful when pure code sharing, i.e. full support of a given piece of code in all platforms is available, is impossible.)
便携式类库(Portable Class Libraries)
可移植类库(Portable Class Library,PCL)是一种特殊的项目类型,它试图帮助开发人员创建一个可从各种平台引用的库.该库是我们的共享代码模型.最后,我们将尝试尽可能多地放入可移植类库中,因为我们可以从许多平台上轻松地将其作为目标.(A Portable Class Library (PCL) is a special kind of project type, that tries to help developers to create a library, which can be referenced from various platforms. The library is our shared code model. In the end we will try to put as much as possible into a Portable Class Library, since we can target it very easily from a bunch of platforms.)
就本文而言,我们仅使用两个平台:WPF(.NET 4.5)和Windows Store(8.1).这限制了.NET的子集,并包括其他方法.通常,我们可以说,我们尝试以PCL为目标的平台越多,.NET-Framework的可用子集就越小.(In the case of this article we just use two platforms: WPF (.NET 4.5) and Windows Store (8.1). This restricts the subset of .NET and includes other methods. In general we can say that the more platforms we try to target with our PCL, the smaller the available subset of the .NET-Framework will be.)
.NET框架的子集由上图中的交集表示.的交集(The subset of the .NET-Framework is represented by the intersection in the picture above. An intersection of)*ñ(N)*平台所包含的元素绝不会超过(platforms will never contain more elements than the intersection of)*N-1(N - 1)*平台.这可以继续下去,直到我们发现与仅支持单个平台相比,几乎可以肯定有自然限制.(platforms. This can be continued until we find that it is almost certain to have a natural limitation as compared to just support a single platform.)
使用PCL作为基础,几乎消除了所有即将出现的技术.可是等等!为什么它们仍在此处列出?因为最后我们将专门针对某个平台.没有可移植应用程序项目.因此,必须在某个阶段包括专业化.当然,这些专业应该尽可能灵活和可维护.因此,我们需要一些技术来实现有效的代码共享和一些优雅的准备工作.(Using the PCL as a basis eliminates almost all the upcoming techniques. But wait! Why are they then still listed here? Because in the end we will specifically target a certain platform. There is no Portable Application project. So specializations have to be included at some stage. And of course these specializations should be as flexible and maintainable as possible. Hence we require some techniques for efficient code sharing and some elegant preparations.)
一个重要的工具是仅在Visual Studio项目中链接文件的功能.通常,如果我们将项目添加到项目中,Visual Studio会复制一个原始文件或创建一个新文件.这不是我们想要的.如果我们有一个文件的两个副本,则需要对两个文件进行相同的更改.有时,我们只希望在几个项目中共享文件的一小部分,而将特定部分专门化.这将在下一节中介绍.(An important tool is the ability to just link against files within Visual Studio projects. Usually if we add items to a project, Visual Studio will either copy an original file, or create a new one. This is not what we want. If we have two copies of a file, we need to make the same changes to both files. Sometimes we want only a fraction of a file to be shared across several projects, with a certain part being specialized. This will be covered in the next section.)
但是,在讨论该技术之前,我们应该阐明如何在各个项目之间有效地共享公共部分.最优雅的方法是创建指向现有文件的链接.因此,我们打开一个新对话框,将现有项目添加到所需项目:(However, before we discuss this technique, we should clarify how the common part can be shared efficiently between various projects. The most elegant way is to create a link to an existing file. So we open a new dialog to add an existing item to the desired project:)
请注意,仅双击文件将导致创建文件的物理副本.这不是我们想要的.我们必须选择(Please note that just double clicking on a file will result in creating a physical copy of the file. This is not what we want. We have to select)“添加为链接”(“Add As Link”)明确地.现在我们只是在项目中添加了对文件的引用,即在给定位置上没有物理文件.链接文件对于在多个项目之间共享代码非常重要.(explicitly. Now we just added a reference to the file within the project, i.e. there is no physical file on the given location. Linking files is really important for sharing code between multiple projects.) 现在,我们知道了如何链接文件以共享代码,让我们看看如何使它变得非常高效,这样每个平台只需要最少的更改.(Now that we know how to link files for sharing code, let’s see how we can make that very efficient, such that only a minimum of changes is required for each platform.)
部分课程(Partial classes)
局部类的想法很简单.为了避免一个文件包含一个可能包含数千行的类,我们可能会将类拆分为多个部分,然后将这些部分保存在不同的文件中.最初引入该解决方案是为了解决将设计师生成的代码和用户代码混合在一起的问题.最终,可以以不干扰用户的方式隐藏自动生成的代码.(The idea of a partial class is simple. In order to avoid having a file that contains a single class with possibly thousands of lines we might split up the class into parts that will be saved in different files. Initially that was introduced to solve the problem of mixing designer generated code and user code. Finally it was possible to hide the auto-generated code in an unobtrusive manner.) 但是,此概念可用于减少复制/粘贴,因此可减少共享类的维护工作,该类部分由可共享的代码组成,而部分由无法共享的代码组成.我们从以下构造开始:(This concept, however, can be used to reduce copy / paste and therefore maintenance for sharing a class that consists partially of code that can be shared, and partially of code that cannot be shared. We start with the following construct:)
//MySharedClass.cs
public class MySharedClass
{
/* Code that can be shared */
/* Code that cannot be shared and must be (re-)implemented for (each or at least some) platforms */
}
现在,我们当然需要添加(Now of course we need to add the) partial
关键词.另外,我们应该创建平台特定的部分实现.(keyword. Additionally we should create the platform specific partial implementations.)
//MySharedClass.cs
public partial class MySharedClass
{
/* Code that can be shared */
}
//MySharedClass.WPF.cs
class MySharedClass
{
/* Specific code for WPF */
}
//MySharedClass.WindowsStore.cs
class MySharedClass
{
/* Specific code for Windows Store */
}
现在唯一的问题是:这些文件放在哪里?这个问题有很多可能的答案.如果我们对如何组织代码有强烈的意见,我们可以过滤这些答案.让我们考虑一下:(Now the only question is: Where to place these files? There are many possible answers to this question. We can filter these answers if we have a strong opinion how to organize our code. Let’s consider:)
- 所有平台都同样重要.(All platforms are equally important.)
- 一个平台比其他平台更重要.(One platform is more important than others.)
- 有更重要的平台和更复杂的平台.但这真的很复杂.(There are more important platforms and more complicated platforms. But it is really mixed.) 在第一种情况下,我们可能想创建一个包含所有部分类文件的解决方案文件夹.现在,每个项目都获得了指向该部分类文件的链接以及其对特定代码段的实现,即扩展了链接的部分类文件的物理文件.(In the first case we might want to create a solution folder that contains all the partial class files. Each project now gets a link to this partial class file and its own implementation for the specific code section, i.e. a physical file that extends the linked partial class file.) 在第二种情况下,我们可以首先在一个平台上创建所有内容.然后,其他平台将获得指向更重要的平台项目中包含的部分类文件的链接.但是,每个项目仍将具有一个包含特定代码的物理文件,该文件扩展了部分类文件.(In the second case we could create everything on one platform first. Then the other platforms will get a link to the partial class file that is contained in the more important platform project. However, every project will still have a physical file that contains the specific code, which extends the partial class file.) 最后,在混合场景中,我们可能想要遵循第一个场景中演示的方法,或者遵循第二个场景中的方法(但针对每个项目).当然,第一种方法可能更有条理,但是第二种方法可能更直接.最后,我们不应将两种方法混为一谈.这意味着我们不应该有一个共享文件夹,但仍要在特定项目中实际提供一些共享实现.我们要么遵循"起源具有物理性"模式,要么遵循"共享内部共享"方法.(Finally in the mixed scenario we might want to either follow the way demonstrated in the first scenario, or the way in the second scenario (but for each project). Of course the first way might be a lot more organized, however, the second way may be more straight forward. In the end we should not mix the two approaches. That means we should not have a shared folder, but still provide some shared implementations physically within a specific project. Either we follow the “originates has physical” pattern, or the “shared within shared” method.)
扩展方式(Extension methods)
扩展方法是真金.如果您不相信我,请考虑以下事项:什么使LINQ成为可能?是lambda表达式吗?优雅,但是好吧,我可以传递对现有函数的引用,也可以使用(Extension methods are true gold. If you don’t believe me then think about the following: What makes LINQ possible? Is it lambda expressions? Elegant, but well, I could just pass in references to existing functions or create anonymous functions using the) delegate
句法.是否可以在C#中编写类似SQL的代码?我个人不喜欢它,我从不使用它.我看到的唯一好处是可以使用(syntax. Is it the possibility to write SQL like code in C#? Personally I don’t like it and I never use it. The only advantage I see is an easier declaration of intermediate variables using the) let
关键字,但是,我通常更喜欢手动操作.这使我们回到了扩展方法.没有它们,我们将编写类似于洋葱的LINQ代码,该代码类似于以下内容:(keyword, however, I usually prefer doing it manually. Which brings us back to extension methods. Without them we would write onion-like LINQ code that would look similar to the following:)
var dataSource = new [] { 1, 2, 3, 4, 5, 10, 15, 20, 26 };
var querySet = SomeClass.Take(SomeClass.Select(SomeClass.Where(dataSource, m => m % 5 == 0), m => m * m), 3);
在这里我只是假装所有这些方法都可以在称为(Here I just pretend that all these methods can be found in the same class that is called) SomeClass
(在通常的名称空间内((within the usual namespace) System.Linq
).我知道您可以猜出该调用的作用,但是,我还确定您需要花几秒钟才能完全理解该代码.(). I know you can guess what the call does, however, I am also sure that you needed some seconds before you fully understood the code.)
让我们看看扩展方法的变化:(Let’s see how this changes with extension methods:)
var dataSource = new [] { 1, 2, 3, 4, 5, 10, 15, 20, 26 };
var querySet = dataSource.Where(m => m % 5 == 0).Select(m => m * m).Take(3);
洋葱现在是一个不错的烟斗.我们从左侧开始,在右侧结束.我们可以像阅读句子一样阅读它.那太好了.我现在的观点不是,我们从左到右开始,而是(The onion is now a nice pipe. We start on the left and end on the right. We can read it like we read a sentence. That’s just wonderful. My point now is not, that we start from left to right, but that the class) SomeClass
完全不见了.我们只是不必知道它叫什么.这对于重构非常有用.提供这些方法的类的名称有什么关系?否.只有名称空间很重要,并且它包含在编译过程中.(is completely missing. We just don’t have to know what it is called. This is super great for refactoring. Does it matter what the name of the class that provides these methods is? No. Only the namespace matters and that it is included in the compilation process.)
而且此功能非常便于共享代码.但是,在我详细介绍之前,我必须警告您.使用扩展方法共享代码存在局限性.而且这些限制中的大多数都来自这个词(And this ability comes in very handy for sharing code. But before I go into details I have to warn you. There are limitations to sharing code using extension methods. And most of these limitations come from the word)方法(methods).如果我们处理属性,具体的类名或其他特殊情况,我们将会迷失方向.(. If we deal with properties, concrete class names or other special cases we are lost.)
让我们以反射为例.在以下代码段中,我们只想找出给定的(Let’s consider reflection as an example. In the following snippet we just want to find out if a given) Type
实例,如果另一个的子类(instance if the subclass of another) Type
目的.通常我们有:(object. Usually we have:)
var result = typeof(FirstClass).IsSubclassOf(typeof(SecondClass));
该代码段已在.NET(例如4.5版)中实现.但是,一旦尝试在Windows Store应用程序上运行此代码段,我们将发现它不起作用.反射(开箱即用)仅提供有限的可能性.您可以使用来访问更多内容(The code snippet has been implemented in .NET (e.g. version 4.5). However, once we try running this snippet on the Windows Store application, we will see that it won’t work. Reflection (out-of-the-box) does only offer limited possibilities. A little bit more can be accessed by using the) GetTypeInfo()
扩展方法,可以在(extension method, which can be found in the) System.Reflection
命名空间.(namespace.)
var result = typeof(FirstClass).GetTypeInfo().IsSubclassOf(typeof(SecondClass));
这给了我们两种可能性:(This gives us two possibilities:)
//First possibility:
public static bool IsSubclassOf(this Type origin, Type compare)
{
return origin.GetTypeInfo().IsSubclassOf(compare);
}
//Second possibility:
public static Type GetTypeInfo(this Type origin)
{
return origin;
}
第一种可能性是让我们在Windows Store应用程序中使用WPF代码.第二种以另一种方式描述了该方案.在这里,我们可以使用WPF应用程序中Windows Store应用程序中的代码.哪一个更好?这取决于!两者都有优点和陷阱.(The first possibility let’s use us the WPF code within a Windows Store app. The second one describes the scenario the other way around. Here we can use the code from the Windows Store app within a WPF application. Which one is better? It depends! Both have advantages and pitfalls.)
让我们从一个简单的观察开始:“真实”(Let’s start with a simple observation: While the “real”)GetTypeInfo(GetTypeInfo)Windows Store应用程序中的方法返回一个(method from Windows Store applications returns a) TypeInfo
实例,我们只使用方法来返回标识,它是类型(instance, we just use the method to return the identity, which is of type) Type
.通常在推断或隐式使用类型时有效.但是一旦我们需要特定的方法(. This will usually work when the type is inferred or implicitly used. But once we require specific methods from) TypeInfo
或需要创建具有显式类型的变量,我们很不幸.此外,这已经说明了一个主要问题:方法的名称,签名或其他属性可能有所不同.这实际上是导致(or need to create variables with explicit types, we are out of luck. Additionally this already illustrates a major problem: Methods may differ by their name, signature or other properties. This is actually one of the reasons for the) GetTypeInfo()
方法.(method.)
具有特定的方法,例如(Having a specific method such as the) IsSubclassOf
如果签名匹配,也可以使用.实际上,为所有平台实施扩展方法更有意义.这样,您无需担心将扩展方法放置在正确的位置.这些方法存在于每个平台上,但专门针对每个平台实施.(also just works if the signature matches. In reality it makes more sense to implement extension methods for all platforms. That way, one does not need to worry about placing the extension methods on the right places. The methods exists on every platform, but is specifically implemented for each platform individually.)
我们会有:(We would have:)
//In the Windows Store project
static WindowsStoreExtensions
{
public static bool DerivesFrom(this Type origin, Type compare)
{
return origin.GetTypeInfo().IsSubclassOf(compare);
}
}
//In the WPF project
static WpfExtensions
{
public static bool DerivesFrom(this Type origin, Type compare)
{
return origin.IsSubclassOf(compare);
}
}
现在,共享代码可以只使用(Now a shared code could just use the) DerivesFrom
方法,不用担心.另外,这种策略使我们能够规避实施细节和属性.我们将使用方法代替属性.(method, without worrying. Additionally such a strategy allows us to circumvent implementation details and properties. Instead of properties we would use methods.)
让我们看一个示例,它再次说明了该解决方案:(Let’s have a look at an example, which illustrates this solution again:)
//In the Windows Store project
static WindowsStoreExtensions
{
public static IEnumerable<PropertyInfo> ListProperties(this Type origin, BindingFlags flags)
{
return origin.GetTypeInfo().DeclaredProperties;
}
}
//In the WPF project
static WpfExtensions
{
public static IEnumerable<PropertyInfo> ListProperties(this Type origin, BindingFlags flags)
{
return origin.GetProperties(flags);
}
}
即使使用(Even though obtaining the properties using the) TypeInfo
Windows Store应用程序中的object是基于访问属性的,我们可以在具体实现中隐藏此细节.但是,我们应该注意,我们扩展了签名以支持其他功能(object in Windows Store applications is based on accessing a property, we can hide this detail in the concrete implementation. We should note, however, that we extended our signature to support an additional) BindingFlags
论据.这样,Windows Store中就不会使用它了,但是我们仍然需要提供它.(argument. This one is then not used in Windows Store, but we still need to supply it.)
它不应该是默认情况,但可能会在某些时候发生.肯定会发生一个平台比另一个平台具有更多功能的情况.无需担心,除非这些限制将导致不同的行为.一旦遇到这种情况,就需要调整实现.最后,从代码行为的角度来看,我们的目标应该是在不同平台上获得相似/相等的结果.(It should not be the default case, but it might happen at some point. It will definitely happen that one platform has more capabilities than another. This is nothing to worry about, unless these restrictions will result in different behaviors. Once we encounter this, we need to adjust the implementation. In the end our goal should be to have similar / equal results on different platforms from a code behavioral perspective.)
接口和面向对象的编程(Interfaces and object-oriented programming)
如我们所见,拥有可能在不同平台上共享的代码的能力可以分为两类.一方面,我们采用库自上而下的方法,专门研究每个库.在另一种方法中,我们有一个并行策略,其中我们起源于一个库,并链接到另一个库中的原始源文件.(The ability to have code that might be shared across different platforms can be divided into two categories, as we have seen. On the one side we have the library top-to-bottom approach, where we specialize within each library. In the other approach we have a side-by-side strategy, where we originate in one library and link to the original source file in another library.)
无论我们做什么,我们都需要代码中的一些结构.如果我们对平台的依赖性过强,则无法将代码放在更通用的库中.此外,不可能仅链接到文件(至少不使用本节之前和之后提到的技术).(Whatever we do, we need some structure in the code. If we have too strong platform dependencies, we cannot place the code in a more general library. Additionally it is impossible to just link against the file (at least without using the techniques mentioned before, and after this section).)
为了获得适当的结构,我们必须回到OOP的原理和模式.通过使用SOLID(尤其是OCP和DIP)原理,我们将对代码进行结构化,使其具有足够的抽象性和泛化性,以适合两种模型(从上到下或并排).(To gain a proper structure we have to go back to the principles and patterns of OOP. By using SOLID (especially OCP and DIP) principles, we will structure our code to have enough abstraction and generalization to fit into either model (top-to-bottom or side-by-side).)
现在我们了解为什么接口对于跨平台开发至关重要.一方面,我们遵循DIP,一切都基于抽象,而抽象不依赖细节.另一方面,即使我们已经必须实现另一个类,我们也可以在想要的任何类上实现接口.(Now we see why interfaces are crucial for cross-platform development. On the one hand we follow DIP and base everything on abstraction, an abstraction that does not depend on details. On the other hand we may implement an interface on whatever class we’d like to - even though we already have to implement another class.)
因此,我们的策略是始终将尽可能多的跨平台(独立)代码吸收到基类中.可能具有某种第三方依赖性的专业化规模很小,需要抽象化.在这种情况下,我认为WPF是第三方依赖项.(Therefore our strategy is to always absorb as much cross-platform (independent) code as possible into base classes. Specializations that maybe have some kind of third party dependency will be small and require abstractions. In this scenario I consider WPF to be a third party dependency.)
游戏引擎自然遵循这条道路.即使渲染需要某种类型的UI对象,它也不依赖于WPF.相反,它只需要类型的对象(The game engine follows this path naturally. Even though the rendering requires some kind of UI object, it does not depend on WPF. Instead it just requires objects of type) IDrawableEntity
.它包含渲染引擎需要了解的所有内容.在一个具体的实现中,渲染引擎可能需要更多信息,但这不是一般定义的问题.(. This contains everything the rendering engine needs to know. In a concrete implementation the rendering engine may require further information, but this is not the problem of the general definition.)
使用IOC容器进行依赖注入(Dependency Injection using an IOC container)
为了遵循上一节中描述的模式,我们还应该公开对属性或构造函数的依赖.这将减少耦合,因为某些类不需要知道在哪里可以找到某种抽象(接口)的具体实现.(To follow the patterns described in the previous section we should also expose dependencies over properties or the constructor. This will reduce couplings as some class does not need to know where to find a concrete implementation of some abstraction (interface).) 因此,强烈建议使用服务定位器甚至更好的服务,依赖注入系统(使用IOC容器).通过解决依赖关系,这基本上可以自动创建对象.(Using a service locator or even better, an Dependency Injection system (that uses an IOC container), is therefore highly recommended. This basically automates the creation of objects by resolving the dependencies.)
定义(Defines)
有时,前面提到的所有技术都会失败. XAML代码提供了一个很好的示例.虽然XAML代码本身大部分可以在WPF和Windows Store应用程序之间移植(即(Sometimes all the techniques mentioned previously fail. A great example is delivered by XAML code. While XAML code itself is mostly portable between WPF and Windows Store applications (speaking of a) UserControl
仅使用控件(在两个平台上都可用),则后面的代码却没有.(that just uses controls, which are available on both platforms), the code behind is not.)
让我们看一下一个非常简单的XAML控件背后的代码的WPF版本.(Let’s have a glance at the WPF version of the code-behind of a very simple XAML control.)
using System;
using System.Windows.Controls;
namespace QuantumStriker.Xaml
{
public partial class EnemyShip : UserControl
{
public EnemyShip()
{
InitializeComponent();
}
public void SetHealthBarTo(Double percent)
{
HealthBar.Width = 24.0 * percent;
}
}
}
看起来还不错.实际上,大多数人不会意识到此片段需要在Windows Store环境中运行才能进行更改.让我们看看Windows Store的等效形式如何:(That does not look too bad. Actually most people would not realize that changes are required for this snippet to run in Windows Store environments. Let’s see how the Windows Store equivalent looks like:)
using System;
using Windows.UI.Xaml.Controls;
namespace QuantumStriker.Xaml
{
public partial class EnemyShip : UserControl
{
public EnemyShip()
{
InitializeComponent();
}
public void SetHealthBarTo(Double percent)
{
HealthBar.Width = 24.0 * percent;
}
}
}
几乎相同的代码?确实是的.但是,这是非常关键的变化,它阻止了例如局部类技术的应用.我们需要不同的名称空间!哇.谢谢微软!实际上,您为95%的控件保留了相同的名称,但是您更改了名称空间.很好!即使这在组织层面上可能很有意义,但对于编写(或想要共享)代码的人来说并没有太大意义.为什么? WPF控件和Windows Store控件之间甚至没有机会发生冲突.但是,这种冲突是我们为什么要首先使用名称空间的原因.这意味着什么?使用不同的名称空间只是令人讨厌.(Nearly the same code? Yes, indeed. But a very crucial change, that prevents, e.g., the partial class technique, from being applied. We need different namespaces! Wow. Thanks Microsoft! You actually keep the same names for 95% of your controls, but you change the namespace. Great job! Even though that might make sense on an organizational level, it does not make much sense for people who write (or want to share) code. Why? There is not even remotely a chance of having a collision between the WPF controls and the Windows Store controls. However, such a collision is the reason why we have namespaces in the fist place. What does that mean? Using a different namespace is just annoying.)
但是,让我们看看乐观的一面.现在,我们有机会介绍(But let’s have a look on the bright side. We now have the chance to introduce the) #define
预处理程序指令.它使我们能够定义可以使用预处理程序语句进行评估的符号,例如(preprocessor instruction. It allows us to define symbols that can be evaluated using preprocessor statements such as) #if
.更好的是,我们还可以在项目设置中的项目级别上全局定义此类符号.最好的方法:有时已经定义了此类符号.一个例子是(. Even better, we can also define such symbols globally on a project level in the project settings. And the best: Sometimes such symbols are already defined. An example would be the) DEBUG
符号,为默认项目目标定义(symbol, that is defined for the default project target)除错(Debug).(.)
using System;
#if NETFX_CORE
using Windows.UI.Xaml.Controls;
#else
using System.Windows.Controls;
#endif
namespace QuantumStriker.Xaml
{
public partial class EnemyShip : UserControl
{
public EnemyShip()
{
InitializeComponent();
}
public void SetHealthBarTo(Double percent)
{
HealthBar.Width = 24.0 * percent;
}
}
}
当然,我们可以为每个项目引入一个符号.但是,一般而言,最好使用现有符号.幸运的是,我们可以使用符号(Of course we could introduce a symbol for every project. In general, however, it is much better to use an existing symbol. Lucky as we are, we can use the symbol) NETFX_CORE
,是为Windows应用商店目标定义的.现在,我们可以在两个项目中使用完全相同的文件.(, which is defined for Windows Store targets. Now we can use the exact same file in both projects.)
完全重写(Complete Rewrite)
有时,所有操作都会失败,并且无法定义通用接口,依赖扩展方法或仅使用预处理程序切换某些代码块是不可能的.有时,最高效的东西实际上是最不高效的,这是完全重新实现.(Sometimes everything fails and it is impossible to define a common interface, rely on extension methods or just switch some blocks of code using the preprocessor. Sometimes the most efficient thing is actually the least efficient, which is a complete re-implementation.) 一开始听起来很疯狂.但是,如果我们考虑与UI相关的问题,我们将最终得出结论,即不同的平台不仅将提供不同的UI框架,而且还将遵循不同的UI行为和样式.因此,无论如何,我们可能都需要重新处理大部分(UI)代码.显然,从头开始比尝试模仿以前的工作要好.有充分的理由支持这一说法.(At first that sounds crazy. But if we think about UI related issues, we will eventually come to the conclusion that different platforms will not only provide different UI frameworks, they will also follow different UI behaviors and styles. Therefore we might need to re-work most of our (UI) code anyway. It should be obvious that starting from scratch is then better than trying to mimic the previous work. There are good reasons to support this statement.) 最后,这样的重写最终可能对用户(更好的体验)和程序员(减少共享近100%不兼容代码的麻烦)产生好处.然而,有时候人们应该在重写之前先想一想…(In the end such a rewrite might therefore be beneficial for the user (better experience) and the programmer (less head scratching on how to share nearly 100% incompatible code). Nevertheless sometimes one should think before rewriting…) 例如,当只是为每个平台重写各种组件时,可能会认为有人认为不兼容的代码可能会被重用.这样,代码可以被重用,并且(甚至更好),所有其他代码也可以被重用.但是,只有在较小的块上构建UI时,这种方法才可行.并且只有这些较小的块几乎是独立的.一旦他们必须相互交互(例如,通过拖放),我们可能会迷路.(For instance maybe the code one thinks is incompatible may be re-used when just rewriting the various components for each platform. This way the code may be re-used and (even better), all other code may also be re-used. But such an approach is only possible if we constructed our UI on smaller blocks. And only if these smaller blocks are nearly independent. Once they have to interact, e.g., over drag-and-drop, with each other, we might be lost.)
WPF特定(WPF specific)
在开始使用WPF时,从我们的角度来看,该平台几乎没有任何关于我们代码的限制.但是,通常Windows Store在某些情况下(尤其是触摸)可能会更好.但是,除了某些特殊领域外,我们始终可以相信WPF提供了更多的可能性.(As we started with WPF, there are hardly any restrictions on this platform (from our point of view) regarding our code. However, in general Windows Store might be superior in some scenarios (especially touch). Nevertheless, besides some special areas we can always count that WPF offers more possibilities.) 如果我们从头开始一个项目,那么为平台提供最多限制的一切设计都是有意义的.这样,我们最终将在以后遇到更少的问题.如果我们为平台设计的限制最少,则必须调整应用程序,如当前文章所示.(If we would start a project from scratch, it would make sense to design everything for the platform that offers the most restrictions. That way we will eventually run into less problems later on. If we design for the platform with the least restrictions, we will have to tweak our application as the current article shows.) 两种方法都是合法的,但是为什么我们要用困难的方式呢?同样,在这种情况下,该应用程序已经为WPF编写,需要我们调整当前代码.最后,我们可以从有趣的课程中学习设计和编写跨平台应用程序.(Both ways are legit, but why should we do it the hard way? Again, in this case the application had already been written for WPF, requiring us to tweak the current code. In the end we can take away interesting lessons for designing and writing cross-platform applications.) WPF特定于引擎的连接是通过以下配置文件完成的.(The WPF specific connection to the engine is done over the following configuration file.)
namespace QuantumEngine
{
public class AvalonConfig : Config
{
static AvalonView view;
protected override IEnumerable<Type> LoadTypes()
{
yield return typeof(AvalonSoundManager);
yield return typeof(AvalonPresentation);
yield return typeof(AvalonDebugBox);
yield return typeof(AvalonTimer);
yield return typeof(AvalonKeyboardInput);
yield return typeof(AvalonMouseInput);
yield return typeof(AvalonTouchInput);
}
internal static AvalonView View
{
get { return view; }
}
public static void Register(Window window)
{
view = new AvalonView(window);
}
}
}
最重要的是创建一个(The most important thing is the creation of an) AvalonView
实例,它基本上基于给定(instance, which basically builds upon the given) Window
实例(因为这是高度WPF特定的).(instance (since this is highly WPF specific).)
namespace QuantumEngine
{
sealed class AvalonView
{
public event EventHandler<TouchCollectionEventArgs> Touched;
Window parent;
public AvalonView(Window window)
{
var source = PresentationSource.FromVisual(window) as HwndSource;
parent = window;
DisableWPFTabletSupport();
if (source != null)
{
source.AddHook(WndProc);
RegisterTouchWindow(source.Handle, 0);
}
}
/* Touch specific native API handling */
}
}
然后,此特殊视图实例在内部用于连接WPF特定的控制器(例如(This special view instance is then used internally to connect the WPF specific controllers (e.g.) AvalonTouchInput
)发送给事件调度员.() to the event dispatchers.)
Windows商店专用(Windows store specific)
Windows应用商店的部分肯定是更多的挠头.一个大问题是音频.我们使用NAudio在WPF中轻松解决了此问题,但是由于NAudio依赖于本机API和直接流访问,因此Windows商店平台当然不提供此功能. SharpDX将自然替代.这是DirectX API的一个很好的包装,可以在Windows Store应用程序中完全访问.(The Windows store part was definitely more head scratching. A big problem is audio. We solved this easily in WPF using NAudio, but as NAudio relies on native APIs and direct stream access, it is of course not available for the Windows store platform. A natural replacement appears with SharpDX. This is a nice wrapper around the DirectX API, which may be fully accessed within Windows Store applications.)
但是,即使此解决方案似乎完全适合我们的需求,仍然存在一个问题:SharpDX无法处理MP3文件(至少没有(However, even though this solution seems fully suited for our needs, there is one remaining problem: SharpDX can’t handle MP3 files (at least without) SharpDX.MediaFoundation
).这是一个巨大的缺点,因为背景声音以MP3格式保存.尽管如此,如果我们真的走到了尽头,我们只会使用两种平台都可以压缩和读取的格式.(). This is a huge drawback as the background sounds are saved in the MP3 format. Nevertheless, if we would really go this path to the end, we would just use a format that is compressed and readable by both platforms.)
Windows应用商店的配置文件看起来与WPF中的配置文件相似.在这里,我们编写了以下代码:(The configuration file for the Windows store app looks similar to the one from WPF. Here we wrote the following code:)
namespace QuantumEngine
{
public class MetroConfig : Config
{
static CoreWindow view;
public MetroConfig()
{
view = CoreWindow.GetForCurrentThread();
}
protected override IEnumerable<Type> LoadTypes()
{
yield return typeof(MetroSoundManager);
yield return typeof(MetroPresentation);
yield return typeof(MetroDebugBox);
yield return typeof(MetroTimer);
yield return typeof(MetroKeyboardInput);
yield return typeof(MetroMouseInput);
yield return typeof(MetroTouchInput);
}
internal static CoreWindow View
{
get { return view; }
}
}
}
我们再次返回由我们的量子引擎专业化提供的特殊类型的集合.即使这不是真正的服务容器,也有点像非常非常轻巧且不完全灵活的服务定位器.的(Again we are returning a collection of special types that are provided by our specialization of the quantum engine. Even though this is not a real service container, this is something like a very very lightweight and not fully flexible service locator. The) Config
量子引擎(可移植类)库中定义的类知道如何创建这些提供的类型的实例.(class, as it is defined in the quantum engine (portable class) library, knows how to create instances of these provided types.)
在这里,我们不需要定义一种特殊的视图.在Windows应用商店应用程序中,始终存在一个特殊的类,称为(Here we do not need to define a special kind of view. In a Windows store application there is always a special class called) CoreWindow
.通过使用静态(. By using the static) GetForCurrentThread()
方法,我们可以获得(method, we can obtain the) CoreWindow
当前线程的实例.既然我们知道,这部分代码只会从主线程中调用,我们可以简单地使用此方法.但是,总的来说,我们将指定对(instance for the current thread. Since we know, that this part of the code will only be called from the main thread, we can simply use this method. In general, however, we would specify a dependency on) CodeWindow
.然后,将需要一些其他方法来解决依赖性.(. Then some other method would be required to resolve the dependency.)
然后,Windows应用商店平台的控制器实现可以使用它.以下代码是触摸控制器的实现.(The controller implementations for the Windows store platform then may use this. The following code is the implementation of the touch controller.)
namespace QuantumEngine.Controller
{
sealed class MetroTouchInput : TouchInput
{
public MetroTouchInput(IGame game)
{
MetroConfig.View.PointerPressed += PointerHandler;
MetroConfig.View.PointerReleased += PointerHandler;
MetroConfig.View.PointerMoved += PointerHandler;
Game = game;
}
void PointerHandler(CoreWindow sender, PointerEventArgs e)
{
if (IsCaptured && e.CurrentPoint.PointerDevice.PointerDeviceType != PointerDeviceType.Mouse)
{
var pos = e.CurrentPoint;
if (pos.IsInContact)
_touchPoints[(Int32)pos.PointerId] = new QPosition(pos.Position.X, pos.Position.Y);
else
_touchPoints.Remove((Int32)pos.PointerId);
e.Handled = true;
}
}
}
}
控制器基本上使用(The controller basically uses the) CoreWindow
为可用的最基本和基本的触摸事件注册一个侦听器.当我们使用(to register a listener for the most elementary and basic touch events available. As we use the) CoreWindow
,我们也可以确保在任何情况下都可以处理该事件.(, we can also be sure to handle the event in any case.)
使用代码(Using the code)
初始版本完成后,一方面可以计算解决方案中的项目数量.解决方案的原始布局如下图所示:(When the initial version was finished, the number of projects in the solution could be counted on one hand. The original layout of the solution looked like the following image:)
当然,支持两个不同平台的平台考虑使某些项目加倍,并引入了共享项目.最后,解决方案已更改为如下所示:(Of course the platform considerations for supporting two distinct platforms doubled some projects and introduced shared projects. In the end the solution has changed to look as follows:)
不用说,给定的图片仅显示了故事的一半.在这里,我们只是为引擎提供了另一个平台.显然其他部分也可能会加倍.最后,我们有10个项目(从5开始).因此,我们为引擎添加了2个项目,为应用程序添加了3个项目.(Needless to say that the given picture only shows half of the story. Here we just supplied another platform for the engine. It is obvious that the other parts might double as well. In the end we have 10 projects, where we started with 5. So we added 2 more projects for the engine and 3 more projects for the app.)
我们从与输入层项目紧密耦合的引擎开始.第一步是解耦此结构.这分三个步骤完成:(We started with an engine that was tightly coupled to the input layer project. The first action has been to decouple this structure. This was done in three steps:)
- 使引擎成为PCL(针对Windows Store应用程序和.NET 4.5框架).(Make the engine a PCL (targeting Windows Store applications and the .NET 4.5 framework).)
- 为可能以不同方式实现的代码介绍接口和其他共享技术.删除对输入层的依赖.(Introduce interfaces and other sharing techniques for code that might be implemented differently. Remove the dependency on the input layer.)
- 创建另外两个提供专业化的项目.一个项目针对Windows Store应用程序,另一个针对.NET 4.5.后者必须再次引用输入层.(Create two more projects that provide the specializations. One project targets Windows Store applications, the other one .NET 4.5. The latter has to reference the input layer again.) 现在我们已经独立于平台,因为我们只需要创建另一个项目,并引用专用引擎(在本例中为Windows Store)就可以了.但是,这将是一场噩梦,因为该应用程序还包含许多代码,例如敌人,船只,故事等等.代替重新创建或复制代码,我们使用诸如链接其他代码,使用定义或扩展方法之类的技术.(Now we are already platform independent, since we just have to create another project, reference the specialized engine (in this case for Windows Store) and we are good. However, this would be a nightmare, as the application also contains a lot of code, e.g., the enemies, ships, story, … and much more. Instead of re-creating or copying the code we use techniques such as linking other code, using defines or extension methods.) 大量新创建的项目是非常不寻常的.在理想的世界中,项目(The high number of freshly created projects is quite unusual. In an ideal world the projects)*图片(Images)*和(and)*声音/(Sounds/)*将是便携式的.这些项目只是嵌入资源.但是,资源管理从.NET应用程序更改为Windows Store应用程序.这真的很不幸,因为我认为旧方法更加透明,舒适和可扩展.我的字节流现在在哪里?无论如何,我们可以共享基础资源(即图像和声音),但不能共享项目.我们需要创建两个单独的项目,一个用于Windows Store,另一个用于WPF,以提供资源.(would be portable. These projects just embed resources. However, resource management changed from .NET applications to Windows Store applications. This is really unfortunate, as I think that the old way was a lot more transparent, comfortable and extensible. Where is my byte stream now? Anyway, we can share the underlying resource (i.e. images and sounds), but we cannot share the projects. We need to create two separate projects, one for Windows Store and one for WPF, to provide resources.)
兴趣点(Points of Interest)
提供的源代码包含完整的Quantum Engine和大多数资产.我只删除了音轨.原因很简单:我不想上传几十MB只是为了给您一些音轨,这可能一点都不有趣.我也删除了除vs模式外的所有游戏模式.(The delivered source code contains the full Quantum Engine and most assets. I only removed the sound tracks. The reason for this is a simple one: I did not want to upload dozens of MB just to give you a few sound tracks, that might not be interesting at all. I also removed all game modes except the versus mode.) 排除这些游戏模式的原因是可能将其发布为Windows Store应用程序.目前,我们正在考虑重新设计某些部件的样式并在Windows Store上免费下载该游戏.如果我们确实发布了应用程序,则可能包括网络模式.但这不是初次提交的标准.(The reason for excluding these game modes is a possible publish process as a Windows Store app. Currently we think about re-styling some parts and publishing the game as a free download on the Windows Store. We might include a network mode if we really publish the app. But that is no criterion for an initial submission.)
历史(History)
- v1.0.0(v1.0.0)|初始版本| 2014年7月13日(| Initial Release | 13.07.2014)
- v1.0.1(v1.0.1)|固定下载包| 2014年7月14日(| Fixed download package | 14.07.2014)
- v1.0.2(v1.0.2)|修正了一些错别字| 2014年7月16日(| Fixed some typos | 16.07.2014)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C#4.0 C#5 C# .NET Windows WPF XAML Dev Architect portable 新闻 翻译