通过1945年演示项目学习XNA 2D引擎IceCream(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/learning-xna-2d-engine-icecream-with-1945-demo-pro-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 23 分钟阅读 - 11222 个词 阅读量 0通过1945年演示项目学习XNA 2D引擎IceCream(译文)
原文地址:https://www.codeproject.com/Articles/327977/Learning-XNA-2D-Engine-IceCream-With-1945-Demo-Pro
原文作者:Tyler Forsythe
译文由本站 robot-v1.0 翻译
前言
IceCream1945 is a demonstration of XNA and the IceCream 2D library in a 2D top-down scrolling shooter similar to 1942 for the NES.
IceCream1945是XNA和IceCream 2D库的演示,其使用的是2D自上而下的滚动射击游戏,类似于1942年的NES.
- 下载1945_Code_Package-1.44 MB(Download 1945_Code_Package - 1.44 MB)
- 下载1945_Demo_Package-382 KB(Download 1945_Demo_Package - 382 KB)
总览(Overview)
XNA是Microsoft提供的出色的游戏开发SDK.它处理了许多核心游戏引擎功能,使您作为开发人员可以直接进入有趣的内容.但是,由于它是开放式的,可以适应所有人,2D和3D游戏,因此,一旦缩小要制作的游戏的范围和类型,它可能会起作用很多.如果要制作2D游戏,则从一个非常强大的"一刀切"的库开始,该库需要大量改进.在XNA之上使用另一层使您更接近您的游戏类型是有意义的,而无需编写任何代码.(XNA is a wonderful game development SDK from Microsoft. It handles so many core game-engine features and allows you as the developer to jump right into the fun stuff. But because it’s open-ended to fit everyone, 2D and 3D games, it can be a bit much to work with once you’ve narrowed down the scope and type of game you want to make. If you’re making a 2D game, you start with a very powerful one-size-fits-all library that takes a lot of refinement. It makes sense to use another layer on top of XNA to get you even closer to your game type without yet having to write any code.) IceCream是用XNA编写的框架,用于处理基于2D Sprite的游戏.如果这是您想要制作的游戏,那么本文和框架适合您.如果您需要3D,则最好花时间阅读其他内容.(IceCream is a framework written in XNA to handle 2D sprite-based games. If that’s the sort of game you want to make, this article and framework are for you. If you want 3D, your time will be better spent reading something else.) 在深入探讨本文之前,我建议您下载源代码和示例演示应用程序并进行演示.它很短,但是演示了本文讨论的许多内容:加载场景,精灵移动,玩家输入(WASD),敌人,子弹和碰撞,动画,滚动背景等.控件是:(Before diving into the meat of the article, I would encourage you to download the source code and sample demo application and give it a playthrough. It’s very short but demonstrates many of the things this article discusses: loading scenes, sprite movement, player input (WASD), enemies, bullets and collisions, animation, a scrolling background, etc. The controls are:)
- 机芯:W,A,S和D(Movement: W, A, S, and D)
- 火子弹:太空(Fire bullet: Space)
- 投下炸弹:左移(Drop bomb: Left Shift)
- 退出:逃生(Quit: Escape) 既然您已经看到了运行中的引擎,那么让我们进一步讨论一下该框架.(Now that you’ve seen the engine in action, let’s talk a little more about the framework.)
什么是IceCream及其历史?(What is IceCream and its history?)
IceCream库是Episcode(LoïcDansart)和conkerjo的劳动成果.自2009年以来一直没有看到存储库提交,直到我找到它,并询问是否存在可用于XNA 4.0兼容性的更新.我很幸运,因为Loïc已经完成了所有工作,但还没有完成.几个小时后,最新的XNA 4.0兼容代码已提交并准备采取行动. Loïc通过电子邮件授予的官方代码许可证可用于您要执行的任何操作,只要它不是针对公共派生编辑器/引擎的.(The IceCream library is the fruits of labor by Episcode (Loïc Dansart) and conkerjo. It hadn’t seen a repository commit since 2009 until I found it and asked if there was an update available for XNA 4.0 compatibility. I was in luck, as Loïc had already done all the work but just hadn’t committed it. A few hours later and the latest XNA 4.0 compatible code was committed and ready for action. The official code license as given by Loïc via email is to do whatever you want with it, as long as it’s not for a public derivative editor/engine.)
IceCream有什么特别之处/为什么要使用它?(What’s Special About IceCream/Why Should I Use It?)
当XNA具有大量的内置功能和GUI编辑器时,为什么还要为XNA编写另一个2D引擎呢?更具体地说,IceCream内置支持Spritesheet,静态和动画Sprite,分层,平铺网格,粒子效果,后处理效果和合成实体(例如:二维人形生物,其手臂和腿部像骨骼一样被动画化,而不是每个位置一个完整的子画面).它甚至还具有一个GUI编辑器,用于将所有这些项目放入您的关卡中(IceCream称为"场景").(Why write yet another 2D engine for XNA when this one has tons of built-in functionality and a GUI editor? More specifically, IceCream has built-in support for spritesheets, static and animated sprites, layering, tile grids, particle effects, post-processing effects, and composite entities (think: 2D humanoid with arms and legs that are animated like a skeleton rather than a single whole sprite for each position). It even has a GUI editor for putting all those items into your levels (“scenes” as IceCream calls them).)
IceCream建立在组件设计模型上.每(IceCream is built on a component design model. Each) SceneItem
可以有0个或多个Components,这是您编写的代码段,可以执行或控制所需的任何操作.最基本的类型是速度分量,它使子图形移动.速度分量可能有一个Vector2来描述X和Y的速度,并且每个(can have 0 or many Components, which are code pieces you write that do or control anything you want. The most basic type is a velocity component, which gives the sprite its movement. A velocity component might have a Vector2 to describe X and Y velocities, and every) Update()
,将精灵移动这些数量.但是IceCream没有任何内置组件,也没有做出有关如何编写游戏的假设.它仅使您能够通过组件将可重用代码附加到每个场景项,(, move the sprite by those amounts. But IceCream doesn’t have any built-in components or make assumptions about how you want to write your game. It just enables you to attach reusable code to every scene item, via components, that is given an) Update()
调用每个循环.(call every loop.)
没有可覆盖的(There is no override-able) Draw()
方法,因为IceCream处理所有图形.这是该引擎的唯一约束.由于它完成了所有绘图,因此如果您要这么做,就没有机会进行自己的绘图.但这也很重要:所有绘图代码均已为您编写. (例外情况是您的主要游戏类继承自(method because IceCream handles all drawing. That’s the only constraint of this engine. Since it does all the drawing, you don’t have the opportunity to do your own drawing if that’s your thing. But that’s also the point: all the drawing code has been written for you. (The exception to this is that your main game class, that inherits from) IceCream.Game
,确实得到了(, does get a) Draw()
覆盖,但组件不覆盖.)(override, but components do not.))
但是,如果您确实缺乏所需的绘图功能,则它是开源的,您可以轻松跳入并对其进行修改以符合您的内心需求.在过去几个月里在代码库中工作之后,我可以说,一旦您了解了事物的位置,就很容易理解.绘图部分有点复杂,因为它功能强大,但不是魔术.(If you feel it’s really lacking a drawing ability you want, however, it’s open-source, and you can easily jump in and modify it to your hearts desire. After working in the codebase for the past few months, I can say that it’s pretty easy to understand once you learn where things are. The drawing portion is a bit complicated because it’s extremely capable, but it’s not magic.)
我将跳过GUI的详细信息,因为我假设您具有一些开发经验和知识.因此,打开随附的项目文件时,MilkShake UI应该在10到15分钟的自我探索中很自然地出现(将MilkShake指向(I’m going to skip over the details of the GUI because I’m assuming that you have some development experience and knowledge. Thus, the MilkShake UI should come pretty naturally in 10 to 15 minutes of self-exploration when opening the included project file (point MilkShake to)IceCream1945/Game.icproj(IceCream1945/Game.icproj)).().)
组件属性(Component Properties)
如前所述,IceCream是基于组件的,而代码的主要位置位于这些组件中.每个组件都可以覆盖以下方法:(As I mentioned, IceCream is component based, and the primary location for your code is within these components. Each component can override the following methods:)
CopyValuesTo(object target)
-由IceCream引擎定期调用以执行深层复制.每当将属性添加到组件时,都需要将其添加到此方法中的复制操作中.由您决定对象状态的哪些部分与深层副本相关,哪些应被跳过.(- Called regularly by the IceCream engine to perform deep-copies. Anytime you add properties to your component, you’ll need to add them to the copy operation that happens in this method. It’s up to you to decide what parts of the object’s state are relevant to a deep copy, and which should be skipped.)OnRegister()
-每当父母时调用(- Called whenever the parent)SceneItem
已向场景注册,这是在场景加载到内存或下一个场景时发生的(is registered with the scene, which happens when the scene is loaded into memory, or the next)Update()
之后(after a)SceneItem
是(is)new
-ed并添加到场景中(脚注:也可以告诉场景立即注册一个项目,而不是下一个(-ed and added to the scene (footnote: it’s also possible to tell a scene to register an item immediately rather than next)Update()
).().)OnUnRegister()
-每当(- Called whenever a)SceneItem
被标记为删除并从场景中删除.(is marked for deletion and removed from the scene.)Update(float elapsedTime)
-调用每个循环使您的组件进入状态,无论这意味着什么.这就是真正的肉发生的地方.检查玩家输入的组件将在此方法中执行输入检查.同样,我们前面提到的VelocityComponent示例将使用此方法来修改其父对象的X和Y位置(- Called every loop for your component to advance state, whatever that might mean. This is where the real meat happens. A component to check for player input would do the input checking in this method. Likewise, our aforementioned VelocityComponent example would use this method to modify the X and Y position of it’s parent)SceneItem
.(.) 有时,在构建场景时将这些属性显示在MilkShake GUI设置区域中是有意义的.为此,我们用(Sometimes it makes sense for these properties to display in the MilkShake GUI settings area when building the scene. To do this, we decorate those properties with)[IceComponentProperty("text")]
.此属性用于告诉MilkShake UI该属性在属性列表UI中应是可编辑的,以及要使用的文本描述.没有此属性的属性不会在MilkShake中公开.考虑这一点的简单方法是,如果它具有(. This attribute is used to tell the MilkShake UI that this property should be editable in the property list UI, and what text description to use. Properties without this attribute are not exposed in MilkShake. The easy way to think of this is, if it has an)IceComponentProperty
装饰器,它是编辑器中的配置值.如果不是,则可能是内部管理的状态属性.(decorator, it’s a configuration value in the editor. If not, it’s probably an internally managed state property.) IceComponentProperty的示例(Example of IceComponentProperty)```c# [IceComponentProperty(“Velocity Vector”)] public Vector2 Velocity { get; set; }
**UI中IceCream组件属性的各种示例(*Various Examples of IceCream Component Properties in the UI*)**![327977/Property_Samples.jpg](https://www.codeproject.com/KB/game/327977/Property_Samples.jpg)
示例:Full VelocityComponent(*Example: Full VelocityComponent*)```c#
namespace IceCream1945.Components
{
[IceComponentAttribute("VelocityComponent")]
public class VelocityComponent : IceComponent
{
[IceComponentProperty("Velocity Vector")]
public Vector2 Velocity { get; set; }
public VelocityComponent() {
Enabled = false;
//we manually Enable the component in other locations
//of code. By default, all components are enabled.
}
public override void OnRegister() { }
public override void CopyValuesTo(object target) {
base.CopyValuesTo(target);
if (target is VelocityComponent) {
VelocityComponent targetCom = target as VelocityComponent;
targetCom.Velocity = this.Velocity;
}
}
public override void Update(float elapsedTime) {
if (Enabled) {
this.Owner.PositionX += Velocity.X * elapsedTime;
this.Owner.PositionY += Velocity.Y * elapsedTime;
}
}
}
}
进入代码(Getting Into the Code)
跳过组件并进入IceCream1945的初始游戏启动(不是核心IceCream库的一部分),我们有了(Moving past components and getting into initial game startup for IceCream1945 (not part of the core IceCream library), we have our) MainGame
类,继承自(class, which inherits from) IceCream.Game
.我们可以选择覆盖常见的XNA方法,例如(. We can optionally override the common XNA methods like) Update()
和(and) Draw()
.我使用此类进行基本初始化,保留当前场景以及在场景之间导航(例如,将标题屏幕设置为1级,将1级设置为游戏结束等).在我的示例中,我希望此类非常轻巧且相对笨拙.(. I use this class for basic initialization, holding the current scene, and navigating among scenes (e.g., title screen to level 1, level 1 to game over, etc). In my sample, I wanted this class to be very lightweight and relatively dumb.)
同样,此类是自定义代码,而不是标准IceCream代码库的一部分.(Again, this class is custom code, and not part of the standard IceCream codebase.)
public class MainGame : IceCream.Game
{
public static GameScene CurrentScene;
public static readonly string SplashIntroSceneName = "Splash";
public static readonly string Level1SceneName = "Level1";
public static readonly string EndingSceneName = "Ending";
public MainGame() {
GlobalGameData.ContentDirectoryName = ContentDirectoryName = "IceCream1945Content";
GlobalGameData.ContentManager = Content;
}
protected override void LoadContent() {
base.LoadContent();
this.IsFixedTimeStep = false;
CurrentScene = new MenuScene(SplashIntroSceneName);
CurrentScene.LoadContent();
}
protected override void Update(GameTime gameTime) {
if (GlobalGameData.ShouldQuit) {
if (CurrentScene != null)
CurrentScene.UnloadContent();
this.Exit();
return;
}
if (CurrentScene.MoveToNextScene) {
if (CurrentScene.SceneName == SplashIntroSceneName ||
CurrentScene.SceneName == EndingSceneName) {
CurrentScene.UnloadContent();
CurrentScene = new PlayScene(Level1SceneName);
CurrentScene.LoadContent();
}
else if (CurrentScene.SceneName == Level1SceneName) {
CurrentScene.UnloadContent();
CurrentScene = new MenuScene(EndingSceneName);
CurrentScene.LoadContent();
}
}
base.Update(gameTime);
CurrentScene.Update(gameTime);
}
protected override void Draw(GameTime gameTime) {
base.Draw(gameTime);
CurrentScene.Draw(gameTime);
}
protected override void UnloadContent() {
base.UnloadContent();
}
}
我在这里指的场景与(The scene that I’m referring to here is different than the) IceCream.Scene
.我已经创建了一个摘要(. I’ve created an abstract) GameScene
的基础类(class that is the base class of) PlayScene
和(and) MenuScene
.这使我可以重用(. This allows me to reuse common methods in) GameScene
但对于类似"菜单"的场景与"游戏性"的场景(可能还有其他场景)有专门的环境处理.在一个(but have specialized circumstance handling for a “menu”-like scene vs a “gameplay” scene (and potentially others). In a) GameScene
,我希望玩家能够积极地控制游戏对象,让AI能够思考,让相机移动.但是一个(, I’m expecting for the player to be actively controlling a game object, for the AI to be thinking, and for the camera to be moving. But a) MenuScene
都是关于呈现信息并等待用户输入的.因此,我觉得将这两种情况分成单独的对象而不是用条件附加单个对象会更清楚.(is all about presenting information and waiting for user input. Hence, I felt it would be more clear to split the two cases into separate objects rather than litter a single object with conditionals.)
MenuScene类Update()方法(MenuScene Class Update() method)```c#
public override void Update(GameTime gameTime) {
base.Update(gameTime);
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
IceCream.Debug.OnScreenStats.AddStat(string.Format("FPS: {0}",
DrawCount / gameTime.TotalGameTime.TotalSeconds));
if (ReadInput || WaitBeforeInput.Stopwatch(100)) {
ReadInput = true;
if (InputCore.IsAnyKeyDown()) {
MoveToNextScene = true;
}
}
}
## 预加载,缓存和GlobalGameData(*Preloading, Caching, and GlobalGameData*)
GlobalGameData类和启动缓存(*GlobalGameData Class and Startup Caching*)```c#
public static class GlobalGameData
{
public static ContentManager ContentManager = null;
public static bool ShouldQuit = false;
public static string ContentDirectoryName = string.Empty;
public static int ResolutionHeight = 720, ResolutionWidth = 1280;
public static int PlayerHealth;
public static int MaxPlayerHealth = 18;
public static bool SoundOn = true;
public static bool MusicOn = true;
public static float SoundEffectVolume = 0.3f;
public static float MusicVolume = 0.3f;
public static List<sceneitem> InactiveSceneItems = new List<SceneItem>();
public static List<sceneitem> ActiveSceneItems = new List<SceneItem>();
public static SceneItem PlayerAnimatedSprite = null;
public static PostProcessAnimation ScreenDamageEffect = null;
public static PointTracking PlayerOnePointTracking = new PointTracking();
}
GlobalGameData.PlayerAnimatedSprite =
scene.GetSceneItem<AnimatedSprite>("PlayerPlane_1");
HealthBarItem = scene.GetSceneItem<Sprite>("HealthBar");
GlobalGameData.PlayerHealth = GlobalGameData.MaxPlayerHealth;
//[...]
GlobalGameData.ScreenDamageEffect =
scene.CreateCopy<PostProcessAnimation>("PlayerDamageScreenEffect");
GlobalGameData.ScreenDamageEffect.Stop();
scene.RegisterSceneItem(GlobalGameData.ScreenDamageEffect);
//[...]
foreach (SceneItem si in scene.SceneItems)
GlobalGameData.InactiveSceneItems.Add(si);
//sort the inactive list so we only have to look at the very
// first one to know if there is anything to activate.
GlobalGameData.InactiveSceneItems.Sort(delegate(SceneItem a, SceneItem b)
{ return b.PositionY.CompareTo(a.PositionY); });
GlobalGameData
是一个单例类(我的自定义代码,不是基础IceCream的一部分),其中包含对游戏设置和常用对象的引用.在诸如碰撞检测之类的过程中,有必要遍历所有场景项以进行检测.诸如此类的事情非常慢,在您的游戏经过简单的概念验证之后,可能会赶上您.所以我创建了Active(is a singleton class (my custom code, not part of base IceCream) that holds references to game settings and commonly used objects. During things like collision detection, it’s necessary to run through all scene items for detection. Something like that is extremely slow and can catch up to you after your game is past a simple proof of concept. So I’ve created Active and) InactiveSceneItems
为此目的列出清单.当遍历寻找碰撞的项目时,我只查看活动的东西,我认为它们是显示在屏幕上或移到屏幕上方的精灵(尽管应该由边界检测组件自动消除它们).这样,我不会在播放器刚启动时检查关卡末端的场景项目,而是可以控制播放器在关卡中移动时场景项目的启用,而不是让所有精灵立即开始遍历关卡.(lists for just this purpose. When running through items looking for collisions, I only look through what’s active, which I consider to be sprites that are shown on screen or have moved beyond it (though those should be eliminated automatically by a bounds detection component). This way I’m not checking scene items at the end of the level when the player just starts, and I can control the enabling of scene items as the player moves throughout the level rather than having all sprites immediately start traversing the level.)
有更快的方法,例如将屏幕划分为象限或其他部分,但目前只有屏幕上的活动列表(There are faster methods, such as dividing the screen into quadrants or other sectioning, but for now, an active list of only on-screen) SceneItem
s足够快.(s is more than fast enough.)
此外,当相机"向上"移动时,诸如健康框和得分之类的内容将每帧移动到屏幕顶部.这要求代码始终与(Additionally, things like the health box and score are moved every frame to be at the top of the screen while the camera moves “upward”. This requires the code to always touch the) PositionY
属性,并且每帧都找到这些对象非常浪费.因此,我们只找到它们一次,就可以随时获得参考.(property, and it’s very wasteful to find these objects every frame. So we find them once and keep a reference at our fingertips.)
本质上,(Essentially,) GlobalGameData
是保存"全局"变量的位置.在更强大的游戏中,这些引用可能会拆分为更简洁的对象,例如静态Settings对象和(is where “global” variables are held. In a more robust game, these references would probably be split into more concise objects, such as a static Settings object and a) SceneItemCache
对象或相应的管理类型对象.但是在所有情况下,我们只需要一个数据副本,就可以从游戏中的几乎所有地方访问它.当全局可访问的上下文对象将以相同的方式工作时,我们不需要将某种上下文对象传递给我们的所有方法(因为我们永远不需要在单个运行实例中将两个上下文共存) ).在"全局变量不好"与膨胀所有方法签名以在整个代码中传递对对象的引用之间,存在一条细线.(object, or corresponding management-type objects. But in all cases, we only need and want one copy of this data, and we want it accessible from nearly everywhere in our game. We don’t want to have to pass around some sort of context object to all our methods when a globally-accessible context object will work just the same (because we’ll never need to have two context’s co-exist in a single running instance). There is a fine line between “global variables are bad” and bloating all your method signatures to pass a reference to an object throughout your code.)
游戏循环:场景移动和背景滚动(Game Loop: Scene Movement and Background Scrolling)
场景随着相机的实际移动而移动.一些滚动器通过将场景移动到静态相机的视图或其他间接方式进行工作,但我认为仅移动相机并使其在整个关卡中进行扫描是最有效的方法.一些原因包括:(The scene is moved along with actual movement of the camera. Some scrollers work by moving the scene into the view of a static camera or other indirect ways, but I decided it was most efficient and mentally straight-forward to move just the camera and have it scan across the level. Some of the reasons include:)
- 如果必须将场景移到摄像机视图中,则需要每帧更改每个场景项的位置,以将其移到摄像机视图中.通过移动相机,仅必须移动通过AI实际移动的场景项目.(If I had to move the scene into the view of the camera, I would need to modify the position of every single scene item every single frame to move them into the view of the camera. By moving the camera, only the scene items actually moving via AI have to move.)
- 使脚本保持静态,但通过脚本在摄像头外部生成对象会增加代码和GUI的复杂性.(Keeping the camera static but spawning objects just outside the view of the camera via a script adds significant code and GUI complexity.)
- GUI编辑器已经设置好,可以让摄像机在布局场景中移动.使用IceCream的目的是利用他人编写的工具.(The GUI editor is already setup for camera movement over a laid-out scene. The point of using IceCream is to leverage the tools someone else has written.) 相机移动代码(Code of Camera Movement)```c# float ScrollPerUnit = 60.0f; […] float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds; float distance = elapsed * ScrollPerUnit;
scene.ActiveCameras[0].PositionY -= distance; BoundsComponent.ResetScreenBounds();
背景运动只是部分真实的.它只是屏幕宽度和32像素高的单个精灵,复制到屏幕的高度再加上两个.每次底部条从摄像机的视线中消失时,都会将其拖到顶部.因此,随着相机沿着场景移动,背景图像将永远从下到上移动,并且我们不必浪费内存将条带复制到场景的整个垂直长度.我们只需要足够的内存来覆盖用户视图.(*The background movement is only partially real. It's just a single sprite the width of the screen and 32 pixels tall, duplicated to the height of the screen plus two. Each time the bottom strip disappears from the view of the camera, it is shuffled to the top. So the background images are perpetually being moved bottom to top as the camera moves along the scene, and we don't have to waste memory copying the strip to the entire vertical length of the scene. We only need enough memory to cover the users view.*)
背景精灵构造规范(*Code of Construction of Background Sprites*)```c#
/* Load Background Water sprites */
Sprite water = scene.CreateCopy<Sprite>("Water1280");
WaterHeight = water.BoundingRectSize.Y;
double totalWaters = Math.Ceiling(GlobalGameData.ResolutionHeight / WaterHeight) + 1;
for (int i = -1; i < totalWaters; ++i) {
Sprite w = scene.CreateCopy<Sprite>("Water1280");
w.Layer = 10;
w.PositionX = 0;
w.PositionY = i * WaterHeight;
scene.RegisterSceneItem(w);
BackgroundWaters.Add(w);
}
背景精灵移动和交换代码(Code of Background Sprite Movement and Swapping)```c# /* Background Water Movement (totally fake)
- Since our camera is moving up the Y axis, all we have to do is shift the background upwards
- everytime the camera moves the equivalent of a tile height. */ BackgroundOffset += distance; if (BackgroundOffset > WaterHeight) { foreach (Sprite water in BackgroundWaters) { water.PositionY -= WaterHeight; } BackgroundOffset -= WaterHeight; }
对于精灵和相机运动,我决定根据两次更新调用之间经过的时间来移动游戏中的物体. XNA喜欢以60 fps的速度运行,无论是否启用(*For sprite and camera movement, I decided to move things in the game based upon the time that has passed between update calls. XNA likes to run at 60 fps regardless of whether you enable*) `IsFixedTimeStep` ,并且在目前的低复杂度游戏中,并没有太大的区别.但是,如果由于场景复杂性而导致帧速率开始下降,那么这种设计决策将使游戏更具可玩性和一致性.(*, and in a low complexity game as it is right now it doesn't make much difference. But if our framerate ever starts to drop due to scene complexity, this design decision will keep the game more playable and consistent.*)
基于时间的精灵运动示例(*Example of Time-Based Sprite Movement*)```c#
public override void Update(float elapsedTime) {
if (Enabled) {
this.Owner.PositionX += Velocity.X * elapsedTime;
this.Owner.PositionY += Velocity.Y * elapsedTime;
}
}
编写此方法的另一种方法是,无论经过多少时间,每次调用都将每个子画面平移X个像素以进行更新.使用此方法的80年代和90年代初期编写的非常古老的游戏在当今的PC上已失控.那时,开发人员认为他们的游戏不会在今天继续玩,因此他们被编写为在没有任何限制的情况下全力以赴.我认为重要的是要意识到代码中的生命比您想象的要多得多.(The other way to write this method is to move each sprite a flat X number of pixels each call to update, regardless of how much time has passed. Very old games written in the 80’s and early 90’s that used this method are out of control on today’s PCs. Back then, developers didn’t think their games would still be played today, so they were written to go all-out with no limiter of any kind in place. I think it’s important to realize your code has much more life in it than you think.) 为了防止这种情况在不可避免的将来发生,即使硬件可以更快地玩游戏,XNA框架也应该足够智能,以将帧速率限制为每秒60帧.(To prevent that scenario from happening in the inevitable future, the XNA framework is supposed to be intelligent enough to cap the framerate at 60 frames per second even if the hardware can play your game much faster.)
玩家移动和射击:PlayerControllableComponent(Player Movement and Firing: PlayerControllableComponent)
PlayerControllableComponent应用于播放器控制的精灵.它通过IceInput监视输入,并相应地移动精灵.它是为多个玩家设置的,但我尚未实现.可以轻松修改其余代码以进行合作,但是我认为这超出了我的第一个练习范围.(The PlayerControllableComponent is applied to the sprite that the player controls. It watches for input via IceInput and moves the sprite accordingly. It’s setup for multiple players, but I have not implemented it. The rest of the code could easily be modified for co-op, but I decided it was out of scope for my first exercise.) PlayerControllableComponent类Update()(PlayerControllableComponent Class Update())```c# public override void Update(float elapsedTime) { // when the owner uses PlayerIndex.One if (Playerindex == PlayerIndex.One) { // if W button is pressed if (InputCore.IsKeyDown(Keys.W)) { // we go upwards Owner.PositionY -= Velocity.Y; } // if S key is pressed if (InputCore.IsKeyDown(Keys.S)) { // we go downwards Owner.PositionY += Velocity.Y; } // if A button is pressed if (InputCore.IsKeyDown(Keys.A)) { // we go to the left Owner.PositionX -= Velocity.X; } // if D button is pressed if (InputCore.IsKeyDown(Keys.D)) { // we go to the right Owner.PositionX += Velocity.X; }
if (BulletTimer.Stopwatch(100) && InputCore.IsKeyDown(Keys.Space)) {
//fire projectile
AnimatedSprite newBullet = Owner.SceneParent.CreateCopy<AnimatedSprite>("FlamingBullet");
newBullet.Visible = true;
newBullet.Position = Owner.Position;
newBullet.PositionX += Owner.BoundingRectSize.X / 2;
VelocityComponent velocityCom = newBullet.GetComponent<VelocityComponent>();
velocityCom.Enabled = true;
Owner.SceneParent.RegisterSceneItem(newBullet);
Sound.Play(Sound.Laser_Shoot_Player);
}
if (BombTimer.Stopwatch(100) && InputCore.IsKeyDown(Keys.LeftShift)) {
//drop bomb
Sprite newBullet = Owner.SceneParent.CreateCopy<Sprite>("Bomb");
newBullet.Visible = true;
newBullet.Position = Owner.Position;
newBullet.PositionX += Owner.BoundingRectSize.X / 2;
Owner.SceneParent.RegisterSceneItem(newBullet);
}
}
//[...] Unused code relating to PlayerTwo
if (InputCore.IsKeyDown(Keys.Escape)) {
GlobalGameData.ShouldQuit = true;
}
}
该组件监视所有玩家的输入,包括发射按钮和炸弹按钮,因此它还在必要时处理产生这些场景项目. IceCream使我们能够非常轻松地处理此问题.使用MilkShake,我创建了FlamingBullet动画精灵,其中已附加并配置了以下组件:(*This component watches for all player input, including the fire and drop-bomb buttons, so it also handles spawning those scene items when necessary. IceCream allows us to handle this very easily. With MilkShake, I created a FlamingBullet animated sprite with the following components already attached and configured:*)
- VelocityComponent:给此项目符号X和Y速度.(*VelocityComponent: Give this bullet X and Y velocity.*)
- LifeComponent:如果此子弹的寿命超过两秒钟,请销毁它.这样可以防止子弹穿过其他支票而永远存活.如果达到2秒,则抛出异常将对测试更有用.当前,没有任何玩家的子弹可以活那么长,所以做的任何事情都是错误的.可以将某些组件编写为小型测试,甚至是谨慎的做法,以确保其他组件正常工作.(*LifeComponent: If this bullet lives for longer than two seconds, destroy it. This prevents bullets from slipping through other checks and living forever. This is more useful for testing by throwing an exception if 2 seconds is ever reached. Currently, no player bullets should live that long, so anything that does is an error. It's possible and even prudent to write some components as small tests to ensure other components are doing their job.*)
- BoundsComponent:如果此子弹完全移出相机,请销毁它.我们不希望玩家一直破坏整个关卡.(*BoundsComponent: If this bullet moves completely out of view of the camera, destroy it. We don't want the player destroying objects all the way down the level.*)
- BulletComponent:此组件负责检查碰撞和造成损坏.(*BulletComponent: This component handles checking for collisions and dealing damage.*)
当玩家发射烈焰子弹时,我请IceCream制作此模板的副本,在VelocityComponent上调整速度,然后向IceCream注册场景项.然后,IceCream引擎和我的组件将接管其余的工作,而PlayerControllableComponent可以完全忘记它.(*When the player fires a flaming bullet, I ask IceCream to make a copy of this template, tweak the speed on the VelocityComponent, and register the scene item with IceCream. The IceCream engine and my components then take over the rest, and the PlayerControllableComponent can completely forget about it.*)
## 碰撞检测(*Collision Detection*)
此演示应用程序中的碰撞检测仅与"子弹"(也称为弹丸)有关.玩家发射子弹,而敌人则发射子弹.每个子弹都是一个带有BulletComponent的动画子画面,该子弹具有两个标志,用于指示是否会损坏玩家以及是否会损坏敌人(可能两者都有).(*Collision detection in this demo application is only relevant for "bullets", aka projectiles. The player fires bullets, and enemies fire bullets. Each bullet is an animated sprite with a BulletComponent, which has two flags for whether it can damage the player and whether it can damage enemies (it could be both).*)
当该组件执行时(*When this component executes*) `Update()` ,它会查看(*, it looks through all scene items in the*) `ActiveSceneItems` 列出并将其边界矩形与项目符号进行比较.如果它们相交,那就是碰撞.一种更好的方法,我在这里没有实现,是在确定矩形交叉之后,进行逐像素或多边形检测的更精细的测试.但是,我再次认为这超出了本次练习的范围. IceCream最初支持多边形碰撞检测,但尚未完全实现.(*list and compares its bounding rectangle to the bullets. If they intersect, it's a collision. A better way of it doing it that I didn't implement here is, after it's determined the rectangles cross, drop into a more granular test of either pixel-by-pixel, or polygon detection. However, I again decided this was out of scope for this initial exercise. IceCream has some initial support for polygon collision detection, but it's not yet fully implemented.*)
如果发生碰撞,则会生成爆炸动画并适当地施加伤害.对于敌人来说,现在他们只是死了.但是,玩家的生命线会随着每次击中而减少.此外,如果播放器被击中,则会短暂播放后处理效果,从而使屏幕模糊并非常短暂地闪烁.这给了玩家视觉上的回击,这要比看子弹是否被击中或生命减少而引人注目.无论他们正在看屏幕的哪个部分,它都能吸引他们的注意力.(*If there is a collision, an explosion animation is spawned and damage is applied appropriately. For enemies, right now they simply die. The player, however, has a life bar that decreases per hit. Additionally, if the player is hit, a post-processing effect is briefly played that causes the screen to blur and flash very briefly. This gives the player visual feedback that they were struck that is far more noticeable than looking to see if bullets hit or life decreased. It catches their attention regardless of what part of the screen they're looking at.*)
```c#
public override void Update(float elapsedTime) {
//We get all scene items ( not recommended in bigger games, at least not every frame)
for (int i=0; i < GlobalGameData.ActiveSceneItems.Count; ++i) {
SceneItem si = GlobalGameData.ActiveSceneItems[i];
if (si != Owner && (si.GetType() == typeof(Sprite) ||
si.GetType() == typeof(AnimatedSprite)) && !Owner.MarkForDelete) {
if (CanDamageEnemy) {
if (si.CheckType(2) && Owner.BoundingRect.Intersects(si.BoundingRect)) {
si.MarkForDelete = true;
Owner.MarkForDelete = true;
GlobalGameData.ActiveSceneItems.Remove(si);
GlobalGameData.ActiveSceneItems.Remove(Owner);
--i;
AnimatedSprite explosion =
Owner.SceneParent.CreateCopy<AnimatedSprite>("BigExplosion");
explosion.Visible = true;
explosion.Position = si.Position;
explosion.PositionX += si.BoundingRectSize.X / 2;
explosion.PositionY += si.BoundingRectSize.Y / 2;
explosion.CurrentAnimation.LoopMax = 1;
explosion.CurrentAnimation.HideWhenStopped = true;
Owner.SceneParent.RegisterSceneItem(explosion);
GlobalGameData.PlayerOnePointTracking.AddScore(50);
Sound.Play(Sound.ExplosionHit);
}
}
if (CanDamagePlayer) {
if (Owner.BoundingRect.Intersects(GlobalGameData.PlayerAnimatedSprite.BoundingRect)) {
--GlobalGameData.PlayerHealth;
Owner.MarkForDelete = true;
//if the player is getting bombarded, we want many flashes
GlobalGameData.ScreenDamageEffect.Reset();
GlobalGameData.ScreenDamageEffect.Play();
Sound.Play(Sound.Quick_Hit);
}
}
}
}
}
后处理对运动员-子弹碰撞的影响(Post Processing Effect on Player-Bullet Collision)
如前所述,IceCream开箱即用地支持各种后期处理效果.当播放器被击中并失去生命值时,许多经典的滚动射击游戏都具有某种屏幕闪烁效果.后处理效果是实现这一技巧的自然方法.我会在第一次加载场景时加载并缓存该效果,并且仅在每次发生碰撞时指示IceCream播放一次.(As I mentioned earlier, IceCream supports various post-processing effects out of the box. Many classic scrolling shooters have some sort of screen-flashing effect when the player is hit and loses a health point. A post-processing effect is the natural way to pull off this trick. I load and cache the effect when the scene is first loaded, and just instruct IceCream to play it a single time whenever a collision occurs.) 我通过Milkshake中的此UI设置了效果:(I set up the effect through this UI in Milkshake:)
然后在BulletComponent中调用它.我打电话(And then call it in the BulletComponent. I call) Reset()
在我调用游戏之前,如果反复击打播放器,效果将立即重新开始,而不仅仅是结束播放一次.我认为这种对玩家的即时反馈对于感觉"紧张"的游戏至关重要.(before I call play, so if the player is hit repeatedly the effect will start over instantly rather than only finishing playing once. I think this sort of immediate feedback to the player is key to a game that feels “tight”.)
if (CanDamagePlayer) {
if (Owner.BoundingRect.Intersects(GlobalGameData.PlayerAnimatedSprite.BoundingRect)) {
--GlobalGameData.PlayerHealth;
Owner.MarkForDelete = true;
GlobalGameData.ScreenDamageEffect.Reset();//if the player is getting bombarded, we want many flashes
GlobalGameData.ScreenDamageEffect.Play();
Sound.Play(Sound.Quick_Hit);
}
}
敌人运动和AI,以及标签功能(Enemy Movement and AI, and Tags Feature)
IceCream成为开源的妙处之一是,如果您认为它不能满足您的所有需求,则可以自由对其进行修改.我所做的整洁更改之一是添加了一个"(One of the wonderful things about IceCream being Open-Source is that you’re free to modify it if you don’t think it serves all your needs. One of the neat changes I made was to add a “) tags
属性,这是我用于分组和其他属性的字符串的简单列表.因为在游戏过程中解析此标记数据会很慢,所以在加载场景时我会一次性解析并缓存.(” property, which is a simple list of strings that I use for grouping and other attributes. Because it would be slow to parse this tag data during game-play, I parse and cache in one pass when the scene is loaded.)
组(标签)缓存(Group (Tag) Caching)```c#
protected Dictionary<string, List> Cache_TagItems;
public GameScene(string sceneName) { […] Cache_TagItems = new Dictionary<string, List>(); }
public virtual void LoadContent() { […] CacheTagItems(); } […] public string GetGroupTagFor(SceneItem si) { foreach (string tag in si.Tags) { if (tag.StartsWith(“group”)) return tag; } return string.Empty; }
protected void CacheTagItems() { if (scene == null) throw new Exception(“Can’t cache tags before loading a scene (scene == null)");
foreach (SceneItem si in scene.SceneItems) {
foreach (string tag in si.Tags) {
if (!string.IsNullOrEmpty(tag)) {
if (!Cache_TagItems.ContainsKey(tag)) {
Cache_TagItems[tag] = new List<SceneItem>();
}
Cache_TagItems[tag].Add(si);
}
}
}
}
public List GetItemsWithTag(string tag) { if (Cache_TagItems.ContainsKey(tag)) return Cache_TagItems[tag]; else return new List(); }
在IceCream1945中,标签的主要用途是对敌人进行分组.随着级别的滚动,每个(*In IceCream1945, the primary use of tags is for grouping enemies. As the level is scrolled, each*) `Update()` 呼叫会检查子画面是否已在相机视野附近越过阈值,并且应该在场景中栩栩如生.因为这是一个自上而下的射手,所以精灵会像波浪一样栩栩如生.也就是说,四个或更多的精灵将作为一个在整个场景中移动.为方便起见,通过标记机制为每个子画面分配了一个组.激活一个精灵时,将读取组标签,并且将激活具有匹配组的所有其他场景项目.这激活了整个"波浪",并使它们作为一个整体运动,而没有额外的代码或关卡设计复杂性.(*call checks to see if a sprite has crossed a threshold near the camera's view and should come to life in the scene. Because this is a top-down shooter, sprites tend to come to life in waves. That is, four or more sprites will move across the scene as one. To facilitate this, each sprite is assigned a group through the tag mechanism. When one sprite is activated, the group tag is read, and all other scene items with a matching group are activated. This activates the entire "wave" and keeps them moving as one without extra code or level design complexity.*)
SceneItem激活和组激活代码段(*SceneItem Activation and Group Activation Snippet*)```c#
int activationBufferUnits = 20;
for (int i=0; i < GlobalGameData.InactiveSceneItems.Count; ++i) {
SceneItem si = GlobalGameData.InactiveSceneItems[i];
if (si.BoundingRect.Bottom + activationBufferUnits > scene.ActiveCameras[0].BoundingRect.Top) {
//grab all other scene items in the same group and turn them on as well
EnableSceneItem(si);
GlobalGameData.InactiveSceneItems.RemoveAt(i);
--i;
string groupTag = GetGroupTagFor(si);
if (groupTag == string.Empty)
continue;
List<sceneitem> groupItems = GetItemsWithGroupTag(groupTag);
foreach (SceneItem gsi in groupItems) {
EnableSceneItem(gsi);
GlobalGameData.InactiveSceneItems.Remove(gsi);
}
}
else //if we didn't find anything to activate, we can just abort the loop. Our inactive list is sorted
break; //so that the "earliest" items are first. Thus, continuing the loop is pointless.
}
计分与成就(Scoring and Achievements)
最后,对于得分和成就跟踪,我有一个(Finally, for scoring and achievement tracking, I have a) PointTracking
如果您想添加多人游戏支持,则可以按玩家创建(或配置为多人游戏存储)单例.现在,它仅用于评分,但它打算在整个应用程序中用作其他用途,例如射击次数,击杀次数等,这些都是有趣的统计数据,可以在关卡结束时查看,甚至可以追踪为玩家的一生而成就.(singleton that can be created per-player (or configured for multi-player storage) if you wanted to add multi-player support. Right now it’s only used for scoring, but it’s intended to be in scope for the entire application for other things like number of shots fired, number of kills, etc that can be fun stats to view at the end of a level, or even track for the life of a player and grant achievements.)
PointTracking代码段(PointTracking code snippet)```c#
///
/// General class for tracking score, number of kills, shots fired,
/// and other metrics for achivements and similar
///
public class PointTracking
{
public int PlayerScore { get; private set; }
public void AddScore(int additionalPoints) {
PlayerScore += additionalPoints;
}
}
到此结束了通过示例游戏IceCream1945到2D XNA IceCream库的介绍.希望您喜欢这篇文章,并下载示例应用程序和源代码. IceCream是一个非凡的库,值得更多关注和贡献.(*That concludes this intro to the 2D XNA IceCream library through the sample game IceCream1945. I hope you enjoyed the article and will download the sample application and source code. IceCream is a phenomenal library that deserves more attention and contributions.*)
请享用!泰勒`福赛斯(Tyler Forsythe),(*Enjoy! Tyler Forsythe,*) [http://www.tylerforsythe.com/(*http://www.tylerforsythe.com/*)](http://www.tylerforsythe.com/) .(*.*)
## 许可
本文以及所有相关的源代码和文件均已获得[The Code Project Open License (CPOL)](http://www.codeproject.com/info/cpol10.aspx)的许可。
C#
.NET
XBox
Windows
Dev
XNA
XNA4.0
game
新闻
翻译