赞助广告

 

年份

最新评论

评论 RSS

创建XNA Shooter游戏——整合在一起

clock 九月 7, 2010 08:46 by author alex
整合在一起 在前两章你已经看到了XNA Shooter的一些代码片段了。在开始之前你需要XNA Shooter项目的所有文件,这些文件应首先被建立。该项目需要所有XACT项目的音效,以及shader、字体纹理、测试、菜单等。最后,在添加.x三维模型和纹理文件后您可以继续下去了,并通过Txeture、Shader和Model类中的单元测试对其进行测试。 在进行了以下操作后,图11-1显示了名为Xna Shooter的新项目: 创建一个名为“XnaShooter”的新Windows XNA游戏项目 从Rocket Commander项目中拖曳所有源代码文件。因为在XnaShooter中不需要,所以下列文件被删除,按命名空间排序: Game命名空间:Asteroid.cs,BaseAsteroidManager.cs,GameAsteroidManager.cs,Level.cs, PhysicsAsteroidManager.cs,SmallAsteroid.cs和SpaceCamera.cs GameScreens命名空间:Help.cs,MissionSelection.cs和Options.cs Graphics命名空间:AnimatedModel.cs和LensFlare.cs Shaders命名空间:ParallaxShader.cs和PreScreenSkyCubeMapping.cs Game1.cs也被移除,RocketCommanderGame类更名为XnaShooterGame。 所有文件中被使用的RocketCommanderXna命名空间被替换为为XnaShooter命名空间。 Mission.cs,Player.cs,MainMenu.cs和XnaShooterGame.cs中的大部分代码被注释,以便编译该项目。 图11-1 声音 源代码是重要的,但没有纹理,声音和三维模型你没法完成一个真正的游戏。由于在第9章已经讨论过XnaShooter XACT项目,这将是添加到新项目中的第一个东西。加入项目中的唯一一个文件是XnaShooter.xap文件,它使采用了很多.wav文件。我倾向于把所有.wav文件也添加到项目中,因为我想看到有哪些文件被直接用于该项目。 在XACT项目被添加到Sounds命名空间后,你就能更改Sound类并通过TestPlaySounds测试所有的声音。该单元测试并不包括所有的声音,但最重要的音效都测试了。像游戏音乐和爆炸声使用了不只一个声音文件。这意味着,如果你播放爆炸声时,三个爆炸声音文件中的一个会被随机选择和播放。这无需特别代码,一切都在XACT中被设置。 public static void TestPlaySounds() { TestGame.Start( delegate { if (Input.MouseLeftButtonJustPressed || Input.GamePadAJustPressed) Sound.Play(Sounds.Defeat); else if (Input.MouseRightButtonJustPressed || Input.GamePadBJustPressed) Sound.Play(Sounds.Victory); else if (Input.KeyboardKeyJustPressed(Keys.D1)) Sound.Play(Sounds.GameMusic); else if (Input.KeyboardKeyJustPressed(Keys.D2)) Sound.Play(Sounds.EnemyShoot); else if (Input.KeyboardKeyJustPressed(Keys.D3)) Sound.Play(Sounds.Explosion); else if (Input.KeyboardKeyJustPressed(Keys.D4)) Sound.Play(Sounds.Health); else if (Input.KeyboardKeyJustPressed(Keys.D5)) Sound.Play(Sounds.PlasmaShoot); else if (Input.KeyboardKeyJustPressed(Keys.D6)) Sound.Play(Sounds.MgShoot); else if (Input.KeyboardKeyJustPressed(Keys.D7)) Sound.Play(Sounds.GattlingShoot); else if (Input.KeyboardKeyJustPressed(Keys.D8)) Sound.Play(Sounds.EMP); TextureFont.WriteText(2, 30, "Press 1-8 or A/B or left/right mouse buttons to play back "+ "sounds!"); }); } // TestPlaySounds() 用户界面 现在渲染用户界面和菜单纹理。上一章已经讨论了不少如何处理输入,用户界面和游戏画面的逻辑实现,所以只需添加所需文件(MainMenu.png,MouseCursor.dds和GameFont.png),看一下游戏的屏幕逻辑(见图11-2)。实现主菜单和其他游戏屏幕上无需花太多时间,它们与Rocket Commander非常类似,但是一些复杂的屏幕如Option和Misson Selection被移除,因为XnaShooter中不需要。 图11-2 您已经学习了XnaShooter的游戏界面,但整个游戏的逻辑将在本章最后讨论。你只有三个游戏屏幕,它们都很容易实现。主菜单显示4个按钮,将新的游戏屏幕添加到堆栈中,让您可以跳出新增的游戏屏幕返回主菜单。Highscore是Rocket Commander中Highscore的精简版,因为不支持网络,所以只显示本机的Highscore。最后是Credit屏幕,显示出几行文字并加上Back按钮。 快速浏览一下MainMenu的Run方法,该方法处理主菜单和进入其他屏幕的四个按钮: // Render background game.RenderMenuBackground(); // Show all buttons int buttonNum = 0; foreach (MenuButton button in menuButtons) // Don't render the back button if (button != MenuButton.Back) { if (game.RenderMenuButton(button, buttonLocations[buttonNum])) { if (button == MenuButton.Missions) game.AddGameScreen(new Mission()); else if (button == MenuButton.Highscore) game.AddGameScreen(new Highscores()); else if (button == MenuButton.Credits) game.AddGameScreen(new Credits()); else if (button == MenuButton.Exit) quit = true; } // if buttonNum++; if (buttonNum >= buttonLocations.Length) break; } // foreach if // Hotkeys, M=Mission, H=Highscores, C=Credits, Esc=Quit if (Input.KeyboardKeyJustPressed(Keys.M)) game.AddGameScreen(new Mission()); else if (Input.KeyboardKeyJustPressed(Keys.H)) game.AddGameScreen(new Highscores()); else if (Input.KeyboardKeyJustPressed(Keys.C)) game.AddGameScreen(new Credits()); else if (Input.KeyboardEscapeJustPressed) quit = true; 纹理 除了菜单纹理、鼠标纹理和字体纹理,你需要更多的纹理。首先是你在前一章就见过的HUD纹理和新的NumbersFont.png纹理,NumbersFont.png纹理让你在HUD顶部显示一些彩色的数字。还有许多效果纹理用在特效系统内,这将在本章后面被讨论到。很难解释哪个纹理用在游戏的哪一部分,请看看图11-3,简单解释了每个纹理的用途。 图11-3 所有纹理都必须添加到项目中,但内容导入器的设置都不尽相同。如鼠标,主菜单,字体纹理不应被压缩成DXT格式(使用DDS的文件),它们仍处于未压缩的32bpp (每个像素的位数)的形式,但其他材质如爆炸效果需要压缩使其变得更小。例如,BigExplosion效果由约30张大小为128×128纹理的纹理组成。没有压缩时一个爆炸效果就约有2 MB。 而通过DXT5压缩你可以降低到0.5MB,让您可以执行好几个爆炸效果,并仍节省了磁盘和显存空间。为了支持alpha通道,应使用DXT5压缩格式代替DXT1,虽然DXT1压缩得更小。 这个游戏约有3 MB的纹理,其中1 MB用于两个爆炸效果,另外1MB用于菜单。其余的用于视觉效果、HUD和字体。特效系统很复杂,通过粒子的相互作用,实现了很酷的爆炸效果。有时特效也整合了物理引擎,允许粒子,烟雾,爆炸与周围环境或自身发生交互。 对于XnaShooter,简单的特效系统就足够了。虽然特效很难编写,但很容易改变或增加新的特效到纹理。只要增加一个Add方法,并通过EffectManager类中TestEffects进行单元测试中。 3D模型 这个游戏使用了大量的三维模型(见图11-4)。我一开始只使用了一个飞船模型和一些特效,但不久后我就发现没有至少3到4种不同敌人,游戏将变得很无趣。这些敌人的行为方式在XNA Shooter中有很大的不同: OwnShip模型是你自己的飞船。允许你发射MG,Plasma,Gatling-Gun或火箭,您还能发射EMP炸弹杀死屏幕上的所有敌人。您的飞在游戏中是最快的,但如果屏幕中充满了敌人,这就帮不了你了,在敌人击落你之前你必须首先击落它们。 Corvette是最基本的敌人,它从左右两边发射MG,生命之不高,武器也不强。主要优点是如果你处在它的射击方向上会立即被击中。如果你没有及时消灭它们,屏幕中充满Corvette,你会不断失去生命值。 小型运输舰是运送道具的小飞船,有25%至50%可能载有一些有用的道具,特别是生命值,当你生命值低下时这是很有用的。击落它们还可能获得EMP炸弹。运输舰还携带其他道具,让您可以切换武器。运输舰不发射武器,但你应避免与它们相撞。小型运输舰的生命值比Corvette还低。 Firebird是一个非常强大的敌人,它们直接对你发射火球。它能计算出你现在的位置迫使你不断闪避火球。它的生命值比其他较小的飞船大一点。不要碰撞Firebirds,这样会极大地降低生命值。如果屏幕上有太多Firebirds,往往只有EMP炸弹可以帮您了。 Rocket-Frigate(火箭驱逐舰)是游戏中最大的飞船。本来我想在关尾制作一个Boss,但制作三维模型和实现游戏逻辑需要花费太多的时间,如果你对射击游戏真的感兴趣,添加Boss和更多关卡应该不是很难。Rocket-Frigate发射与你类似的火箭,但小很多,也不会造成很大的损害。除了重装甲和高生命值,这个敌人的主要优势是火箭具有追踪能力,它将一直跟踪你的飞船直到耗尽燃料。如果你操作熟练的话,对付一个Rocket-Frigate不难并能仍然击中它,但关尾时要对付多个Rocket-Frigate就难得多。请确保您有一个重型武器或EMP炸弹应付这种情况。 小行星是不是一个真正的敌人,它们只是在你周围漂浮阻止您的行动。它们不能射击,但与之碰撞会失去了大量的生命值。普通武器很难击毁它们,但如果有EMP炸弹就可以把它们全消灭。您可能注意到,我借用了Rocket Commander中的小行星模型。 图11-4 这些敌人对游戏是很重要的,但没有道具会失去很多乐趣,而没有背景景物,看起来会很乏味。道具能回复生命值,补充EMP炸弹或更改四个武器中的一个:MG,Plasma,Gatling-Gun,以及火箭发射器。 背景物体与游戏不发生互动,只是放在背景上并产生阴影。首先, LandscapeBackground.X模型被渲染并在关卡中重复出现。本章后面您将会学到创建过程和产生关卡的细节。LandscapeBackground.X模型渲染后再把建筑物和植物渲染在它上面。由于场景是一个山谷,中间有相同的高度,所以您可以方便地添加建筑物和植物。所有的物体都是随机添加和产生的。您还可以添加更多的物体,改变场景模型列表是很简单的,只需添加另一种模型,它会自动生成在地面上。 动画纹理 在Rocket Commander中您已经使用过动画纹理了,但是你没学过如何实现它们。首先基本你要有一组纹理,能以1/30秒的速度改变。在XNA Shooter有两组动画纹理实现了两个爆炸效果。你只需为每个爆炸效果加载30个纹理并处理它他们,因为你不止一次需要用到这个代码,所以应该抽象到一个新的类:AnimatedTevaxture (见图11-5 )。 图11-5 AnimatedTexture的构造函数与Texture类非常相似,但你仍要通过纹理文件名检查纹理。爆炸特效使用连续的纹理名称,如BigExplosion0001.dds,BigExplosion0002.dds, BigExplosion0003.dds等等。下面构造函数中的代码用来加载所有这些文件名进入内部xnaTextures列表。请注意,在初始版本的Rocket Commander代码(Managed DirectX)中加载DDS文件,但在XNA中应编译成.xnb文件,这是Xbox 360平台上唯一的载入纹理的方式(Windows平台上仍支持直接加载DDS文件)。 // Ok, now load all other animated textures List<XnaTexture> animatedTextures = new List<XnaTexture>(); animatedTextures.Add(internalXnaTexture); int texNumber = 2; while (File.Exists(filenameFirstPart + texNumber.ToString("0000")+".xnb")) { animatedTextures.Add(BaseGame.Content.Load<Texture2D>( filenameFirstPart + texNumber.ToString("0000"))); texNumber++; } // while (File.Exists) xnaTextures = animatedTextures.ToArray(); 在Select方法的帮助下您可以选择任何载入的纹理,此类的其他部分和Texture类完全一样。这意味着你可以选择纹理并将其显示在屏幕上,因为内部xnaTexture变量被分配了正确纹理,你也可以对纹理实行shader。您也可以直接调用GetAnimatedTexture访问任何动画纹理。 /// <summary> /// Select this animated texture as the current texture /// </summary> /// <param name="animationNumber">Number</param> public void Select(int animationNumber) { if (xnaTextures != null && xnaTextures.Length > 0) { // Select new animation number internalXnaTexture = xnaTextures[animationNumber % xnaTextures.Length]; } // if } // Select(num) Billboards 现在你有了XNA Shooter的所有内容,但仍然需要思考如何来显示这些内容。您没有任何代码去渲染场景、物体和新的特效。在Rocket Commander你只需显示爆炸这个唯一的特效。在您的新游戏中把所有特效直接在屏幕上看起来不是很有说服力。在3D场景中直接以多边形的形式显示特效有这样几个优点: 您不必为每个3D特效计算二维位置和大小。当使用三维多边形时他们和其他东西一样被转化到屏幕上。 借助于深度缓冲,能将特效显示在物体的前面和后面。这样一来飞船引擎后的照明和烟雾特效即使在飞船其他部分或其他三维物体在前面的情况下也能显示正常。 如果您将特效排序,可以通过alpha混合将特效叠加起来。这样,许多特效叠加能产生更好的3D效果。 为了能将3D特效直接显示屏幕上,通常使用Billboard这种技术。Billboard使用3D方形(两个三角形)显示纹理。有时特殊显卡的功能,如点精灵也可以使用。在任何情况下观察Billboard都能看见它们。而对于其他三维多边形,如果它们朝向错误的方向,你就不能看到它们或他们变得扭曲和变小(见图11-6 )。 图11-6 对于某些特效这种行为是好的,例如,一个三维爆炸环从正面看是正确的,但如果从90度角的两侧看,它几乎消失了。大多数特效从旁边看效果不好。爆炸,灯光效果,火焰和等离子球等等特效都是从正面抓取的,但从其他方向看也类似。例如,火球特效,从各个方向看都应是一个球体,不应该被扭曲,变小或消失。为了实现这一点你必须确保你总是能看到特效,即始终将特效多边形转到面向观察者的方向。Billboard类可以帮助你实现这个任务(见图11-7)。 图11-7 Billboard类中最重要的方法是Render,它将Billboard加入到Billboard列表,在每一帧结束调用RenderBillboards方法时会渲染这个列表。Render方法有6个重载方法,但您也可以调用RenderOnGround方法将Billboard渲染到xy平面上。Render中的一个重载方法还你让你指定右和上的向量。通过这种方式,您可以随意调整飞船爆炸的爆炸环,并还能添加其他爆炸特效。 在您查看Render方法之前现看一下Billboard类的单元测试,展示了如何使用这个类: Billboard.Render(plasma, new Vector3(-40.0f, 0.0f, 0.0f), 5.0f, BaseGame.TotalTimeMs * (float)Math.PI / 1000.0f, Color.White); Billboard.Render(fireball, new Vector3(-40.0f, +50.0f, 0.0f), 5.0f, 0, Color.White); Billboard.RenderOnGround(ring, new Vector3(-25.0f, 0.0f, -100.0f), 5.0f, 0, Color.White, vecGroundRight, vecGroundUp); // etc. // Render all billboards for this frame Billboard.RenderBillboards(); 在Render方法中vecRight和vecUp向量用于构建Billboard多边形。这些向量可以直接从目前使用的视矩阵中提取。借助于BaseGame类,很容易提取这些向量,通过CalcVectors辅助方法,这些操作会自动在RenderBillboards中完成。 /// <summary> /// Calc vectors for billboards, will create helper vectors for /// billboard rendering, should just be called every frame. /// </summary> public static void CalcVectors() { // Only use the inverse view matrix, world matrix is assumed to be // Idendity, simply grab the values out of the inverse view matrix. Matrix invViewMatrix = BaseGame.InverseViewMatrix; vecRight = new Vector3( invViewMatrix.M11, invViewMatrix.M12, invViewMatrix.M13); vecUp = new Vector3( invViewMatrix.M21, invViewMatrix.M22, invViewMatrix.M23); } // CalcVectors() 快速浏览一下Billboard类中的Render方法: /// <summary> /// Render 3D Billboard into scene. Used for 3D effects. /// This method does not support rotation (it is a bit faster). /// </summary> /// <param name="tex">Texture used for rendering</param> /// <param name="lightBlendMode">Blend mode for this effect</param> /// <param name="pos">Position in world space</param> /// <param name="size">Size in world coordinates</param> /// <param name="col">Color, usually white</param> public static void Render(XnaTexture tex, BlendMode lightBlendMode, Vector3 pos, float size, Color col) { // Invisible? if (col.A == 0) return; TextureBillboardList texBillboard = GetTextureBillboard(tex, lightBlendMode); Vector3 vec; int index = texBillboard.vertices.Count; vec = pos + ((-vecRight + vecUp) * size); texBillboard.vertices.Add( new VertexPositionColorTexture( vec, col, new Vector2(0.0f, 0.0f))); vec = pos + ((-vecRight - vecUp) * size); texBillboard.vertices.Add( new VertexPositionColorTexture( vec, col, new Vector2(0.0f, 1.0f))); vec = pos + ((vecRight - vecUp) * size); texBillboard.vertices.Add( new VertexPositionColorTexture( vec, col, new Vector2(1.0f, 1.0f))); vec = pos + ((vecRight + vecUp) * size); texBillboard.vertices.Add( new VertexPositionColorTexture( vec, col, new Vector2(1.0f, 0.0f))); texBillboard.indices.AddRange(new short[] { (short)(index+0), (short)(index+1), (short)(index+2), (short)(index+0), (short)(index+2), (short)(index+3), }); } // Render(tex, pos, size) 如你所见,构造了四个顶点并添加到顶点列表中。每个纹理和光线混合模式被整合到各自的TextureBillboardList中。组成屏幕四边形的两个多边形索引被添加到索引列表。TextureBillboardList中的顶点和索引缓冲区连同RenderBillboards方法中的贴图和shader一起被渲染。

创建XNA Shooter游戏——概述

clock 八月 23, 2010 17:01 by author alex
概览 本章我将引导你制作XNA Shooter游戏,一个小小的射击游戏,它真的很好玩,借助于很酷的特效、漂亮的3D模型、以建筑物和植物作为背景的游戏场景、实时阴影映射、很棒的音乐和音效,这个游戏的外观和感觉比通常的街机游戏更好。当我开始制作这个游戏时,我也没有想到能做出这么棒的游戏(你可能从游戏名就能猜出这点,这个名称一点也不伟大)。但当我加入阴影映射(这在最后一章的赛车游戏中也被用到)后,背景和3D建筑物看起来非常漂亮,类似于上一章的漂亮的UI界面和一些很酷的post-screen shaders(见第8章)对此功劳也不小。 本章让你跟随制作XNA Shooter的每一步。游戏的基本编码花了两至三天,只有一些较高级的类如EffectManager和阴影映射需要一些额外的调整。阴影映射不光难以实现,而且要让你花几天甚至几周的时间反复调试才能使它看起来好一些,并修正一些问题(通常总是有问题)。XNA Shooter完全基于第5章的Xna GraphicEngine和第8章的Rocket Commander。只有这样才能在这么短的时间内做出一个这么好的游戏。 创建三维模型无需花很多时间,因为大部分来自老项目已经有了。由于simple camera是固定的,它始终看着地面,所以像渲染天空盒和镜头眩光可以不被考虑。该相机还简化了阴影映射,因为你总是看到相同大小的3D区域。 游戏逻辑在四个类中被处理: Player类,在Rocket Commander已经使用过。这里更简单,因为你只需要保存当前时间,得分,生命值和武器类型。 Unit类处理所有的敌人单位,它更新敌人的位置和AI,并把他们渲染在屏幕上。 Projectile类处理所有你和敌人发射出的弹药。这包括等离子球,你的火箭,火球或敌人的火箭弹。即时武器如MG或Gatling-gun没有弹药,它们立即命中目标并在屏幕上直接显示效果。 最后,Item类用来处理在击落运输舰或火箭护卫舰后掉落道具,并允许玩家在飞过它们时捡起道具。道具包括生命值,电磁脉冲炸弹,或者只是四种武器中的一个。 游戏逻辑比Rocket Commander复杂一点,但当你研究了所有这些类后,你会发现Misson类其实很简单,当你把所有的东西都整合在一起时,游戏差不多也就完成了,只需再做些测试和修复错误,游戏就能运行得很好。

输入和用户界面——SpaceCamera类

clock 八月 21, 2010 09:23 by author alex
SpaceCamera类 SpaceCamera类比SimpleCamera类复杂得多,赛车游戏的ChaseCamera看起来倒简单点。主要的原因这是我写的第一个空间相机类,其中使用到的四元数的数学知识并不那么容易。该相机支持三种模式:菜单画面的相机,游戏中的相机,单元测试中的自由相机。Rocket Command游戏的SpaceCamera有点简单,因为四元数被删除了,只允许俯仰和偏航火箭,而火箭绕Z轴旋转被引擎自动处理,这使得飞行起来容易些,不容易迷失方向。这个变化的主要原因是为了使用Xbox 360手柄也能很好地控制火箭,难度比初版Rocket Commander大。 快速浏览一下SpaceCamera中的重要方法,它们处理所有输入并更新相机位置: /// <summary> /// Handle player input for the game. /// This is where all the input happens in the game. /// </summary> private void HandlePlayerInput() { if (Player.lifeTimeMs < Player.LifeTimeZoomAndAccelerateMs) { float speedPercentage = Player.lifeTimeMs / (float)Player.LifeTimeZoomAndAccelerateMs; // Use quadradric product for better speed up effect Player.SetStartingSpeed(speedPercentage * speedPercentage); // Always move forward Translate(Player.Speed * BaseGame.MoveFactorPerSecond * Player.MovementSpeedPerSecond, MoveDirections.Z); if (Player.gameTimeMs < 100) { yawRotation = 0; pitchRotation = 0; pos = Vector3.Zero; } // if } // if 前面的代码增加火箭速度,如果游戏刚刚开始,同时也重置旋转角度和位置。接下来,您检查游戏是否结束和处理特殊情况使之能移动。然后处理鼠标和键盘输入。这些是我初次实现SpaceCamera类的原始代码,更多代码在以后添加以支持更多的输入设备: #region Mouse/keyboard support if (Input.MouseXMovement != 0.0f || Input.MouseYMovement != 0.0f) { float xMovement = Input.MouseXMovement; float yMovement = Input.MouseYMovement; Rotate(RotationAxis.Yaw, -xMovement * rotationFactor); Rotate(RotationAxis.Pitch, -yMovement * rotationFactor); } // if (Mouse.left.Pressed) // Use asdw (qwerty keyboard), aoew (dvorak keyboard) or // cursor keys (all keyboards?) to move around. // Note: If you want to change any keys, use Settings! if (Input.Keyboard.IsKeyDown(moveForwardKey) || Input.Keyboard.IsKeyDown(Keys.Up) || Input.Keyboard.IsKeyDown(Keys.NumPad8)) { float oldPlayerSpeed = Player.Speed; Player.Speed += 0.75f * BaseGame.MoveFactorPerSecond; } // if if (Input.Keyboard.IsKeyDown(moveBackwardKey) || Input.Keyboard.IsKeyDown(Keys.Down) || Input.Keyboard.IsKeyDown(Keys.NumPad2)) { float oldPlayerSpeed = Player.Speed; Player.Speed -= 0.75f * BaseGame.MoveFactorPerSecond; } // if if (Player.speedItemTimeout > 0) { Player.speedItemTimeout -= BaseGame.ElapsedTimeThisFrameInMs; if (Player.speedItemTimeout < 0) { Player.speedItemTimeout = 0; // Reduce to max. possible speed if (Player.Speed > Player.MaxSpeedWithoutItem) Player.Speed = Player.MaxSpeedWithoutItem; } // if } // if // Adjust current speed by the current player speed. float moveFactor = Player.Speed * maxMoveFactor; float slideFactor = maxSlideFactor; // Always move forward Translate(+moveFactor, MoveDirections.Z); // Slide if (Input.Keyboard.IsKeyDown(moveLeftKey) || Input.Keyboard.IsKeyDown(Keys.Left) || Input.Keyboard.IsKeyDown(Keys.NumPad4)) { consumedAdditionalFuel = true; Translate(-slideFactor, MoveDirections.X); } // if if (Input.Keyboard.IsKeyDown(moveRightKey) || Input.Keyboard.IsKeyDown(Keys.Right) || Input.Keyboard.IsKeyDown(Keys.NumPad6)) { consumedAdditionalFuel = true; Translate(+slideFactor, MoveDirections.X); } // if // Up/down if (Input.Keyboard.IsKeyDown(Keys.F)) { Translate(+slideFactor, MoveDirections.Y); } // if if (Input.Keyboard.IsKeyDown(Keys.V)) { Translate(-slideFactor, MoveDirections.Y); } // if #endregion 为了支持Xbox 360下面的代码是在2006年初,这个游戏发布前的一天被添加的。实现Xinput很简单,我很高兴XNA在所有的输入类中都使用Xinput。把所有输入设备都放在一个命名空间中,这个主意很好,唯一的问题是Xbox 360运行时中没有鼠标类。就算这个类不支持,微软也应实现一个虚拟类,至少可以实现当鼠标可选时,用不着改变所有的输入代码。幸好,你以通过自己的Input类解决了这个问题。 下面是Rocket Commander中的X360控制代码: #region Input support for the XBox360 controller // 2006-03-09: Added Input support rotationFactor = 3.0f * BaseGame.MoveFactorPerSecond; // Change camera rotation when right thumb is used. if (Input.GamePad.ThumbSticks.Right.X != 0.0f || Input.GamePad.ThumbSticks.Right.Y != 0.0f) { float xMovement = Input.GamePad.ThumbSticks.Right.X; float yMovement = Input.GamePad.ThumbSticks.Right.Y; Rotate(RotationAxis.Yaw, -xMovement * rotationFactor); Rotate(RotationAxis.Pitch, yMovement * rotationFactor); } // if (Mouse.left.Pressed) // Use left thumb for moving around if (Input.GamePad.ThumbSticks.Left.Y != 0) { float oldPlayerSpeed = Player.Speed; Player.Speed += 0.75f * Input.GamePad.ThumbSticks.Left.Y * BaseGame.MoveFactorPerSecond; // Only decrease fuel if change happened if (oldPlayerSpeed != Player.Speed) consumedAdditionalFuel = true; } // if // Slide if (Input.GamePad.ThumbSticks.Left.X != 0) { consumedAdditionalFuel = true; Translate(slideFactor * Input.GamePad.ThumbSticks.Left.X * 2, MoveDirections.X); } // if #endregion } // HandlePlayerInput() 你可以看到代码使用了大量的辅助方法,像Rotate,Translate、Input类中频繁使用的属性和从BaseGame类获得的有用的数值,如MoveFactorPerSecond。 Translate方法沿着x , y ,或z轴移动目前的相机。 x轴是用来左右移动,在Rocket Commander中使用A和D键或方向键实现。 y轴用来上下移动而z轴用来前进后退。在Rocket Commander中z轴是最重要的,你沿着这根轴以极高的速度移动。 /// <summary> /// Translate into x, y or z axis with a specfic amount. /// </summary> /// <param name="amount">Amount</param> /// <param name="direction">Direction</param> private void Translate(float amount, MoveDirections direction) { Vector3 dir = direction == MoveDirections.X ? XAxis : direction == MoveDirections.Y ? YAxis : ZAxis; pos += dir * amount; } // Translate(amount, direction) 最后,Rotate旋转相机。Yaw是左右转动和Picth让你上下转动。原版本的Rocket Commander使用四元数,它还允许你翻滚火箭。在XNA版本的Rocket Commander中火箭自动调整,这样在Xbox 360上更容控制。 /// <summary> /// Rotate around pitch, roll or yaw axis. /// </summary> /// <param name="axis">Axis</param> /// <param name="angle">Angle</param> private void Rotate(RotationAxis axis, float angle) { if (axis == RotationAxis.Yaw) yawRotation -= angle; else pitchRotation -= angle; } // Rotate(axis, angle) 所有相机类都要通过单元测试进行测试。如果您尝试创建自己的相机类请务必对其进行测试,避免出错。

输入和用户界面——相机

clock 八月 21, 2010 09:21 by author alex
相机 处理输入和管理游戏画面是重要的,当你实现了所有的游戏屏幕后就会看到一个更专业更好的游戏。 对于游戏本身,尤其如果它是三维的,如果你想在游戏世界中自由走动,你需要一个相机类。第5章介绍的SimpleCamera类能在一个简单的3D环境中移动和变焦。还有更有趣和有用的相机类:Rocket Commander游戏中是SpaceCamera类,赛车游戏中是ChaseCamera类。Xna Shooter需要的相机很简单,你可以重用SimpleCamera类,移动观察目标的位置。 图 10-10比较了这本书中使用的相机类。这里讨论SpaceCamera类,下几章讨论ChaseCamera类。SimpleCamera从GameComponent类继承,你能很容易地添加新的相机类。 图 10-10

输入和用户界面——游戏屏幕

clock 八月 17, 2010 09:37 by author alex
游戏屏幕 在图10-2你已经看到了一个简单的主菜单的例子,但您的游戏包含的不仅仅是游戏屏幕和菜单。您通常还需要一个credit屏幕让你出名,有一个Option屏幕让用户轻松设置所有设置和更改屏幕分辨率,最好还有一个Help屏幕解释游戏的基本规则(见图10-3)。 图 10-3 大多数游戏屏使用一个特殊的背景纹理把大多数的信息显示在屏幕上。在任务选择画面,您可以选择四项任务中的其中一个,然后启动游戏。其他屏幕只是显示出一些信息和只有在Option屏幕上允许用户可以一些东西。所有的游戏画面在按下Back键后返回主菜单。 你使用游戏屏幕堆栈自动处理所有游戏屏幕上(见图10-4)。这可以让你使用更复杂的多层次的菜单系统,并可以随时返回以前的画面。如果你想返回到主菜单,只需移除堆栈中除最后一个以外的所有项。另一个我经常使用的技巧是在主菜单前添加另一个屏幕,当用户退出游戏时显示“购买这个游戏”屏幕。这个类本身只有几行代码,你只需在主类中添加一行额外的代码。大多数游戏屏幕类也非常简单。 图 10-4 为什么游戏屏幕堆栈很好?因为您需要做的就是从IgameScree接口继承得到到所有的游戏屏幕类(见图10-5),然后您可以使用下面的代码自动渲染和处理屏幕了。每个游戏屏幕类的Render方法会在推出此屏幕时返回true,这样将返回到前一个屏幕。当所有游戏屏幕都被移除则退出游戏。 Figure 10-5 // No more game screens? if (gameScreens.Count == 0) { // Then quit Exit(); return; } // if (gameScreens.Count) // Handle current screen if (gameScreens.Peek().Render()) { // Play sound for screen back Sound.Play(Sound.Sounds.ScreenBack); gameScreens.Pop(); } // if (gameScreens.Peek) 帮助屏幕 作为一个非常简单的例子,下面是GameScreens命名空间下的Help类的完整代码。其他游戏屏幕类也不复杂,除了Mission类,它要负责处理整个游戏。 /// <summary> /// Help /// </summary> class Help : IGameScreen { #region Properties /// <summary> /// Name of this game screen /// </summary> /// <returns>String</returns> public string Name { get { return "Help"; } // get } // Name #endregion #region Run /// <summary> /// Run game screen. Called each frame. /// </summary> /// <param name="game">Form for access to asteroid manager</param> public bool Run(RocketCommanderGame game) { // Render background game.RenderMenuBackground(); // Show helper screen texture game.helpScreenTexture.RenderOnScreen( new Rectangle(0, 174 * BaseGame.Height / 768, BaseGame.Width, 510 * BaseGame.Height / 768), new Rectangle(0, 0, 1024, 510)); if (game.RenderMenuButton(MenuButton.Back, new Point(1024 - 210, 768 - 140)) || Input.KeyboardEscapeJustPressed) return true; return true; } // Run(game) } // class Help 就是这样。看起来很简单,不是吗?如果你想了解得更多,请自己看一下游戏屏幕类。有些类包含单元测试表明行为方式和使用情况。 游戏中的用户界面 在这一节我要你跟随以下步骤制作XNA Shooter的游戏用户界面。用户界面不是很复杂,但你会遇到几个问题。以下内容被显示在这个游戏中: 任务时间:分,秒显示你玩了多久 当前分数:你射击,击落,收集物品和武器获得分数 最高分:努力成为排名第一 您目前的生命值:如果完全失去,则死 当前使用的武器和可以投掷的电磁脉冲炸弹 图10-6显示使用下列纹理显示这些内容: HudTop.png在屏幕上的最上面一栏,显示时间,分数和最高分 HudButtom.png在底部,显示生命值和当前武器 GameFont.png,您前几场游戏中已经使用过的游戏字体 NumbersFont.png,顶端部分的数字,看起来比默认字体更酷 图 10-6 一个新的类通过NumbersFont.png纹理去显示数字,归功于Texture类和RenderOnScreen方法,这不难。 NumbersFont.png纹理在NumbersFont类中处理,它与第4章的TextureFont类很像,但简单得多,因为你只需要11个矩形,0至9的10个数字和一个冒号显示时间。 private void RenderHud() { // Render top hud part hudTopTexture.RenderOnScreenRelative4To3(0, 0, hudTopTexture.GfxRectangle); // Time BaseGame.NumbersFont.WriteTime( BaseGame.XToRes(73), BaseGame.YToRes(8), (int)Player.gameTimeMs); // Score BaseGame.NumbersFont.WriteNumberCentered( BaseGame.XToRes(485), BaseGame.YToRes(8), Player.score); // Highscore BaseGame.NumbersFont.WriteNumberCentered( BaseGame.XToRes(920), BaseGame.YToRes(8), Highscores.TopHighscore); // Render bottom hud part Rectangle bottomHudGfxRect = new Rectangle(0, 24, 1024, 40); hudBottomTexture.RenderOnScreenRelative4To3(0, 768 - 40, bottomHudGfxRect); // Health Rectangle healthGfxRect = new Rectangle(50, 0, 361, 24); hudBottomTexture.RenderOnScreenRelative4To3(50, 768 - 31, new Rectangle(healthGfxRect.X, healthGfxRect.Y, (int)(healthGfxRect.Width * Player.health), healthGfxRect.Height)); // Weapon and Emps! Rectangle weaponMgGfxRect = new Rectangle(876, 0, 31, 24); Rectangle weaponGattlingGfxRect = new Rectangle(909, 0, 27, 24); Rectangle weaponPlasmaGfxRect = new Rectangle(939, 0, 33, 24); Rectangle weaponRocketsGfxRect = new Rectangle(975, 0, 24, 24); Rectangle weaponEmpGfxRect = new Rectangle(1001, 0, 23, 24); TextureFont.WriteText(BaseGame.XToRes(606), BaseGame.YToRes(768 - 20) - TextureFont.Height / 3, "Weapon: "); // Show weapon icon! Rectangle weaponRect = Player.currentWeapon == Player.WeaponTypes.MG ? weaponMgGfxRect : Player.currentWeapon == Player.WeaponTypes.Gattling ? weaponGattlingGfxRect : Player.currentWeapon == Player.WeaponTypes.Plasma ? weaponPlasmaGfxRect : weaponRocketsGfxRect; hudBottomTexture.RenderOnScreenRelative4To3( 715, 768 - 31, weaponRect); // And weapon name TextureFont.WriteText(BaseGame.XToRes(717+weaponRect.Width), BaseGame.YToRes(768 - 20) - TextureFont.Height / 3, Player.currentWeapon.ToString()); TextureFont.WriteText(BaseGame.XToRes(864), BaseGame.YToRes(768 - 20) - TextureFont.Height / 3, "EMPs: "); // Show emp icons if we have any for (int num = 0; num < Player.empBombs; num++) hudBottomTexture.RenderOnScreenRelative4To3( 938 + num * 23, 768 - 31, weaponEmpGfxRect); } // RenderHud() 这种解决办法在PC很好,但一旦你运行在Xbox 360上并在电视屏幕显示(在写RenderHud方法之前我就写了TestHud单元测试)上你会看到,HUD不是完全可见的,或者更糟,几乎看不到。 如果你从来没在一个需要连接到电视机的游戏平台上编写游戏,这可能是一个新的问题,因为在电脑显示器,您可以使用100%的可视面积,没有安全区域的概念。但对大多数电视屏幕,你看不到100%的屏幕,更多的是90%,这意味着约10%屏幕边界宽度和高度都是不可见的(见图10-7)。 Figure 10-7 你可能会问,为什么XNA不把所有东西都自动放到安全区域中去呢。因为这不是那么容易。电视机接收全部信号,并根据输入电缆和电视机的模式,你会看到不同的结果。下面是我已经遇到的情况: 通过VGA或DVI电缆连接的PC显示器,您可以看到100% 通过VGA电缆把Xbox 360连接到电脑显示器,您也能看到100%(如果分辨率不匹配则接近100%) Xbox 360连接到一个旧式SCART显示器,约92%可见 Xbox 360连接到我的新戴尔24寸监视器(HDTV):约93%~95%可见(取决于分辨率) 一些旧电视机(据XNA文档和网上的信息)约80%~90%,但我从来没有见过80%的情况,这可能是最坏的情况 如你所见,在Xbox 360上不是正好是90%,不过不用担心。在不同的情况下结果可有很大差异,XNA框架和你的游戏也没法帮你自动检查。只有一点可以肯定的是,在电脑上100%的屏幕像素都是可见,这也就是为什么许多电脑游戏使用的边界显示用户界面元素和其他信息。如果你看一下Xbox 360游戏,你会发现,它们往往只有一个较简单的界面,也不在屏幕边缘放置信息。 你不能只是将HUD显示在90%的安全区域,因为如果使用者能看到更多区域就会发现画面有错误,因为90%区域外没有画面,如果用户看到得更少仍有以前同样的问题。在继续工作之前你应该停在这里,并重新考虑这一问题。如图10-6那样显示用户界面元素对Xbox 360游戏机来说不是个好主意。最好的解决办法是改变用户界面的图片,把他们放在安全区域里。图10-8显示了如何改变HUD的图片使之能适用两种显示情况:在PC上,您把他们在屏幕边界上;在Xbox 360,放在内部92%的区域 (在90%的安全区域仍可见,但如果只有85%或更少就很难看到,,但我从来没有见过这种最坏的情况,大多数电视机都能达到90%~95%)。 图 10-8 图10-9显示了最XNA Shooter游戏的最终屏幕布局。请查看Misson类中的RenderHud方法获取更多细节。它更好地适应了宽屏分辨率,在Xbox 360看上去很好,即使较小分辨率的电脑上看起来也不错。为了测试菜单和游戏屏幕可运行游戏项目示例,这个例子仍基于开始于第5章的XnaGraphicEngine项目,但你有一些新的升级过的类。 图 10-9 诀窍 以我的经验,在Xbox 360开发XNA游戏你必须记住几件事。在PC上这不是个大问题,但需要花额外一点时间让它们正常工作。一个主要的问题是,如果你只在PC编写游戏而最后才在Xbox 360上测试,很多事情可能出差错(.NET Compact Framework上性能不佳、UI元素屏幕边界上导致在一些电视上不可见或对Xbox 360手柄支持差,而手柄是Xbox 360游戏机最主要的输入设备等等)。如果你有兴趣,请到我的博客仔细阅读更多关于这些问题的讨论。 测试,测试还是测试。这是最重要的技巧。不断编写单元测试在PC和Xbox 360平台上。我同时打开PC和Xbox 360两个项目(两者使用相同的文件,但我只开发PC解决方案,而Xbox 360的解决方案只用于部署和测试)。我的几乎所有类都有单元测试,类编写完我就不断地测试。 不要使用foreach循环,尤其是在渲染循环中。这可能听起来有点疯狂,因为这在PC平台上没关系,现在的CPU速度足够快,能够在每帧处理创建和删除成千上万的物体,大多数游戏甚至都不需要。但在compact framework中,你每开始一个foreach循环都将创建一个新的enumerator实例,会占用很多内存,不久之后就会有很多抛弃的对象,而这些对象系统必须自己收集。这可能需要一些时间并极大地降低游戏性能。在PC版本上可能会超过200帧,但您的Xbox 360版本会停留在30~40帧左右。避免每帧都建立新的数据,并避免foreach循环(只需用正常循环取代,往往只需一条额外的代码)可以让性能提高100%或更多。 在Arena Wars(我开发的第一个.NET游戏),我就从来没有在游戏中创建任何数据。所有的对象在任务之前就被创造好,重用他们(这没什么大不了,因为游戏规则不允许无限多的单元,你可以通过dead元创建新单元)。这种做法可以减少垃圾收集性能冲突,这种冲突在慢的电脑NET 1.1情况下可能发生。在以后的项目,我无需关心太多关于建立新物体的问题,我只是用简单的方式编码,因为单元测试能帮你迅速的解决问题,但在.NET Compact Framework情况中未必是最好的。对于XNA Shooter和XNA Racer,我确定大部分的游戏数据在每一关卡开始就被建立,在游戏中不会变化。这使得写代码容易得多,可以让重构变得更加自由,以改善整体设计和代码的执行速度。我建议你采用“clean code”,并在开始时就考虑优化,重要代码要更好的得到优化,不然在项目中很难被管理。归功于NET 2.0中垃圾收集的改进,重用现有对象和异常处理比以前好得多,而且我们今天还拥有快得多的计算机。 电视上的可见区域可能会产生问题。只要搜索一下Xbox 360游戏画面的截图,你会发现,GUI (图形用户界面)大多数PC游戏有很大不同。PC游戏界面往往在屏幕边界显示提示,小按钮,以及其他不那么重要的事情。如果在Xbox 360游戏机这样做UI元素会被截除。对于XNA Shooter我不得不返工所有的用户界面元素,因为他们不适合电视屏幕,把它们放在一个栏(如Windows任务栏)中是不切合实际,因为在PC和电视屏幕上有很大不同。所以我把所有的UI元素放在浮动栏中,将根据屏幕自动调整。 重要的是让重要的用户界面元素在此90%(或93%,如果你想更接近边缘)矩形之内。这意味着在1920×1080分辨率下只使用90%(1728×945),或在屏幕边缘5%左右的地方(x坐标:96,y坐标:54)绘制用户界面元素。这些像素的位置取决于屏幕分辨率,要在你的游戏主类中计算好,并利用它们绘制用户界面。 有关.NET Compact Framework、如何在Xbox 360上更好地使用XNA框架的更多知识,请阅读.NET Compact Framework小组写的以下文章,网址在http://blogs.msdn.com/netcfteam/archive/2006/12/22/managed-code-performance-on-xbox-360-for-the-xna-framework-1-0.aspx。

输入和用户界面——Input类

clock 八月 17, 2010 09:29 by author alex
Input类 Input类有许多轻松获得所有最常用的键盘,鼠标和手柄键状态(见图10-1 )的属性。此类还提供了一些辅助方法来管理键盘输入,文字输入,但你可能不会在第一个游戏中使用的Input类的所有代码,但当你的游戏发展并使用更多的用户界面代码时,就会觉得它们的用处了。此类最重要的方法是Update方法,该方法每一帧都被调用(在BaseGame类的Update方法中被自动调用)。 图10-1 请注意,Xbox 360中不存在鼠标功能,当您编译为Xbox 360时无法获得鼠标状态(没有这个类)。所以,你必须注释掉所有鼠标代码才能使游戏运行在Xbox 360。这并不轻松,尤其是在项目中有数以百计的鼠标调用时。一个较好的解决办法是完全隐藏鼠标状态(把它变成私有),只通过Input类的属性获取鼠标参数。正如你在图10-1中看得的那样,如果你要检查是否有另一个键或按钮被按下,你可以直接操作手柄和键盘状态,但对于鼠标,您只能通过Input类的属性。现在,如果运行在PC上,会返回所有属性,但在Xbox 360上,只返回false或虚拟的鼠标位置,您可以像在PC上一样设置并获得鼠标位置,但用户不能直接控制。 现在,同样的代码可用于两个平台。XNA以同样方式的98%的代码实现了目标,行为你的引擎可达到100%的。 例如,Xbox 360 手柄的A键或键盘的方向键使用非常频繁。使用以下的代码,通过向上/向下键键盘浏览菜单条目非常容易。此外也支持手柄向上/向下控制光标,通过辅助属性使用左摇杆控制菜单更容易些。如果你每次都编写所有状态检查去判断一个键或一个按钮是否被按下或没按下,您可能一会儿就会发疯了。 // Handle GamePad input, and also allow keyboard input if (Input.GamePadUpJustPressed || Input.KeyboardUpJustPressed) { Sound.Play(Sound.Sounds.Highlight); selectedButton = (selectedButton + NumberOfButtons - 1) % NumberOfButtons; } // if (BaseGame.GamePadLeftNowPressed) else if (Input.GamePadDownJustPressed || Input.KeyboardDownJustPressed) { Sound.Play(Sound.Sounds.Highlight); selectedButton = (selectedButton + 1) % NumberOfButtons; } // else if GamePadUpJustPressed 方法代码如下: /// <summary> /// Game pad up just pressed /// </summary> /// <returns>Bool</returns> public static bool GamePadUpJustPressed { get { return (gamePadState.DPad.Up == ButtonState.Pressed && gamePadStateLastFrame.DPad.Up == ButtonState.Released) || (gamePadState.ThumbSticks.Left.Y > 0.75f && gamePadStateLastFrame.ThumbSticks.Left.Y < 0.75f); } // get } // GamePadUpJustPressed 该代码使用目前的手柄状态(这是每帧开始时分配的)和上一帧的手柄状态以按键状态是否已经发生变化。如你所见,如果您每一次重复编写此代码去检查手柄按钮是否被按下,你会发疯的。复制与粘贴第一次或第二次不错,但如果你重复使用相同的代码,最好还是写一个辅助方法或属性,让它为您执行这项工作。以后当你写更多的用户界面代码时,它将更容易检查所有输入状态,处理许多不同的输入设备。 Input类中的Update方法 Update方法并没有什么复杂,但它是Input类中最重要的部分,因为上一帧的所有状态被复制,然后你获得每一个键盘,鼠标和手柄输入的新状态。这里有一些额外的代码处理相对鼠标移动。鼠标的相对移动在XNA中不支持,不同于DirectX中的DirectInput类,它能让您获得相对鼠标位置去替代绝对鼠标位置,比如用相对坐标(-3.5,7.0)替代绝对坐标(350,802)。 /// <summary> /// Update, called from BaseGame.Update(). /// Will catch all new states for keyboard, mouse and the gamepad. /// </summary> internal static void Update() { #if XBOX360 // No mouse support on the XBox360 yet :( mouseDetected = false; #else // Handle mouse input variables mouseStateLastFrame = mouseState; mouseState = Microsoft.Xna.Framework.Input.Mouse.GetState(); // Update mouseXMovement and mouseYMovement lastMouseXMovement += mouseState.X - mouseStateLastFrame.X; lastMouseYMovement += mouseState.Y - mouseStateLastFrame.Y; mouseXMovement = lastMouseXMovement / 2.0f; mouseYMovement = lastMouseYMovement / 2.0f; lastMouseXMovement -= lastMouseXMovement / 2.0f; lastMouseYMovement -= lastMouseYMovement / 2.0f; if (MouseLeftButtonPressed == false) startDraggingPos = MousePos; mouseWheelDelta = mouseState.ScrollWheelValue - mouseWheelValue; mouseWheelValue = mouseState.ScrollWheelValue; // Check if mouse was moved this frame if it is not detected yet. // This allows us to ignore the mouse even when it is captured // on a windows machine if just the gamepad or keyboard is used. if (mouseDetected == false) mouseDetected = mouseState.X != mouseStateLastFrame.X || mouseState.Y != mouseStateLastFrame.Y || mouseState.LeftButton != mouseStateLastFrame.LeftButton; #endif // Handle keyboard input keysPressedLastFrame = new List<Keys>(keyboardState.GetPressedKeys()); keyboardState = Microsoft.Xna.Framework.Input.Keyboard.GetState(); // And finally catch the XBox Controller input (only use 1 player) gamePadStateLastFrame = gamePadState; gamePadState = Microsoft.Xna.Framework.Input.GamePad.GetState( PlayerIndex.One); // Handle rumbling if (leftRumble > 0 || rightRumble > 0) { if (leftRumble > 0) leftRumble -= 1.5f * BaseGame.MoveFactorPerSecond; if (rightRumble > 0) rightRumble -= 1.5f * BaseGame.MoveFactorPerSecond; Microsoft.Xna.Framework.Input.GamePad.SetVibration( PlayerIndex.One, leftRumble, rightRumble); } // if (leftRumble) } // Update() 您可能会注意到Update方法最后的手柄振动代码,可以使用下列方法让手柄振动。,如果下一帧不调用GamePadRumble,Update方法将自动减少振动强度。您还可以为小事件使用较弱的振动效果。启用振动效果玩Shooter会更有乐趣。 /// <summary> /// Game pad rumble /// </summary> /// <param name="setLeftRumble">Set left rumble</param> /// <param name="setRightRumble">Set right rumble</param> public static void GamePadRumble( float setLeftRumble, float setRightRumble) { leftRumble = setLeftRumble; rightRumble = setRightRumble; } // GamePadRumble(setLeftRumble, setRightRumble) 鼠标矩形 Input类也有一些方法帮助您检测鼠标是否处在一个用户界面元素(例如一个菜单按钮)上。使用下列方法来检查鼠标悬停在用户界面元素上。如果您进入这个矩形该方法也将自动播放声音。 /// <summary> /// Mouse in box /// </summary> /// <param name="rect">Rectangle</param> /// <returns>Bool</returns> public static bool MouseInBox(Rectangle rect) { #if XBOX360 return false; #else bool ret = mouseState.X >= rect.X && mouseState.Y >= rect.Y && mouseState.X < rect.Right && mouseState.Y < rect.Bottom; bool lastRet = mouseStateLastFrame.X >= rect.X && mouseStateLastFrame.Y >= rect.Y && mouseStateLastFrame.X < rect.Right && mouseStateLastFrame.Y < rect.Bottom; // Highlight happened? if (ret && lastRet == false) Sound.Play(Sound.Sounds.Highlight); return ret; #endif } // MouseInBox(rect) 例如:你可以使用这个代码让您选择主菜单元素,如图10-2。如我以前所说,尽量保持简单。菜单应显示游戏的所有重要部分。背景纹理也应该选用一张漂亮的图片——由你的图形艺术家制作(如果你有的话)。 图 10-2 要能工作你可以使用下面的代码片断: //Render background game.RenderMenuBackground(); //Show all buttons int buttonNum =0; MenuButton [] menuButtons ==new MenuButton [] { MenuButton.Missions, MenuButton.Highscore, MenuButton.Credits, MenuButton.Exit, MenuButton.Back, }; foreach (MenuButton button in menuButtons) // Don’t render the back button if (button !=MenuButton.Back) { if (game.RenderMenuButton(button,buttonLocations [buttonNum ])) { if (button ==MenuButton.Missions) game.AddGameScreen(new Mission()); else if (button ==MenuButton.Highscore) game.AddGameScreen(new Highscores()); else if (button ==MenuButton.Credits) game.AddGameScreen(new Credits()); else if (button ==MenuButton.Exit) quit =true; }//if buttonNum++; if (buttonNum >=buttonLocations.Length) break; }//if //Hotkeys,M=Mission,H=Highscores,C=Credits,Esc=Quit if (Input.KeyboardKeyJustPressed(Keys.M)) game.AddGameScreen(new Mission()); else if (Input.KeyboardKeyJustPressed(Keys.H)) game.AddGameScreen(new Highscores()); else if (Input.KeyboardKeyJustPressed(Keys.C)) game.AddGameScreen(new Credits()); else if (Input.KeyboardEscapeJustPressed) quit = true; //If pressing XBox controller up/down change selection if (Input.GamePadDownJustPressed) { xInputMenuSelection = (xInputMenuSelection + 1) % buttonLocations.Length; SelectMenuItemForXInput(); }//if (Input.GamePad) else if (Input.GamePadUpJustPressed) { if (xInputMenuSelection <= 0) xInputMenuSelection = buttonLocations.Length; xInputMenuSelection -; SelectMenuItemForXInput(); }//if (Input.GamePad) 如果使用更多的控制元素,比如复选框,处理滚动条或编辑框,代码会更加复杂,在你重复地复制和粘贴相同的代码前,你应该引进UI类去处理所有的用户界面代码。如果你只需要使用一个编辑框,您也可以直接把代码写入Option类中,就像在Rocket Commander中做的那样,但是如果你使用多次,代码应该被重构。处理文本框在XNA中更复杂,因为你必须实现自己的键盘输入方法。 在XNA中输入文字 您可能还注意到,在Input类中没有keyboardStateLastFrame变量,因为与MouseState和GamePadState不同,下一帧您无法再次使用KeyboardState结构,数据不再有效。之所以发生这种情况是因为KeyboardState为所有键值使用内部List,会被下一帧再次使用,当您调用Keyboard.GetState时会覆盖所有的键盘状态。如果要修正这个问题你需维持自己的键值列表,每次从上一帧的键值克隆。使用通用List类构造函数会自动为你复制所有内容。 使用此list与直接使用键盘状态的工作方式相同。看一下Input类中的KeyboardSpaceJustPressed的属性。 /// <summary> /// Keyboard space just pressed? /// </summary> /// <returns>Bool</returns> public static bool KeyboardSpaceJustPressed { get { return keyboardState.IsKeyDown(Keys.Space) && keysPressedLastFrame.Contains(Keys.Space) == false; } // get } // KeyboardSpaceJustPressed Input类的其余部分与我刚才提到的键盘输入文字的方法很类似,为文本框获取键盘输入与一个普通的Windows文本框类似,下面是单元测试: /// <summary> /// Test keyboard chat input /// </summary> public static void TestKeyboardChatInput() { // Better version with help of Input.HandleKeyboardInput! string chatText = ""; TestGame.Start("TestKeyboardChatInput", null, delegate { TextureFont.WriteText(100, 100, "Your chat text: " + chatText + // Add blinking | ((int)(BaseGame.TotalTime / 0.35f) % 2 == 0 ? "|" : "")); Input.HandleKeyboardInput(ref chatText); }, null); } // TestKeyboardChatInput() 该单元测试显示chatText字符串和后面的一个闪烁|标志。然后HandleKeyboardInput方法用来让使用者输入新的文字。看看HandleKeyboardInput方法: /// <<summary> /// Handle keyboard input helper method to catch keyboard input /// for an input text. Only used to enter the player name in the /// game. /// </summary> /// <param name="inputText">Input text</param> public static void HandleKeyboardInput(ref string inputText) { // Is a shift key pressed (we have to check both, left and right) bool isShiftPressed = keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift); // Go through all pressed keys foreach (Keys pressedKey in keyboardState.GetPressedKeys()) // Only process if it was not pressed last frame if (keysPressedLastFrame.Contains(pressedKey) == false) { // No special key? if (IsSpecialKey(pressedKey) == false && // Max. allow 32 chars inputText.Length < 32) { // Then add the letter to our inputText. // Check also the shift state! inputText += KeyToChar(pressedKey, isShiftPressed); } // if (IsSpecialKey) else if (pressedKey == Keys.Back && inputText.Length > 0) { // Remove 1 character at end inputText = inputText.Substring(0, inputText.Length - 1); } // else if } // foreach if (WasKeyPressedLastFrame) } // HandleKeyboardInput(inputText) 该方法使用了Input类中的两个辅助方法。首先,IsSpecialKey用来判断键入的是文字还是如F1,方向键,Delete,Enter之类的其他键,这些其他键不会直接加入到文字中去。然后,你处理Backspace键的特殊情况。您还可以扩展代码来处理Enter或Delete键。 下一个问题是,你不是总能将键值添加到您的输入文字中。首先无论用户是否按下Shift键,按“A”键总是在文本框中添加A。而一些特殊键 如+,-,{等等,将显示为“Plus”,“Minus”,“OemOpenBrackets”。为解决这个问题,如果XNA会为您提供键的真正意义应该不错,但它没有,你必须通过Input类中的KeyToChar辅助方法自己实现: /// <summary> /// Keys to char helper conversion method. /// Note: If the keys are mapped other than on a default QWERTY /// keyboard, this method will not work properly. Most keyboards /// will return the same for A-Z and 0-9, but the special keys /// might be different. Sorry, no easy way to fix this with XNA ... /// For a game with chat (windows) you should implement the /// Windows events for catching keyboard input, which are much better! /// </summary> /// <param name="key">Keys</param> /// <returns>Char</returns> public static char KeyToChar(Keys key, bool shiftPressed) { // If key will not be found, just return space char ret = ' '; int keyNum = (int)key; if (keyNum >= (int)Keys.A && keyNum <= (int)Keys.Z) { if (shiftPressed) ret = key.ToString()[0]; else ret = key.ToString().ToLower()[0]; } // if (keyNum) else if (keyNum >= (int)Keys.D0 && keyNum <= (int)Keys.D9 && shiftPressed == false) { ret = (char)((int)'0' + (keyNum - Keys.D0)); } // else if else if (key == Keys.D1 && shiftPressed) ret = '!'; else if (key == Keys.D2 && shiftPressed) ret = '@'; [etc. about 20 more special key checks] // Return result return ret; } // KeyToChar(key) 通过这种方法,HandleKeyboardInput方法现在以您所期望的方式工作了,单元测试可以进行了。Input类中有更多的单元测试,研究一下可以了解更多有用的的属性和方法。

输入和用户界面——概览

clock 八月 3, 2010 14:56 by author alex
输入和用户界面 概览 本章将深入理解已经使用了很多次的Input类,接着讨论学习如何在XNA Shooter中实现一个良好的图形用户界面(GUI)。借助Input命名空间,在XNA中处理用户输入是非常简单的,通过以下三行代码您可以轻松地获得鼠标,键盘和游戏手柄的当前状态: mouseState = Mouse.GetState(); keyboardState = Keyboard.GetState(); gamePadState = GamePad.GetState(PlayerIndex.One); 如果现在您还没有加入Input命名空间,只需将鼠标移至Mouse,Keyboard,或GamePad文字上并按下Ctrl ,就会自动将下面一行添加到using directives中: using Microsoft.Xna.Framework.Input; 现在您可以使用这些states检查手柄是否被连接,空格键是否被按下,或鼠标的当前位置在哪等。这正是你在第一和第二章写第一个XNA游戏时所写的代码。 但随着游戏变得更加复杂,你会频繁地重复相同的代码。与引擎中的类做法类似,你通常不添加更多的功能,而是使现有功能更易使用。例如,Texture类用来储存的XNA纹理,纹理大小,是否Alpha,还包含了许多有用的方法,使纹理在SpriteHelper类的帮助下直接显示在屏幕上。 所有这一切都能在标准的XNA框架中实现,但你不得不写更多的代码。如果您只想把一个单一的纹理显示在屏幕上,那么写这么多辅助类是毫无意义的,但如果你要编写许多项目和使用数以百计的纹理,您将会感谢之前自己写的所有辅助类。只有简化了纹理,shader,以及材质才可能通过几行代码就实现复杂的图形代码,你可以在单元测试的图形类中看到这些代码。 同样的逻辑也适用于Input类。好处并不明显,但以后当你编写新的UI (用户界面)代码实现自定义按钮或菜单控制时,你会发现通过许多辅助类(包括Iuput类)做起来更容易。 你已经知道如何使用此Input类,因为你已经使用过很多次了。为了你下一个游戏XNA Shoot,你还将编写UI类和为更高级游戏显示的菜单逻辑。思路是重用现有Rocket Commander游戏的game screen类。有些类需要移除,您可能需要一写新的类,但总体程序逻辑是非常相同的。 游戏在Misson类显示用户界面(弹药,生命值,得分等等)。对于更复杂的游戏,如下一部分的赛车游戏,游戏逻辑和用户界面会分离成更多的类。你应该总是取保游戏代码尽可能简单,但不要太简单。如果你想要做的只是把“Hello World” 显示在屏幕上,编写30个辅助类是毫无意义的。 用户界面,特别是菜单界面是一个很大的议题。曾经有一段时间几乎每个游戏都有非常复杂的菜单动画甚至3D图形,但它很难使用。不仅对你,而且对用户而言,应尽量使事情保持简单。你将在本篇末学习一个很好的菜单设计。

通过XACT添加声音——总结

clock 八月 3, 2010 14:55 by author alex
总结 在这一章中你学习了XNA中的声音效果。无论你喜欢与否必须使用XACT。XACT让你能在Xbox 360和Windows平台上播放音效和音乐。 下面是你在本章学到的要点: XACT是在Xbox 360上使用XNA播放声音的唯一方法。 XACT工具开始可能比较烦人,但一旦你习惯了就能享受到新的很酷的功能。例如,比起自己编写代码实现淡入和淡出,在XACT中设置更加方便。 XACT不支持MP3,Ogg,或ACC等其他自定义的压缩格式。只有两种格式支持,Windows平台上的ADPCM和Xbox 360平台上的XMA。 XMA对Xbox 360来说是一个很好的压缩格式,但ADPCM并不真正有用。如果你有时间而且想在Windows上使用更小的声音文件,可以从DirectX SDK中实现DirectAudioVideoPlayBack到您的引擎或使用其他外部声音引擎。 对于XMA我推荐75%质量,ADPCM则推荐每个块128或256个采样。Windows版本的游戏可能会比360版本大一倍,因为ADPCM编码压缩率比较糟糕。 让Sound类易于使用并在任何地方都能使用。 使用Sound枚举快速获取和播放可用的sound cue。 为你经常调用的代码编写辅助类。 在XACT中设置自定义cue变量是首先进行的,然后导入到XNA代码中。SetVariable方法能修改cue实例变量。 下一章讨论了Input类处理键盘,鼠标和Xbox 360的手柄,它已经在图形引擎中了,但所有的方法还没有详细讨论。另外,您将了解菜单系统和用户界面代码。例如,当你每次移过或点击控件时用户界面代码会自动播放声音。对于自定义控件你必须自己完成所有的检查和处理。您还可以了解XNA Shooter游戏,它将在第11章中完成。

通过XACT添加声音——挑战:建立您的XACT项目

clock 八月 3, 2010 14:54 by author alex
挑战:建立您的XACT项目 本章的挑战很简单,只需建立一个类似于本章示例的XACT项目。重要的是,使用XACT你会觉得很顺手,否则你将花费大量的时间重写不必要的代码,或者您要在XACT中搜索您之前未见过的隐藏在某处的功能。 此外,为XACT项目编写自己的Sound类并首先进行单元测试。然后实现构造函数和载入XACT项目、wave和sound bank。最后,实现Play方法使您的单元测试正常工作。您可以查看本章的XnaShoterSoundProject和TestSounds单元测试。 现在您已是一个经验丰富的XACT使用者了。XACT的更多详情您还可以访问XACT的官方论坛,其中包含了许多技巧,如果遇到问题,您还可以寻求XACT专家的帮助: http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=323 在您尝试使用X3Daudio时,你也可以查看是否有Rocket Commander的更新版本,并学习它是如何做的。

通过XACT添加声音——Sound类

clock 八月 3, 2010 14:53 by author alex
  Sound类 创建Sound类真的很简单。你已经在第一章看到了一些Sound类,sound cue回放非常简单。为了使使用起来更简单,您可以定义一个枚举包含所有的声音cue名称。通过这种方式可以确保每一个sound cue确实存在,而且你不会输错名称。这样做的另一个优点是智能感知,它将会显示隐藏在XACT项目中所有可用的sound cue。花费两分钟时间写这些枚举是值得的。在Sound类中的Play方法帮助下,现在播放sound cue很容易。 /// <summary> /// Play /// </summary> /// <param name="soundName">Sound name</param> public static void Play(string soundName) { if (soundBank == null) return; try { soundBank.PlayCue(soundName); } // try catch (Exception ex) { Log.Write("Playing sound " + soundName + " failed: " + ex.ToString()); } // catch } // Play(soundName) /// <summary> /// Play /// </summary> /// <param name="sound">Sound</param> public static void Play(Sounds sound) { Play(sound.ToString()); } // Play(sound) 您也可以通过让字符串私有使得只允许声音枚举。如果sound bank无法在构造函数中初始化(例如,如果该文件丢失)此方法将立即返回而您无法播放任何声音。错误会被记录到日志文件中。接着你调用PlayCue播放并忘记sound cue。如果失败你只写入日志文件,因为你不想因为某些声音文件的丢失打断正常的程序逻辑。 正如你之前所见,您也可以使用GetCue方法记住一个sound cue,然后做多个事情。如果您想自己停止cue而不是等待声音放完,这样做是很有用的。作为一个例子,下面的代码用来停止播放音乐: someSoundCue = soundBank.GetCue("SomeSound"); someSoundCue.Play(); ... someSoundCue.Stop(AudioStopOptions.Immediate); 本书中的游戏的Sound类中的大部分代码是相同的,下面看看不同和特殊功能(见图9-14)。 图 9-14 首先你会发现在Sound枚举中有不同的sound cue名称。Rocket Commander和赛车游戏使用自定义的picth cue。XNA Shooeter不使用任何特殊设置,你只需在游戏开始时播放音乐,然后播放所有音效。 为了使输入播放声音的代码更舒适,创建了几个辅助方法。例如,PlayExplosionSound只是取代了下面的代码使其能更快并更容易编写: Sound.Play(Sound.Sounds.Explosion); 对于音乐播放添加了两个额外的方法:StartMusic和StopMusic,它们也保存了music cue的音轨。 每个sound类中还有一个单元测试用来检查回放声音是否正常,并检查音量是否正确。下面是Rocket Commander声音类中的单元测试: /// <summary> /// Test play sounds /// </summary> public static void TestPlaySounds() { TestGame.Start( delegate { if (Input.MouseLeftButtonJustPressed || Input.GamePadAJustPressed) Sound.Play(Sounds.Bomb); else if (Input.MouseRightButtonJustPressed || Input.GamePadBJustPressed) Sound.Play(Sounds.Click); else if (Input.KeyboardKeyJustPressed(Keys.D1)) Sound.Play(Sounds.GameMusic); else if (Input.KeyboardKeyJustPressed(Keys.D2)) Sound.Play(Sounds.MenuMusic); else if (Input.KeyboardKeyJustPressed(Keys.D3)) Sound.Play(Sounds.Explosion); else if (Input.KeyboardKeyJustPressed(Keys.D4)) Sound.Play(Sounds.Fuel); else if (Input.KeyboardKeyJustPressed(Keys.D5)) Sound.Play(Sounds.Victory); else if (Input.KeyboardKeyJustPressed(Keys.D6)) Sound.Play(Sounds.Defeat); else if (Input.KeyboardKeyJustPressed(Keys.D7)) { Sound.PlayRocketMotorSound(0.75f); Sound.ChangeRocketMotorPitchEffect(0.5f); } // else if else if (Input.KeyboardKeyJustPressed(Keys.D8)) Sound.StopRocketMotorSound(); TextureFont.WriteText(2, 30, "Press 1-8 or A/B or left/right "+ "mouse buttons to play back sounds!"); }); } // TestPlaySounds() 最后,Update方法被BaseGame类中的Update方法自动调用。Update方法只为您确保在XACT中所有参数,循环,时间值的更新。 火箭发动机声音 快速看一下Rocket Commander中的火箭发动机声音。前面你已经看到完成这个工作所需的代码,下面是如何整合在一起并配合Rocket Commander游戏引擎,一开始这是用DirectSound写的,但在XACT中工作得也很好: /// <summary> /// Play rocket motor sound /// </summary> public static void PlayRocketMotorSound() { // Get new cue everytime this is called, else we get Xact throwing // this: The method or function called cannot be used in the manner // requested. rocketMotorSound = soundBank.GetCue(Sounds.RocketMotor.ToString()); // Plays the sound looped, set in XACT rocketMotorSound.Play(); // Make motor category a little silent motorCategory.SetVolume(0.86f); } // PlayRocketMotorSound(volume) /// <summary> /// Change rocket motor pitch effect /// </summary> /// <param name="pitchFactor">Pitch factor</param> public static void ChangeRocketMotorPitchEffect(float pitchFactor) { rocketMotorSound.SetVariable("Pitch", 55 * MathHelper.Clamp(pitchFactor - 1, -1, 1)); } // ChangeRocketMotorPitchEffect(pitchFactor) /// <summary> /// Stop rocket motor sound /// </summary> public static void StopRocketMotorSound() { rocketMotorSound.Stop(AudioStopOptions.Immediate); } // StopRocketMotorSound() 游戏开始后你只需调用PlayRocketMotorSound方法就能播放火箭的声音,然后通过ChangeRocketMotorPitchEffect方法修改它: // Adjust rocket playback frequency to flying speed Sound.ChangeRocketMotorPitchEffect(0.66f + speed * 0.9f); 如果你失败或任务结束,则调用StopRocketMotorSound方法停止播放火箭的声音。 赛车游戏中的声音逻辑工作方法类似,但更加复杂,因为你有13个不同的发动机声音。看一下赛车游戏的sound类了解更多细节。 Whoosh,那是什么呀? 初版本的Rocket Commander使用自己的3D音效计算公式并使用DirectSound的音量和和panning属性。在XNA中不能改变panning。所有声音必须以在XACT中创建和设置的方式播放。对于3D声音,你应该使用真实的3D声音代码,在XNA电测版本中我写了一些播放3D声音的代码,并实现了一个在三维世界中的3D监听对象。 这个3D监听对象在XNA中被移除,现在您不能使用3D音效。希望将来会做出改变,如果3D可以被实现,我一定会更新这本书中的代码,但现在你只能播放单声道的声音,没有立体声或环绕声。 如果你可以使用X3DAudio和X3DAudioListener (这些类可能有不同的名称,重要的是您可以设置和播放3D音频效果,并且在3D listener类的帮助下确定玩家的位置),在设置了所有重要的cue后,下面的代码用于播放3D声音: // Setup 3D listener for 3D sound Sound.Listener = new X3DAudioListener( zAxis, yAxis, cameraPosition, movementVector); 现在,为每个声音设置发射器并播放3D音频实例。目前还没有C#代码可以做到这一点,如果你对此感到困惑,你可以看看下面的链接去了解X3Daudio类是如何使用C++的。在C#中做法类似,但希望能更容易编写。 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directx9_c/ audio_xact_overview_x3daudio.asp 菜单声音 这里有一些关于菜单提示声音的小技巧: 使用如PlayMenuHighlight或PlayMenuClick之类的辅助方法播放声音,而不是使用Sound枚举。 如果你想用不同的音量播放菜单声音,例如主要按钮声音响而小控件声音轻,可以在XACT中建立两个sound cue而不是编写自己的自定义代码,这样做工作量很大而且没有必要。 尝试写辅助方法判断鼠标是否移到了控件上并重用此代码,这样,你就无需每次都自己处理播放点击或高亮的声音。 请确认您还支持Xbox 360控制器和键盘。编写一个只支持鼠标的游戏在Xbox 360毫无意义,因为Xbox 360不支持鼠标。 当移动到菜单项时播放高亮的声音,用户按下手柄键或按下space或Enter键选择菜单时播放点击的声音。

通过XACT添加声音——使用XACT

clock 八月 3, 2010 14:51 by author alex
使用XACT XACT曾经是一个只提供给Xbox 360开发者的工具,你不能轻易地得到它。您必须是一个经验证的Xbox 360开发者,并获得XDK(Xbox开发工具包)。即使你是一个专业的游戏开发者并从业多年,这也不是一件容易的事情。 但是,现在已经发生了变化。一年前微软决定在DirectX SDK包含XACT,虽然在PC游戏上用得不多,但这对XNA游戏开发是很重要的,因为这是在Xbox 360上使用XNA播放声音的唯一办法。您不必安装DirectX SDK就能使用XACT工具,它已经包含在XNA GSE安装文件中了。您可以在开始-所有程序-Microsoft XNA Game Studio Express-Tools-XACT Auditioning Utility中找到它。如果您使用另一个版本的XACT请确保它支持XNA,例如,2006年8月的DirectX SDK已包含了XACT 2.0,但它将内容文件保存为4.0版本。而2006年10月的DirectX SDK和XNA框架使用4.1版本,与之前的版本不兼容。如果您加载旧的XACT项目可能会出现非常奇怪的错误,如文件丢失或提示InvalidOperationException错误。未来的版本也可能有所不同,为了确保您的XACT项目能被支持,请检查.xap文件的版本(.xap只是一个简单的文本文件,你可以用任何文本编辑器打开它)。 .xap文件的结构很简单,看起来类似xml文件。但版本号有点混乱,你要确保是13版(即XACT工具的4.1版,这可在关于对话框中看到,见图9-4)。像11或12的老版本(在老的DirectX软件开发工具包中)不被支持。这个问题会发生在打开一个早期XNA测试版本(2006年8月至10月之间)时,测试版本仍然使用旧的XACT工具。 图 9-4 Signature = XACT2; Version = 13; Options { } Global Settings { [...] 如果您只想在Visual Studio的XACT项目内改变一个简单的值,如音量大小,只需在Visual Studio的文本编辑器打开.xap文件,找到储存音量的位置并改变它就可以了。 对于游戏开发者来说最好有专人负责音效和音乐。让他学会使用XACT工具,你就不用关心声音,音量和格式的问题了。但不幸的是,大多数小团队没有专人负责声音,如果你要全部自己实现,那么除了要制作图形及其他游戏内容文件,您也将制作自己的声音效果。一开始可以利用现成的音效或免费提供的声音。可选用类似的音乐,可以从你的MP3库中找,最终版本您需要自己的音乐,当然,你不能只用别人的音乐。一些音乐文件都是免费使用的,但大多数音乐都不是免费的。获取音效文件比较容易,对于一些简单的音效你也可以本书中的音效文件。如果你没有音效文件或没人可以帮你,我建议你在互联网上寻找免费音效的网站。 为了让事情变得更有趣,你将要为这本书第三部分最后的一个游戏——Xna Shooter创建sound项目,Xna Shooter是一个简单的射击游戏,有很酷的三维图形、声音效果并支持键盘,鼠标和Xbox 360手柄(将在下一章详细讨论)。 创建项目 当您打开XACT工具,您将看到一个新的空项目(见图9-5)。它不包含任何声音文件,wave或sound bank,但创建新项目时某些变量和设置已经创建了: 项目树目录,它总是包含相同的根节点。你不能改变它。 两种默认类别:Default和Music,它可以将声音文件标记为音效或音乐,这两种文件行为是不同的。 一个全局变量,SpeedOfSound,长度始终是343.5和4个cue实例变量,多普勒频移,Cue实例数量,定位角。所有这些变量只对3D声音来说才是重要的,但在XNA中3D音效不被支持。当你编写代码来播放音效时会详细讨论这个议题。 一个全局effect路径,现在没有任何东西。 其他根节点是空的。你必须自己创建子节点,可以单击工具栏或右键点击节点并选择New。例如,“New Wave Bank”创建一个新的wave bank。 图 9-5 要将wav文件导入到新的XACT项目中你必须首先建立一个wave bank,它储存了所有wav文件,然后加载并被你的游戏使用。不仅仅是加载wav文件,还要做大量的其他工作,XACT能使音效艺术家在设置各种参数时有更多的灵活性。当你必须自己实现XACT项目时帮助不大,但至少XACT能让您的项目可以更好地组织声音文件,你能知道在哪可以改变声音的参数,而不需要进入游戏代码。使用XACT的缺点是,你不能动态加载声音文件,所有内容的文件(如编译模式)必须在启动游戏时被创建和加载,当游戏运行时就不能再添加东西了。一开始这让我很恼火,因为我喜欢在编写和调试游戏时添加动态的东西,但一旦习惯了,这也很不错。现在,我在项目早些时候就创建所有声音文件,并只使用单元测试中已有的声音。以后sound项目会被不断地修改。 创建wave bank 要创建一个wave bank,您可以右击wave bank,并选择“New Wave Bank”,或者您也可以使用工具栏创建一个wave bank(第八个选项,橙白相间的图标,见图9-5)。Wave bank仅储存使用到的所有声音文件。你可以有不同的wave bank,举例来说,如果游戏中有多个关卡,你只需要载入当前关卡的wave bank,通过这种方式可以分离波形文件并优化您的游戏。这本书的所有项目和我曾经制作过的其他项目只包含一个wave bank,相对于分割音乐和音效以支持流媒体,我认为拥有多个wave bank不是很重要,你可能永远都用不到。 在创建好自己的wave bank后,您就可以拖放wav文件到wave bank或者使用右键菜单选择“Insert Wave Files”。图9-6显示了Xna Shooter游戏将要使用到的所有声音文件。 图 9-6 如您所见,所有的声音文件仍然未被使用,用红色的斜体显示。一旦您将这些波形文件添加到sound,它们会变成绿色。但在此之前,你应该确保所有的文件都有正确的设置。默认情况下没有选择压缩,除了音乐文件大多数声音文件都相当小,所有的音效文件都小于1 MB。音乐文件占用了高达36 MB的磁盘空间,长3.5分钟,并以前面讨论过的格式(16位44KHz的立体声的PCM)储存。 如果你想压缩的音乐文件,请在wave属性面板中单击它并选择您想要的压缩预置。目前显示为<none>,但如果你点击下拉列表,其中并没有其他选项。你必须首先创建一个压缩预置然后才可以使用它。XACT工作原理与其他预置类似。例如,如果你想为音效创建一个自定义变量,你必须首先在变量部分创建它,然后才可以在sound bank中选择并使用它。 音乐数据压缩 要创建一个新的压缩预置可以单击左边树目录中的Compression Presets并选择“New Compression Preset”(见图9-7)。现在输入一个名称,你只是想创建一个音乐文件压缩架构。Notes部分不会在XACT工具的外部使用,只是给你一个简单的注释。其他面板没有Notes部分和不需要的可选参数。您可能会在第一次使用XACT时感到困惑。一开始我不喜欢它,但过了一段时间后就会习惯,你必须记住,这是在Xbox 360和XNA播放声音的唯一办法。如果您创建一个只能在Windows运行的游戏,您仍然可以使用DirectSound或其它声音引擎,也许这样使用起来更简单或有其他优点。最后,用每块128个样本(默认值)设置Windows压缩(只能用于ADPCM)。更高的值会使文件更小,但质量会下降。每块32个样本是最高的ADPCM质量,但文件会增大约30%。 图 9-7 Xbox 360上您只能选择XMA,但压缩率好很多。你可能会问,当你只能选择一钟压缩时却有这么多的选项。好吧,希望将来能获得更多的压缩格式。XMA对Xbox 360来说很好,但有时它产生的文件有点大,但它听起来音质很好。但在Windows平台上ADPCM无法使用。它音质不如MP3文件,而且大小是MP3的4-5倍。若支持WMA或MP3一定会很棒。 不幸的是,XACT不会即时显示所产生的XMA文件的大小。你必须首先编译该项目,才会告诉您XMA格式的波形文件被压缩了多少。要编译该项目,您可以点击工具栏的最后一个按钮或按下F7。默认情况下,该项目将编译为Windows平台,您仍然没法看到XMA压缩的结果。你必须在View菜单中切换到Xbox 360 output然后编译XACT项目,这样wave bank将显示Xbox 360上的文件大小(PC压缩率始终会显示,它们总是同一比率,27%是默认的ADPCM的设置)。 下表显示了XnaShooterGameMusic.wav文件的压缩比(未压缩时有36 MB)。 Open table as spreadsheet 压缩 文件大小 压缩率 Uncompressed 36 MB 100% MP3 with 192kbit 4.8 MB 13% MP3 with 128kbit 3.1 MB 8.5% AccPlus with 48kbit, similar to MP3 192kbit 1.2 MB 3% AccPlus with 24kbit, similar to MP3 96kbit 0.6 MB 1.6% OGG with 160kbit 3.8 MB 11% XACT ADPCM 32 samples per block 12.5 MB 34% XACT ADPCM 128 samples per block (default) 9.9 MB 27% XACT ADPCM 512 samples per block 9.3 MB 26% XACT XMA 100% Quality 14.3 MB 40% XACT XMA 90% Quality 8.9 MB 25% XACT XMA 75% Quality 5.5 MB 15% XACT XMA 60% Quality (default) 4.7 MB 13% XACT XMA 30% Quality 3.5 MB 10% XACT XMA 10% Quality 2.9 MB 8% 低质量的XMA文件在大小上与MP3相当,但是质量更糟。我不会推荐使用非常低压缩设置的XMA文件。使用50%或更高,文件的大小仍然接近于60%(默认设置)和75%(我最喜爱的设置),因为对于高质量设置来说,文件仍保持非常小(低于15%,类似于192kbit的MP3,质量也差不多)。如果你的游戏没有音乐或只有一个音轨,以上这一切将不会带给你任何问题,但如果你有超过10个的音乐文件,对压缩设置您要三思而后行,并探寻各种可能性。 在对音乐文件进行压缩后,文件大小在Xbox目录中约为5-6 MB,在Win目录中约为10 MB(见图9-8)。Rocket Commander游戏XACT项目大小几乎是这个的两倍多,因为它使用了两个音乐文件。在本书的以后几个游戏中使用更多的音乐文件,文件大小也会增长得更多。 图 9-8 Sound Banks 虽然您可以编译该项目加载到您的XNA游戏中,但还不能使用任何声音,因为XACT项目中只包含波形文件。但是要播放声音必须首先建立在一个sound bank。同样你也可以建立多个sound bank,但通常不需要。 让所有的wav文件工作最简单的办法是将它们拖动到您的sound bank中。但别急,sound bank在哪?您还没有。你必须首先创建它。为此,您可以点击sound bank节点或工具栏上的第二个橙色图标(在wave bank旁边)。新的sound bank命名为“Sound Bank”,如果你只有一个wave bank和一个sound bank,只需保持默认的名字。如果您有多个wave bank或sound bank,最好起一个适当的名称,以便您可以更容易得找到。 从wave bank拖动所有波形文件到sound bank,你应该将窗口位置设置成可以完整地看到这两个窗口。Sound bank窗口由两部分组成:顶端的sound名称和底部的cue名称。在wave bank中选择所有文件并将它们拖动wound bank的sound名称部分(见图9-9)。在这里每一个声音可以得到更多的设置,如改变音量,频率(音高)或采用其他变量或效果。您可以多次添加相同的波形文件并给予不同声音名称。对于更复杂的游戏你甚至可以在多个音轨中组合几个波形文件并彼此混合,然后用这个“组合”代替单个波形文件。要添加多个音轨只需将另一个文件拖动到顶部,XACT会自动创建第二个音轨。但本章您不会用到这个功能。 图 9-9 当你在sound bank编辑声音时应确保音乐(XnaShooterGameMusic)使用Music类别而不是Default(您可以在左侧的sound属性面板中编辑)。因为我想在背景中播放多个音乐,所以通常还使声音更加柔和而不使用默认值。在您的游戏可以使用music类别控制音量。此外,你可能想确保在同一时间只播放一个音乐文件(或者如果你有多个音乐文件,而且你也不想停止并重新启动它们,这时你也可以调用StartMusic方法多次)。 为了优化音乐播放,你应该把所有要播放的音乐文件放在一个sound bank入口内(例如,MenuMusic和GameMusic,可参见Rocket Commander例子)。当音乐entry被选择时点击sound bank窗口右侧的“Play Wave”。这里您可以强制循环播放,一些声音已经是循环播放了,但对于音乐文件最好能确保它也是循环的。 最后你要确保同一时间只播放一个音乐文件,如果另一个音乐文件有一个很好的淡出效果也不错。幸运的是,XACT可以为你做这一切。您现在可以为每个音乐文件设置淡入/出持续时间,或者更聪明地设置整个音乐分类(见图9-10)。默认LimitInstances设定为false,一旦你在属性面板中将其设置为true就能激活它。如果您尝试在一个音乐项目开始后再播放其他音乐项目将引发异常,因为BehaviorAtMax设定为FailToPlay。将它改变为Replace可以修正错误。其余的设置见图9-10。 图 9-10 Cue变量 如果你想,你可以再次编译项目,但您的XNA代码还没能处理任何声音效果。XNA只允许访问一个sound cue,它必须首先被创建。这一步很简单,只要选取所有sound的名称并将它们拖动到sound bank窗口的sue部分。名称会自动取自文件名,但你可以重命名。我通常将音乐文件重命名,这样能使在XNA中处理起来轻松点。 在XNA Shooter中使用的另一个很酷的功能是,你可以将多个声音效果用在一个单一的sound cue上。这个功能可用于爆炸效果。您有三个爆炸波形文件,但为了简化问题,您只想告诉XACT播放一个爆炸声音,它能随机播放其中一个。为了做到这一点,您要从cue列表中删除Explosion2和Explosion3的sound cue,现在将Explosion1 cue重命名为Explosion。最后,将Explosion2和Explosion3拖动到Explosion sound cue上。您现在应该可以看到如图9-11所示。提示:为了让编辑变得更轻松,使sound bank窗口最大化。有时当滚动条出现或消失时,你可能会拖动到错误的位置或错误地重复项目(我就一直发生这种错误)。 图 9-11 您可能还注意到我还将游戏音乐sound cue改名为GameMusic,这样能更容易地把它写入XNA代码中。另外,请给其他sound cue也起一个好记的名称。 再次编译该项目后就可以在XNA中使用XACT项目并存取所有的sound cue了。请注意,您无需总在XACT中编译XACT项目,当你将.xap项目添加游戏项目中时XNA会自动为你编译。或者您也可以只添加编译过的文件并在XNA中自己加载他们。无论采取何种方式,.xap文件无疑是XNA最顺手的解决方案。但是在使用XACT时,我仍建议按F7键编译该项目来测试一个新的单元测试直到它工作正常。 其他效果 XACT允许设定其他效果和声音参数,这对3D音效特别有用,你可以为声音效果设置各种参数。结合一些小的定制编程你能做的事情非常强大,如根据玩家的位置改变音效,并自动检测三维几何或空间的声场。如果在水下或玩家被手雷击中,您的引擎也可以扭曲音效。变化的可能性是无穷无尽的。 对于这个小游戏你不需要任何特殊的效果,但作为一个例子我会向你展示如何创建一个自定义变量,使您可以在运行时改变频率(音高)。在XACT直接改变很简单,但在XNA中你没法直接改变这些设置。你必须首先使cue变量公有,并确保声音能使用这个自定义变量。 这不仅是为了好玩。对于Rocket Commander和本书最后一部分的游戏你需要这个效果扭曲火箭发动机的声音或根据目前赛车的速度改变赛车发动机的声音。 要创建新的cue实例变量只需点击“Cue Instance”并从弹出菜单中选择“New Cue Instance Variable”。您可以输入您想要的任何东西和使用任何你喜欢的值,他们还没有绑定任何东西。在这里你调用新的变量pitch并从-100变化到100,其余属性保持不变(见图9-12)。 图 9-12 接下来,您必须确保XACT对这些变量做了些有用的事。您可以设定它并将它指派到任意一个声音效果,但在储存这些值之前它什么也不能做。这里您可以使用RPC预置(Runtime Parameter Control)现存的参数,但现在的选择非常有限(你只能改变音量,音高混响电平)。也许将来会有更多的选择。例如,在Rocket Commander XNA中设置panning是不可能的,这只能用在Managed DirectX版本中。如果XACT支持3D音效,那么你可以很容易进行真正的3D声音计算,但不幸的3D listener在初版XNA测试版本中被取消。在XACT中设置3D音效参数是可能的,你可以在本地XACT项目中使用他们,而且XNA并尚不支持(如果您像我以前那样实现过类似的东西,那么在XNA中它们都不会工作,这一点也不好玩)。 总之,要改变音效的音高,您可以轻松地使用新的RPC预置。只要点击RPC预置,并添加一个新的RPC预设,调用PRC音高或只保留默认名称。选择Sound:Picth作为参数,并使用新创建的Picth变量。现在,你可以拖动下面图表中左,右点使Picth 变量中的-12等于-100和12等于100(见图9-13)。 图 9-13 在预设能用来分配所有的声音前你还要使用这个新的变量。为了做到这一点只需将一个声音文件从sound bank(不使用wave或cue,他们无法正常工作)拖动到PRC预置中。 下面的代码可以用来启动一个sound cue,并动态地改变音高。如果你只需设置一个静态音调值则不用这么麻烦,只需在XACT中建立一个新的cue并设定不同的音高值。但对于发动机的声音你必须动态地改变原有的音高,因此你必须经历这些麻烦。 rocketCue = soundBank.GetCue("RocketMotor"); rocketCue.Play(); // Set the global volume for this category motorsCategory.SetVolume(MathHelper.Clamp(volume, 0, 1)); // Set pitch from -55 to +55 (little more than half the max values) rocketCue.SetVariable("Pitch", 55 * MathHelper.Clamp(pitch, -1, 1)); 你马上会看到Sound类的其余部分更容易,您可能会在XACT中花大部分时间,然后在花几分钟时间在编写Sound类上。 在前面的例子你也看到了改变音量的一种方法。直接改变cue的音量默认不支持,但你可以以您刚刚创建音调变量的同样方式创建一个音量的cue变量。

通过XACT添加声音——没有DirectSound可用

clock 八月 3, 2010 14:35 by author alex
没有DirectSound可用 我估计你以前可能用过DirectSound,当你在XNA中编写第一个游戏时您会发现sound命名空间与DirectSound有很大不同。快速浏览一下Managed DirectX中的DirectSound是如何使用的。首先,你必须初始化DirectSound设备: soundDevice = new DirectSound.Device(); soundDevice.SetCooperativeLevel(form, CooperativeLevel.Priority); 现在,您可以载入所有的声音,确保您有多个声音缓冲器和处理文件不存在的情况有点复杂,这将导致DirectSound提示InvalidOperationException错误。在Managed DirectX Rocket Commander代码中,下列代码是用来确保所有的声音文件存在,以确保您可以多次播放声音: if (File.Exists(Directories.SoundsDirectory + "\\" + filename) == false) { Log.Write("Sample " + filename + " was not found!"); loaded = false; return; } // if (File.Exists) BufferDescription bufferDesc = new BufferDescription(); bufferDesc.StaticBuffer = true; bufferDesc.ControlPan = true; bufferDesc.ControlVolume = true; bufferDesc.ControlFrequency = true; buffer = new SecondaryBuffer[setNumberOfSimultaneousSounds]; channelVolume = new float[setNumberOfSimultaneousSounds]; channelPanning = new float[setNumberOfSimultaneousSounds]; buffer[0] = new SecondaryBuffer( Directories.SoundsDirectory + "\\" + filename, bufferDesc, Sound.Device); defaultFrequency = buffer[0].Frequency; for (int i = 1; i < buffer.Length; i++) buffer[i] = buffer[0]; try { for (int i = 1; i < buffer.Length; i++) buffer[i] = buffer[0].Clone(Sound.Device); } // try catch { } // catch 最后,代码处理播放声音。借助于Sample类(ControlPan,ControlVolume,ControlFrequency等)的构造函数中的代码您可以设定每个声音缓冲通道的音量和panning。因为Rocket Commander中的Sample类代码很长,所以这里没有显示用来管理所有声音缓冲器和每个声音的属性(音量,panning,频率)的额外代码,如果您有兴趣可以去看原始代码。 // Make sure to start at position 0 buffer[currentBufferNum].SetCurrentPosition(0); // And play the sound buffer[currentBufferNum].Play(0, BufferPlayFlags.Default); 在XNA中你不再需要Sample类,你可以扩展Sound类以支持你需要的一切,包括MusicManager,Sample类和Rocket Commander中的Sound命名空间的其他东西。 由于XACT的结构(这将在下面讨论),你必须自己加载XACT项目、wave和sound banks。但不必担心,代码非常简单。 audioEngine = new AudioEngine("YourSoundProject.xgs"); waveBank = new WaveBank(audioEngine, "Wave Bank.xwb"); soundBank = new SoundBank(audioEngine, "Sound Bank.xsb"); 就是这些,你要做的只是调用它: soundBank.PlayCue(soundName); 看起来很容易,不是吗?您可能会要求DirectSound中的额外功能。它们没有丢失,但在XNA中处理声音的思路不一样。声音文件已不再完全由自定义代码管理,作为替代,XACT声音项目包含了所有预定义的声音参数。你无需自己修改每个声音的音量或音调,你可以在XACT工具中处理这一切。这对确保所有的声音有相同的音量,并确保声音不是太大是很有用的。你可以指定某些声音只被允许播放一次(如背景音乐),为开火或爆炸播放相同声音的多个实例。您将在本章后面学到更多的相关知识。 处理.wav文件 DirectSound和XACT都要求声音文件必须是.wav格式。如果您有其他格式的声音文件则必须首先进行转换。例如,MP3文件不被支持,你不能在DirectSound播放它们或简单地把它们拖动到您的XACT项目中。你必须首先转换为.wav文件。另请注意,在XACT中某些特定的.wav文件不被支持,当你添加声音文件到wave bank时将获得一个警告讯息告知格式不支持。 确保一个音乐文件的格式是正确的,并在XACT中以最佳途径压缩不是很容易(见图9-1)。 图 9-1 您要将声音文件都转换成16位44千赫立体声的PCM wav文件以确保可以压缩它们。如果音乐只有单声道,你还必须将它保存为立体声,如果您有8-bit的PCM文件,那就不要尝试去使用它们。一定要转换为16bit和立体声,否则XACT不会允许你正确地压缩声音文件。对于8bit文件你会得到如图9-2所示的错误讯息,而单声道文件压缩得不好。压缩时16bit立体声文件会损失一些音质,但选择正确的XMA和ADPCM压缩级别会让这种损失降到最小。 图 9-2 请注意,XACT并不真正支持音乐文件,它更擅长处理音效文件。在PC平台上,如果你有MP3音乐文件,那么使用ADPCM编码可能没有多大意义,而且已经有一些代码可以播放它们。但在Xbox 360平台上,您目前别无选择,只能使用XMA压缩和XACT。 音效文件也可以被压缩,但我建议您尝试使用高压缩比。对于PC游戏使用高度压缩的声音文件是非常少见的,通常使用低音质的文件(22KHz单声道),因为这样载入更快,使用MP3文件是不切实际的,因为这样做将使CPU在每次播放声音时解压缩或者在游戏开始时解压缩,这可能需要占用一段时间。在大多数情况下,增加了一些额外的MB到您的游戏不会影响很大,但如果你想和我一样确保游戏尽可能小而不丧失任何功能或任何质量下降,你仍将尝试压缩所有文件直到一切足够小但仍听起来不错。 我还压缩本书中所有游戏使用到的所有纹理,使用不同的格式以找出一个最佳的格式。例如,菜单和图形界面使用PNG格式,因为它以无损方式储存点阵图,但对模型纹理或场景纹理来说png文件太大。在这种情况下使用DDS格式,这也有利于节省显存,这对Xbox 360来说是很重要的。不要浪费你的主内存,磁盘空间或显存——它们在游戏机平台上是宝贵的。 例如,Rocket Commander游戏使用了约15个声音文件(见图9-3),使用了压缩使得小了大约1MB。 图 9-3 基本知识和文件格式讲完了,下面您将在XACT中创建首个声音项目了。

通过XACT添加声音——概览

clock 八月 3, 2010 14:31 by author alex
通过XACT添加声音 概览 本章将更深入的讨论XACT。XACT是微软跨平台音频创作工具(Microsoft’s Cross-Platform Audio Creation Tool)的简称,它可以让您为Windows和Xbox 360平台创建音频项目。事实上,它也是在XNA中播放声音的唯一方法。这对Windows平台并不那么重要,因为你可以插入您想要的任何其他声音引擎(如DirectSound,OpenAL,FMod等等)。但在Xbox 360上并不容易,因为你只能使用XNA框架,它不能使用任何其他框架播放声音。您只能使用XACT的原因是在XNA中不能使用任何非托管代码,出于同样的原因您也不能使用任何其他的图形或输入引擎,只能用XNA提供的。. 但是,XACT不是一件坏事,它能确保在Windows和Xbox 360平台使用相同的声音内容文件和播放代码。例如,Xbox 360仅支持XMA压缩,而PC使用其他压缩格式,如MP3, Ogg,ADPCM和其他自定义的压缩格式,但XACT只支持ADPCM。IP语音应用通常使用专有的声音压缩格式,因为人类的声音比音乐更容易被压缩,例如,音效文件非常短小,但作为一个游戏开发者通常想要尽可能高品质的声音效果,因此,音效文件往往是未压缩的或使用无损压缩。而对于音乐来说,它占用了大量的空间。如果您的游戏是在DVD上或者您有足够的硬盘空间可以浪费,你可以存储未压缩的wav文件。对于XNA游戏来说这并不好,因为你不能访问DVD,浪费几百MB的空间在音乐上并不是一个好主意。 在过去的DirectX或托管DirectX应用程序中,你最有可能使用DirectSound播放声音或使用其他如OpenAL、Fmod之类的框架。即使使用这些框架,您仍然需要做大量的自定义代码,你必须判断哪些声音格式是最适合你的,你将自己处理声音的压缩。此外,您必须自己实现播放音乐的方式。例如,DirectSound对音乐来说不是很有用,它只是支持wav文件,如果你想播放MP3或OGG音乐文件,您必须使用如DirectShow、Windows MCI、ActiveX播放代码之类的其他框架。听起来要做大量的工作,但大部分游戏中播放声音的实际代码是非常简单的。如果您要使用复杂的3D音效计算或支持环绕立体声会复杂点,但它仍比开发3D引擎容易。

XNA3.0初步——安装XNA Game Studio 3.0

clock 七月 30, 2010 14:39 by author alex
节选自 http://shiba.hpe.sh.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4,其版权归作者所有 [更多...]

管理Shader——总结

clock 七月 30, 2010 10:32 by author alex
总结 在这一章中我确信您理解了shader的工作原理,包括为何要声明顶点格式,如何通过shader处理顶点数据等。如果您以前有过shader的编程经验,这一章可能不是最令人兴奋的,不过也许你可以学到一些你以前不知道的细节。 下列步骤说明了在xna中如何处理三维数据: 从Model类(或自己定义的顶点缓冲器)获得三维几何数据。 请确认您的shader和xna代码使用相同的顶点格式。 调用shader technique和执行每一个pass渲染您的三维数据。 Vertex Shader处理输入数据并通过vertexoutput结构传递给Pixel Shader,将世界坐标转换到屏幕的正确位置。 Pixel Shader最终将每个像素显示在屏幕上,注意尽量确保Pixel Shader又快又小。 在下一章你要学习normal mapping(法线映射) shader的效果,为第8章建立您自己的游戏做好准备,在第8章你还会学习post screen shader效果。

管理shader——挑战:编写Diffuse per pixel shader

clock 七月 30, 2010 10:32 by author alex
挑战:编写Diffuse per pixel shader 作为一个小的挑战以更熟悉shader,你可以编写一个简单的Diffuseperpixel shader,与 specularper pixel shader有相同的工作原理,只不过消除了镜面反光。 Diffuseperpixel通常用在没有光泽的材料上,为了能够切换specularperpixel和diffuseperpixel,你应该在simpleshader.fx文件里建立两种technique。 加上以下代码后,您就可以使用空格键动态地在specularperpixel和diffuseperpixel之间切换了: if (Input.Keyboard.IsKeyDown(Keys.Space)) effect.CurrentTechnique = effect.Techniques["DiffusePerPixel"]; else effect.CurrentTechnique = effect.Techniques["SpecularPerPixel"];

管理Shader——将Shader导入到您的引擎

clock 七月 30, 2010 10:30 by author alex
将Shader导入到您的引擎 感谢DirectX和Xna,加载fx文件和设置所有必需的参数是很容易的(上一章在绘制2D直线和3D直线时你已经这样做) 。在下一章您会使用一个更一般的类,它可以接受许多不同的shader,并采用了更优化的方式设置所有必需的参数,但对于这一章在引擎中只使用simpleshader.fx文件。 与以往一样,你通过定义单元测试开始。在命名空间Shader中创建一个新文件simpleshader.cs并写入以下代码: #region Unit Testing public static void TestSimpleShader() { SimpleShader shader = null; Model testModel = null; Texture testTexture = null; TestGame.Start("Test SimpleShader.fx", delegate { shader = new SimpleShader(); testModel = new Model("Apple"); testTexture = new Texture("Marble"); }, delegate { // Render model shader.RenderModel(testModel.XnaModel, testTexture); }); } // TestSimpleShader #endregion 该单元测试还没有Rendermodel的方法,让我们创建它: public void RenderModel(XnaModel someModel, Texture texture) { } // RenderModel(someModel, texture) 现在,编译后您就可以看到一个空白屏幕。 编译Shader 此单元测试中要在屏幕上显示点什么,你首先必须加载并编译Shader。正如你加载模型和纹理一样,您使用xna的内容管道(content pipeline)。只需将.fx文件拖动到您的项目(同其他材质和模型一样放至正确的目录),这样在项目创建时会自动编译shader,如果Shader并没有编译你还会获取Shader编译的错误信息。同时,你也要确保marble.dds纹理被正确加载,因为您的单元测试中要用到。 加载effect和加载纹理一样简单,只需定义一个effect变量,并在simpleshader构造函数中加载它: #region Variables Effect effect = null; #endregion #region Constructor public SimpleShader() { effect = BaseGame.Content.Load<Effect>( Path.Combine(Directories.ContentDirectory, "SimpleShader")); } // SimpleShader() #endregion 如果您是在Windows平台上,您也可以动态加载shader,对于在游戏中测试和改变shader是很有用的。我通常使用un-compiled shader文件,我不想改变shader了。下面的代码是用来编译和加载shader的。 请注意,这些类和方法只适用于在Windows平台上。如果您想对Xbox 360使用这些代码,请将#if !XBOX360 #endif围绕这些程序行。 CompiledEffect compiledEffect = Effect.CompileEffectFromFile( Path.Combine("Shaders", shaderContentName + ".fx"), null, null, CompilerOptions.None, TargetPlatform.Windows); effect = new Effect(BaseGame.Device, compiledEffect.GetEffectCode(), CompilerOptions.None, null); 使用参数 在你学习建立Shader的过程中,世界,观察和投影矩阵对转换三维数据,并在屏幕得到正确显示是非常重要的。你应在rendermodel方法设置所有这些Shader的参数,并由您的单元测试调用。 BaseGame.WorldMatrix = Matrix.CreateScale(0.25f, 0.25f, 0.25f); effect.Parameters["worldViewProj"].SetValue( BaseGame.WorldMatrix * BaseGame.ViewMatrix * BaseGame.ProjectionMatrix); effect.Parameters["world"].SetValue( BaseGame.WorldMatrix); effect.Parameters["viewInverse"].SetValue( BaseGame.InverseViewMatrix); effect.Parameters["lightDir"].SetValue( BaseGame.LightDirection); effect.Parameters["diffuseTexture"].SetValue( texture.XnaTexture); 在此代码中首先是设置世界矩阵。这是非常重要的,因为如果世界矩阵没有设置,也许从以往的操作中会产生无法预料的数值,你当然不希望三维模型处于一些随机的位置。因为您的apple.x是相当大的,你应该把它缩放得小一些以适合您上一章建立的simplecamera类。 然后计算worldviewproj矩阵和设置的其他矩阵,lightdir、diffusetexture也很重要,因为FX Composer中会自动加载贴图,而程序中不会,你必须自己设置。如果你从内容管道加载模型,Xna会从模型数据自动加载所有使用的纹理。所以在单元测试中,应加载marble.dds纹理。 顶点格式 在您渲染你的3D苹果模型前必须确保您的程序和shader知道使用哪种顶点格式。在DirectX可以使用一个预定义的固定功能顶点格式,但在xna中不行。你必须用类似于shader中vertexinput结构的方式定义顶点声明。因为你使用内置的vertexpositionnormaltexture结构,所以没无需定义每个值,但在下一章你将学到如何使用您自定义的tangentvertex格式。 // Use the VertexPositionNormalTexture vertex format in SimpleShader.fx BaseGame.Device.VertexDeclaration = new VertexDeclaration(BaseGame.Device, VertexPositionNormalTexture.VertexElements); 您无需在每次调用rendermodel时创建一个新的顶点声明,但为了保持简洁,你每次调用都建立一个新的顶点声明。它以图形设备作为第一个参数,顶点元素的数组vertexpositionnormaltexture结构作为第二个参数。如需详细资讯请参阅第7章。 使用Shader渲染 现在要渲染苹果,您首先应指定您想要使用的technique(默认设置为第一个technique,但最好知道如何设置technique)。你将一直使用effect类的currenttechnique属性。您为每一个technique中的pass(正如我以前说过,通常只要有一个pass)渲染三维数据。渲染苹果并不容易,因为xna只提供一个mesh.draw的方法,可参见model类的代码。 XNA Framework另一个缺少的功能是创造盒,球,或茶壶的mesh辅助类。你也会注意到大部分的direct3dx名字空间的功能在xna中不存在。当您处理自己的content processor时只能使用部分的方法,对您的引擎或测试模型,网格,或shader并没有帮助。因为所有的顶点和索引缓冲区是只写的,所以加载模型时您也不能处理任何的顶点或索引的数据,这虽然有利于快速硬件访问,但不够灵活。在您代码中你只是模拟的mesh.draw方法,只是使用了自己的effect类。 effect.CurrentTechnique = effect.Techniques["SpecularPerPixel"]; effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); // Render all meshes foreach (ModelMesh mesh in someModel.Meshes) { // Render all mesh parts foreach (ModelMeshPart part in mesh.MeshParts) { // Render data our own way BaseGame.Device.Vertices[0].SetSource( mesh.VertexBuffer, part.StreamOffset, part.VertexStride); BaseGame.Device.Indices = mesh.IndexBuffer; // And render BaseGame.Device.DrawIndexedPrimitives( PrimitiveType.TriangleList, part.BaseVertex, 0, part.NumVertices, part.StartIndex, part.PrimitiveCount); } // foreach } // foreach pass.End(); } // for effect.End(); 详细地说这意味着你遍历了每一个pass(这里只有一个)渲染所有网格(meshes)(这里也只有一个),然后你制定的所有mesh parts(仍然又只有一个)最后,您调用drawindexedprimitives方法渲染所有shader中的顶点。然后pass和shader关闭,你终于可以看到带有marble.dds纹理的苹果显示在屏幕上 。(见图6-16) 图 6-16 测试Shader 现在程序可以运行了,你可以试着测试一下其他贴图,材质或渲染模式。 比如要实现线框的效果,你可以看一下前面的图6-5,您可以在Shader开始前改变Fillmode: BaseGame.Device.RenderState.FillMode = FillMode.WireFrame; 或者可以载入的另一个纹理或另一个模型,Shader类允许这样做。最简单的方式是修改环境光,散射光和镜面光的值来渲染一个怪苹果(见图6-17)。 图 6-17 effect.Parameters["ambientColor"].SetValue( Color.Blue.ToVector4()); effect.Parameters["diffuseColor"].SetValue( Color.Orange.ToVector4()); effect.Parameters["specularColor"].SetValue( Color.Orchid.ToVector4()); 请注意,您必须将颜色值转换为vector4。当您在effect.Begin()和effect.Ene()之间设定shader参数,你必须调用Effect.Commitchanges()方法,以确保将您所做的更改发送给GPU,但如果象在simpleshader类中的一样,设置参数后才开始effect.Begin()就无需如此。 如果你通过shader参数名去设置参数,当你不当心拼写错参数很容易发生错误,在下一章我们将学习如何更有效率地去设置参数。 /*728*90文章内页广告*/ var cpro_id = 'u241919';

管理Shader——Shader教程

clock 七月 30, 2010 10:28 by author alex
Shader教程 这节贯穿创建你自己的shader的整个过程。这不是世界上最有用的shader,但它展示了如何得到shader,哪些工具能被使用,以及为你将来的可能更令人兴奋的shader做准备。 看一看图6-4你要前进的目标,它展示了来自于上一章的apple模型,不过这一次你的新shader文件SimpleShader.fx 应用于其上,另一个纹理被用来作为它上面的一些文本。 图6-4 SimpleShader.fx使用了一个normal vertex shader,它只转换3D数据,一个对于pixel shader而言简单的镜面反射 per-pixel 技法,pixel shader为之处理每一个像素、计算周围的环境、漫射、镜面反射成分。因为你不能对pixel shader 规划任何特殊规则,并且展示镜面反射光特效唯一一种方式就是用vertex shader去做,所以这种计算使用固定功能shader是不可能的。你可能要问为什么在vertex shader中计算镜面反射成分很糟糕。好的,如果你有一个低多边形的对象,就像这里的apple,或者来自于较早的normal mapping 范例的球体,仅仅在vertex shader中计算色彩成分将看上去很糟糕,因为你只能为每个顶点计算合成出来的颜色。如果你看一看apple的线框(如图6-5)你能看到那只有一串顶点(这些点连接成线),之间的所有数据不会以正确的方式计算,只能以内插值替换。但是如果像在图6-4中的这样一束高光在两个顶点之间,就根本不会被看到。顺便说一下,如果你想以线框模式渲染,只要在你渲染任何3D数据之前,添加下列代码行: BaseGame.Device.RenderState.FillMode = FillMode.WireFrame; 图6-5 FX Composer 为了从仅有的一点SimpleShader.fx shader起步,你将是使用可以免费得到的 FX Composer 工具,你能在Nvidia 主页(www.Nvidia.com)的developers部分下载它(或者Google找到它)。 在你安装和启动FX Composer之后,你将看到如图6-6所示的屏幕。 它展示给你几个panel面板,甚至能被艺术家用来测试texture纹理、shader技法、修改shader 参数诸如color值、特效强度以及更多。对于你作为开发者最重要的panel面板是在中间的, 它显示了当前被选择的.fx文件(类似于Visual Studio)的源代码。 如你所见,.fx 文件看上去非常类似于 C# 或 CPP,它有语法高亮区和看上去类似于C#的许多关键字。例如,string 就是一个文本字符串;float 就是一个浮点数等等就像在C#中。不过也有其他类型例如float3或者float4x4。这些在float 名后面的数字仅仅表明次元;float3和XNA中的Vector3同样结构,它也包含3个浮点数(x、y、z)。Float4x4 描述了一个矩阵并且容纳16 个float数值(4×4);它也和 XNA的Matrix结构是同一种格式。你必须知道的最后一些变量类型是texture纹理。Textures被定义为纹理类型,你还必须具体指定一个样本来告诉shader如何使用这个shader(使用时筛选texture尺寸等等)。 图6-6 通过一个专栏,在上面你看到可选的semantic值(WorldViewProjection、Diffuse、Direction等等)后面显示的变量。它们告诉FX Composer 如何填充以及如何使用该值。 对于你的 XNA 程序,这些值没什么,指定这些semantic总是非常普通的。它允许你在其他程序,诸如FX Composer、3D Studio Max中使用shader,当你在后面通读了shader的时候它也很有用。semantic告诉你规定要用的确切的数值。在XNA中你不受应用的限制;例如,你能把world matrix 设置为viewInverse逆视野,不过一会儿就将令人困惑,不是么? 其他panel面板现在对你不那么重要,不过这里有每一面板的说明: 使用顶部的工具条来快速加载一个shader,以及保存你当前的文件。工具条的最后一个按钮是build构建你的shader,并且在底部的log面板显示你的任何编译错误(非常像Visual Studio)。你每一次构建shader它都自动保存,你应该尽可能地时常使用(或者热键Ctrl+F7)该按钮以确保你的shader总是被编译和保存。 左边的Materials 面板显示给你一个所有你当前能加载到FX Composer中的shader列表,一个shader带有一个小预览球,你一改变任何shader,球体就随之改变。这里最重要的按钮是“Assign to Selection”,把材质设置给当前被选择的Scene面板对象。 Textures 面板显示了当前shader使用的纹理。如果你加载一些外部的shader、texture文件经常找不到是因为它们存在于另一个文件夹或者不匹配.fx shader文件。那些不能被加载的Texture被显示为全蓝的bitmap位图,你首先应该确保加载纹理,否则shader输出通常是黑的和无效的。 在右边的Properties 面板显示了shader中你能设置的所有参数(如果不是,它们已经被FX Composer填充了,像world、viewInverse等等)。要让shader在你的XNA引擎中以同样的方式工作,你必须也在游戏引擎中设置所有这些值,特别是在3D世界中依赖于当前摄像机位置、viewport、object的矩阵。如果你不设置参数,就使用.fx文件源代码中直接被设置的默认值。为了确保所有参数总是合法数值,举个例子,即使用户或者引擎没有设置任何颜色参数,你应该始终确保这些参数是有用的默认设置,如漫反射颜色设置为白色: float4 diffuseColor : Diffuse = {1.0f, 1.0f, 1.0f, 1.0f}; 最后,Scene 面板显示了一个像标准球体的简单范例对象来测试你的shader。你也能把它改变为一个立方体、圆柱体或者其他。二者择一地,你甚至能导入模型文件,并且四周播放它们,不过大多数文件不能很好工作,当你导入场景的时候,在FX Composer中的摄像机总是陷入困境。我正好坚持标准球体对象,在你的XNA引擎中进行所有高级的测试。FX Composer 2.0 在加载和处理自定义3D数据和模型文件上将会好很多。你可以使用Collada 文件,甚至管理所有的shader,这些shader被你模型中的每一个mesh所使用。如果你读到这里,可以获得FX Composer 2.0 就用它。 FX文件布局 如果你以前作过OpenGL,就不得不亲自编写顶点和 fragment shader(在DirectX中就意味着vertex和pixel shader),你将很高兴听到.fx文件把所有的shader代码放在一个地方,在你的.fx 文件中有许多不同的 pixel shader 语句块来支持多重目标结构(multiple target configurations)比如对应于GeForce 3 的pixel shader 1.1、为了支持Nvidia GeForce FX 或者 ATI Radeon 9x 就是pixel shader 2.0。具有在一个.fx文件中支持多重vertex和pixel shader的能力有另一个有用的次要影响,就是把相似的shader一起放置,并且它们都使用一些公有的方法和相同的shader参数,这使得shader开发更加容易。举个例子,如果你有一个normal mapping shader用于光亮的金属表面,它看起来就像金属,你可能需要另一个有更多的漫反射外观的shader用于石头,你可以把不同的shader技法(shader technique)放到同一个shader文件,然后在你的引擎中基于你想展示的材质选择习惯的技法。 作为一个典型的.fx 文件范例,图6-7展示了SimpleShader.fx 文件的规划层次。更多的复杂shader有更多的vertex shader和pixel shaders 以及更多的 technique技法。你不必对每一个技法创建新的vertex shader或者pixel shader;你能用你选购要的方式组合它们。一些shader可能也使用了multiple Pass,这意味着它们渲染1st Pass传入的一切,然后2nd pass 再次渲染完全相同的数据,来给材质添加更多的特效或层次。使用multiple Pass通常对于实时应用程序来说太慢,因为10 个pass的渲染意味着相同shader下,你的渲染时间将10倍高于仅仅渲染一个Pass。有时候使用multiple Pass可能很有用:使用multiple Pass来实现一些以别的方式不适应于shader指令限制的特效,或者对于你能使用由1st pass产生的,并且在2nd pass修改的post screen shader,来实现更好得多的effect特效。举个例子,模糊开销了大量的指令,因为要得到一个好的模糊结果,你必须混合许多像素来计算每一个模糊的点。 图6-7 例如,10×10 范围的模糊将开销100个像素读取指令,如果整个屏幕上你有一两百万个像素想要模糊听起来可不好。既然这样,仅仅在X方向模糊然后得到结果,second pass在Y方向上用另一系列的10个像素读取指令来模糊它,这样做要好很多。现在你的shader运行快了5倍,并且看起来几乎相同。从1600×1200的背景图片首先采样成400×300,然后执行模糊,你甚至有更好的执行效能产生,这将给你带来16倍的效能提高(现在令人吃惊吧)。 第八章谈论post screen shader;不过首先编写SimpleShader.fx文件吧。如你在图 6-7 所见,shader 文件使用相当多的shader参数。一些参数不是那么重要,因为你也能直接给shader进行硬编码材质设定,而且这种方式使你能在引擎中改变材质的颜色和外观,并且你能对许多不同的材质使用shader。其他参数,比如matrices和texture 非常重要,如果引擎不设置这些参数你就不能使用shader。在引擎中,材质数据,比如mcolor和texture值,应该在你创建shader的时候被加载,并且world matrix世界矩阵、light direction光照方向等等应该被每一帧设置,因为这些数据可能每一帧都变化。 参数 如果你想紧跟shader创建的步伐,你可能想现在打开FX Composer ,开启一个空白的.fx 文件。 选择File-〉New 来创建一个新的空白.fx 文件,并且删除所有内容。你将开启一个完全空白的.fx 文件。 你可能想做的第一件事就是快速回忆起关于这个文件的情况,稍后当你打开它时,在文件顶部添加一行描述或者注释: // Chapter 6: Writing a simple shader for XNA 正如你在SimpleShader.fx 文件概览中所见的,你首先需要几个矩阵来转换vertex shader中的3D数据。这可能是worldViewProj矩阵、world matrix、和viewInverse matrix。worldViewProj matrix 结合了world matrix,后者把你想渲染的对象放到世界中正确的位置;view matrix,把3D 数据转换到你的摄像机(view space)视图空间(如上一章图 5-7 );最后,projection matrix,把(view space point)视图空间点放到正确的屏幕位置。该矩阵允许你仅仅使用一个矩阵乘法操作,就把input位置快速转换为最终的output位置。world matrix is then used to perform operations 3D世界中的 比如计算 world normal、lighting calculations等等。viewInverse通常只是用来得到更多关于摄像机位置的信息,这个信息能通过4th行得到的矩阵萃取: float4x4 worldViewProj : WorldViewProjection; float4x4 world : World; float4x4 viewInverse : ViewInverse; 这些矩阵的每一个值都是一个float4×4类型(这是和XNA中的矩阵相同的数据类型),为了获得诸如FX Composer或者3D Studio Max这些应用程序的支持,你最好使用shader semantic来描述这些值,当模型塑造者想看一看带有shader的3D模型看上去如何的时候,这样做是非常重要的。很酷的一件事是,无论在FX Composer、3D Studio还是在你的引擎中,模型看上去绝对相同。这个事实能节约你大量的游戏开发时间;特别缩短了要正确得到3D对象的所有外观的测试过程。 现在是保存文件的时候了。只要按 Build 按钮,或者 Ctrl+F7 ,你被提醒输入一个新shader的名字。就命名为SimpleShader.fx ,把它放到你的XnaGraphicEngine内容文件夹,这样,你以后在XNA中可以快速使用它。在保存之后,FX Composer将在源代码的Tasks panel下告诉你“There were no techniques” 和“Compilation failed”。好吧,马上你将实现这些technique技法,不过首先要实现剩余的参数。因为你的shader使用了一个light使得你的apple加亮 (如图 6-4)你需要一个light,它可以是一个点光源或者一个方向光源。使用点光源有点儿更复杂,因为你必须为每一个单独的顶点计算光线方向(如果你喜欢,甚至可以对每一个单独的像素)。如果你使用一盏聚光灯,计算甚至变得更加复杂。点光源的另一个问题是它们通常随着距离衰减,如果你的3D世界很大,将需要大量灯光。Directional light方向光源简单得多,对于快速模拟室外环境的太阳非常有用,比如在下面几章中你将创建的游戏。 float3 lightDir : Direction < string Object = "DirectionalLight"; string Space = "World"; > = { 1, 0, 0 }; 除了仅仅给所有材质添加的常规亮度的环境光,下列光源类型通常被使用在游戏中: Directional Lights:方向性光,最简单的光源类型,非常易于实现。你能在shader中直接使用光源方向进行内部光源计算。在现实世界中没有方向性光存在;甚至太阳也是一个巨大的遥远的点光源,不过它很易于实现户外光照场景。 Point Lights: 点光源,计算一个单独的点光源不是非常困难,但是你不得不去计算超过一定距离使光源衰减的下降程度,万一你需要光源的方向,它也需要在shader中被计算,它会降低速度。不过点光源的主要问题是对于任何一个比房间大一点的场景,你需要超过1个点光源。3D射击游戏通常使用技巧来限制在同一时间内被看到的点光源的数量,但是对于户外游戏比如策略游戏,使用一个方向性的光源,仅添加稍稍几个的光照特效,以及对一些特殊效果使用简单的点光源,要简单得多。 Spot Lights: 聚光灯,与点光源是同样的事物,它们仅仅指向一个方向,并且由于光照圆锥计算,只照亮一个小的区域。聚光灯计算起来有一点更困难,但是如果你能忽略光照计算的困难部分(例如,当使用一个带有多重聚光灯的复杂的normal mapping shader 法线映射着色器),它甚至可以比使用点光源快很多。目前,你在Shader Model 3.0中仅能做条件语句比如“if”,前面几个shader版本也支持这些语句,但是所有的“if”语句和“for”循环只是正好被解开和展开,你不能像在Shader Model 3.0中获得很大的执行效能受益。 现在前面的代码看上去有点儿更复杂了;第一行几乎和矩阵一样。float3 明确说明你使用了一个Vector3 ,Direction 告诉你lightDir 被应用为一个方向性光源。其括号内部定义了一个Object和Space 型变量。这些变量被称为annotations (评注),它们规定了FX Composer 或者3D Studio Max这些其他程序参数的使用。这些程序现在知道如何使用该值,它们将自动把它分配给可能已经在场景中存在的light对象。你刚好能用这种方式在一个3D程序中加载shader文件,并且不必手工连接所有的light、材质设定、纹理就立刻工作。 接下来你将定义材质设定;你将使用的材质设定和标准的DirectX 材质所采用的相同。你能以这种方式在3D Studio Max等程序中使用类似的shader,或者以往的 DirectX材质,并且所有的color值被自动地正确应用。在引擎中你通常设置环境(ambient)和散射(diffuse)颜色,有时你也对于镜面颜色计算指定不同的光亮值(shininess value )。你也许注意到这里你不再使用任何 评注(annotation) - 你也可以在这里指定,不过即使你不定义annotation评注,材质设定在 FX Composer 和 3D Studio Max 中都工作良好。引擎也仅仅使用默认值 万一你以后不想为单元测试重写默认值。 float4 ambientColor : Ambient = { 0.2f, 0.2f, 0.2f, 1.0f }; float4 diffuseColor : Diffuse = { 0.5f, 0.5f, 0.5f, 1.0f }; float4 specularColor : Specular = { 1.0, 1.0, 1.0f, 1.0f }; float shininess : SpecularPower = 24.0f; 最后,你的shader需要一个纹理,看上去比显示一个灰色球体或苹果更有点儿趣味。取代使用apple纹理就像来自于上一章那样的原始苹果,你将使用一个新的测试纹理,在下一章当你添加normal mapping的时候,它将变得更有趣。纹理调用了marble.dds (如图 6-8): texture diffuseTexture : Diffuse < string ResourceName = "marble.dds"; >; sampler DiffuseTextureSampler = sampler_state { Texture = <diffuseTexture>; MinFilter=linear; MagFilter=linear; MipFilter=linear; }; 图6-8 ResourceName 评注(annotation)仅仅使用在 FX Composer,并且它从shader所在的同一个文件夹中自动加载 marble.dds 文件 (确定marble.dds 文件也在 XnaGraphicEngine 的content 文件夹)。这个例子只是具体指定你想对纹理使用linear filtering线性过滤。 Vertex Input Format 在你最后编写vertex shader 和 pixel shader 之前,你必须指定顶点数据在游戏和vertex shader之间被传入的方式,它被处理为VertexInput 结构体。它使用和XNA的VertexPositionNormalTexture结构体相同的数据,它被应用于apple模型。借助于前面定义的的worldViewProj matrix ,后面在vertex shader 中转换了apple的position 位置。texture coordinate 仅仅用来得到每一个你稍后将在pixel shader中渲染的像素的纹理坐标,并且为了光照计算需要normal(法线)值。 你应该始终确保你的游戏代码和shader使用了完全相同的顶点输入格式。如果你不这么做,错误数据可能应用在纹理坐标,或者顶点数据可能缺失,渲染起来一团糟。最好的实行是在应用程序中定义你自己的顶点结构(见下一章的TangentVertex ),然后在shader中定义同样的顶点结构。在你的游戏代码调用shader之前,还要设置使用的顶点声明,它描述了顶点结构体的规划。在下一章,你能找到更多关于此的细节。 struct VertexInput { float3 pos : POSITION; float2 texCoord : TEXCOORD0; float3 normal : NORMAL; }; 你还是必须以相似的方式定义从vertex shader 传入到pixel shader的数据。起先也许听起来不熟悉,我向你许诺这是最后你必须去做的一件事,就是最终得到shader代码。如果你看一看图6-9,你能看到3D几何学经历的路线,从你的应用程序content数据到shader,shader使用图形硬件来以你想要的方式最终在屏幕上结束。虽然这整个过程比起在DirectX的旧岁月仅仅使用固定功能管道更加复杂,但是它允许你对每一个顶点优化代码,并且你能在每一步(无论在应用程序级别,还是动态的,当顶点在vertex shader被处理或者当它在屏幕被渲染的时候你能更改最终的像素颜色)更改数据。 图6-9 你的shader的VertexOutput 结构体传递了被转换的 vertex 位置、被应用的纹理的texture coordinate、一个normal法线和 halfVec 向量,为了直接在pixel shader中执行镜面反射颜色计算。这两个向量都必须被作为 texture coordinate传递 ,因为 从vertex 到pixel shader传递的数据只能是 position位置、color颜色、或者 texture coordinate数据。不过那没关系;你仍然能使用VertexInput 结构体中的相同形式的数据。它对于告诉 FX Composer、你的应用程序、或者任何其它使用shader的程序,在VertexInput 结构体中使用正确的semantics (Position、TexCoord0,和 Normal)非常重要。 因为你亲自定义了 VertexOutput 结构体,并且它只被shader内部使用,你能把任何你想要的放到这里,不过你应该尽可能让它保持短小,你也被你能忽略的pixel shader 的 texture coordinate的数目所限制(在pixel shader 1.1中是4,在 pixel shader 2.0中是8)。 struct VertexOutput { float4 pos : POSITION; float2 texCoord : TEXCOORD0; float3 normal : TEXCOORD1; float3 halfVec : TEXCOORD2; }; Vertex Shader vertex shader 现在抓取了VertexInput数据,并且为了pixel shader把它转换为屏幕位置,pixel shader最终为每一个可见的多边形的点渲染输出的像素。vertex shader的最前面几行通常看起来和每一个其他的vertex shader极其相似,不过为了用在pixel shader中,你经常在vertex shader结尾预先估计数值。如果你正在使用pixel shader 1.1 你不能做某些事 比如 normalizing vectors或者执行复杂的数学函数如power(指数函数)。不过即使你使用 pixel shader 2.0 (就像你为这个shader) 你可能想预先估计某些数值来提高pixel shader的速度,pixel shade被每一个单独的可视的像素所执行。通常比起像素,你会拥有少得多的顶点,并且你在vertex shader 进行每一个复杂的计算能成倍加快pixel shader的执行效率。 // Vertex shader VertexOutput VS_SpecularPerPixel(VertexInput In) { VertexOutput Out = (VertexOutput)0; float4 pos = float4(In.pos, 1); Out.pos = mul(pos, worldViewProj); Out.texCoord = In.texCoord; Out.normal = mul(In.normal, world); // Eye pos float3 eyePos = viewInverse[3]; // World pos float3 worldPos = mul(pos, world); // Eye vector float3 eyeVector = normalize(eyePos-worldPos); // Half vector Out.halfVec = normalize(eyeVector+lightDir); return Out; } // VS_SpecularPerPixel(In) vertex shader 拿 VertexInput 结构体当作为一个参数,它被自动填充,并且经由你稍后将在.fx文件末尾定义的shader technique 从3D应用程序数据中传递过来。这里重要的部分是VertexOutput 结构,它从 vertex shader 中被返回,然后传递给 pixel shader。数据不是正好被 1:1 传递为pixel shader的,而是在每一个单独的多边形点之间,所有数值是以内插值替换的(如图 6-10)。 图6-10 对于任何位置和颜色值,这个过程是一件好事,因为当数值被正确的插入,输出看起来会好得多。不过万一你使用normalized 向量,就会被GPU自动进行的插补处理搞乱。为了修正之,你必须在pixel shader(如图6-11)中re-normalize (重新法线化)向量。有时候他能被忽略,因为加工品不可见,要不是你的镜面反射每像素计算将被每一个低多边形数目的对象可见。如果你使用pixel shader 1.1你不能在pixel shader 中使用normalize method 方法。你可以使用一个辅助立方体映射来代替,它对每一个可能的输入数值包含预先估算的normalized 数值 。更多细节请看下几章的NormalMapping shader effect和ParallaxMapping shader effect。   图6-11 如果你再次快速看一看源代码(或者如果你正尝试亲自编写你的第一个shader),你能发现通过屏幕输出位置的计算你入门了。因为所有的矩阵操作期待一个Vector4 ,你必须把你的Vector3输入数值转换为Vector4 ,并且把设置W成分设为1来得到worldViewProj 矩阵(转化意味着矩阵的移动)的转化部分的默认行为。 接下来的纹理坐标正好pixel shader被忽略了;你对操作它不感兴趣。你可以,举个例子,这里的乘以纹理坐标或者添加一个偏移量。对于细节映射或者water shader,带有不同的乘法因子或者偏移量,有时候复制纹理坐标,然后在pixel shader中多次应用就很有益处。 来自于apple 模型的每一个法线向量然后被转化到 world space世界空间,当你四处旋转apple 模型的时候就很重要。然后所有的法线也都被旋转,另外light看上去将不正确,因为lightDir 值不知道每一个模型被旋转了多少,并且lightDir 值正好储存在世界空间。你的顶点数据在应用于world matrix 之前,仍然是在所谓的object space中,如果你喜欢这么做(例如,四处摇晃物体或者朝一个目标方向拉伸它),它也能用于几个特效。 你在vertex shader 中最后一件要做的事是要计算light direction 和 eye vector之间的平分线向量,它帮助你在pixel shader中计算镜面反射颜色。正如我之前说过的,在这里的vertex shader 中计算它更有效率得多,而不是对于每一个点一遍又一遍再次重新计算这个值。平分线向量用于phong shading ,以及当从一个方向靠近light direction 看物体的时候,产生镜面高光(如图6-12)。 图6-12 Pixel Shader pixel shader 负责最终在屏幕上输出某些像素。要测试之,你只需输出任何你喜欢的颜色;举个例子,下列代码对于每一个屏幕上被渲染的像素仅仅输出红色: // Pixel shader float4 PS_SpecularPerPixel(VertexOutput In) : COLOR { return float4(1, 0, 0, 1); } // PS_SpecularPerPixel(In) 如果你现在按下 Build,shader 仍然不编译,因为你还没有定义一个 technique 技法。只需定义下列 technique 技法来使得 the shader 工作。 technique的语法 总是相似的;通常你只需 one pass (这里调用的 P0 ),然后你定义使用的vertex shader和pixel shader通过具体指定应用的 vertex shader 和 pixel shader 版本: technique SpecularPerPixel { pass P0 { VertexShader = compile vs_2_0 VS_SpecularPerPixel(); PixelShader = compile ps_2_0 PS_SpecularPerPixel(); } // pass P0 } // SpecularPerPixel 现在你终于能够在FX Composer 中编译shader ,并且你应该看到如图 6-13所示的输出。确定你在 FX Composer 的 Scene panel 分配完了shader 选项(点击 sphere,然后在 Materials panel点击SimpleShader.fx 材质,并且点击“Apply Material” 按钮)。 图6-13 下一步你应该进行的是把 marble.dds 纹理放到 sphere上。借助于pixel shader中的tex2D方法完成之,pixel shader期待一个纹理采样作为第一个参数,texture coordinates 作为第二个参数。用下列代码替换来自于前面代码中的return float4 这行,构造你的3D对象: float4 textureColor = tex2D(DiffuseTextureSampler, In.texCoord); return textureColor; 在编译shader之后,你现在应该看到如图 6-14所示的结果。如果你只看到一个黑色的 sphere 或者根本看不到 sphere,你很可能没有加载好 marble.dds 纹理(看 Textures panel 面板,确定纹理被加载就像先前描述的;你能在属性中点击diffuseTexture ,并且亲自加载它)。 图6-14 你必须要做的最后一件事是基于lightDir和halfVec数值,计算diffuse 散射颜色和specular镜面反射颜色成分。正如所提及的,你也想确保在pixel shader中,这些法线被重新re-normalized 来除掉人工因素。 // Pixel shader float4 PS_SpecularPerPixel(VertexOutput In) : COLOR { float4 textureColor = tex2D(DiffuseTextureSampler, In.texCoord); float3 normal = normalize(In.normal); float brightness = dot(normal, lightDir); float specular = pow(dot(normal, In.halfVec), shininess); return textureColor * (ambientColor + brightness * diffuseColor) + specular * specularColor; } // PS_SpecularPerPixel(In) diffuse color 散射颜色通过计算re-normalized的法线(在 world space, 看本章前面的 vertex shader 讨论)和 lightDir的点乘得到,lightDir也在 world space中。 如果你做任何矩阵乘法、点乘、或者叉乘计算,它们在同一空间中总是是很重要的,否则结果会大大错误。点乘 行为只是你需要计算 散射颜色的方式。 如果lightDir 和 normal point 在同一个方向,就意味着法线是正指向太阳的,并且diffuse color 应该取最大值(1.0);如果normal 是在 90 度角,点乘将返回 0 ,并且diffuse component 是 zero。为了从暗侧业能看到sphere,ambient color环境颜色被添加,当没有散射或者镜面反射光可见的时候,这种光也照亮sphere。 specular color 镜面反射颜色是由Phong公式通过法线和平分线的点乘来计算的,平分线是你在vertex shader中计算过的。然后你由 shininess 因子得到这个结果的幂,来使得被镜面高光影响的区域大大减小。越高的 shininess 值,highlight 变得越小 (如果你想目睹,就上下微调shininess 值)。你能最终在pixel shader的末尾添加所有色彩值,用纹理色彩乘以这个结果,并且返回要在屏幕上被喷绘的一切(如图 6-15)。 图6-15 现在你完成了shader上的工作。被保存的shader文件现在甚至能被诸如3D Studio Max 等其他程序使用,以帮助艺术家目睹3D模型在游戏引擎中将看起来如何。接下来你将在游戏引擎中执行shader。

管理Shader——Shader概览

clock 七月 30, 2010 10:16 by author alex
  管理Shader 现在基本图像引擎已经被启动并且运行了,你暂时可以把注意力放在shader开发。正如上一章提及的,在XNA中一切事物在shader的帮助下被渲染,即便当你使用 SpriteBatch 类或者 SimpleEffect 类模仿固定功能行为,不直接和shader交互的时候。本章遍历从零编写shader的整个过程,然后你学习所有关于vertex buffer、vertex shader 处理过程,以及pixel shader最终如何在屏幕上产生像素。如果你已经是一个shader专家,你很可能想跳过本章或者仅仅快速浏览。但是因为下面几章都依赖于shader的基本知识,所以我想确保所有人在同一水准上。 因为在XNA中shader是如此重要,如果你还没有牢固的shader知识,你的确应该尝试遵循本章的所有步骤。本章的设计方式是为了让你亲自遵循每一个独立步,在本章结尾你将编写你的第一个shader,你应该知道一切是如何从开始到完成的。然后你将为下几章做准备,并且你能容易地编写自己的shader。 在开始下几章炫酷的normal mapping effect或者post screen shader之前,你首先必须学会基础。shader不仅仅用于高质量的特效,也取代非常简单的渲染过程,这些处理曾经被用在固定功能管道的渲染。在学习了一点儿shader和图形卡发展的历史之后,你将经历创建一个简单shader的过程,只要渲染一个带有每像素镜面反射光的简单3D模型,然后把shader导入到你的游戏引擎。你将用来创建shader、甚至在你开始把更多代码放进引擎前测试shader的主要工具,是来自于Nvidia的FX Composer,它是一个快速创建自己的shader的好工具。 Shader概览 固定功能意味着GPU开发者已经安排了一种固定方式来转换顶点(显示卡能够T&L,意思是能transform 和 light vertices)并且以预编程的方式直接在GPU硬件上输出像素。改变这种行为是不可能;你只能enable和disable某种特性。对于早期游戏这样相当好,因为GPU硬件做了大量的工作,保证CPU自由安排其他任务,并且GPU比CPU渲染3D数据快很多,因为GPU正是为了渲染多边形而被深度优化过的。如果你听说过“Direct3D 7.0 capable graphic cards”,这正是它所能做的;早期的显示卡有一个相似的行为,不过在Direct3D 5.0以及之前直接在硬件级的带有执行buffer之类的GPU上编程真的很困难。在1999年左右Nvidia GeForce系列图形卡问世的时候,Direct3D 7.0 简化了这个渲染过程。现在一次性渲染带有多重纹理的多边形成为了可能,使用纹理压缩让显存适应容纳更多的纹理,直接在显存中使用vertex buffer来提高执行效能。 历史 即使是在Direct3D 7.0中取得这些巨大进步之后,游戏们看起来并没有太大的不同,它们只是运行得更快,也许有更多的多边形,并且利用纹理压缩和多重纹理的特效来展现更多的纹理,以及混合它们。真正的革命随着DirectX 8和DirectX 9而到来。DirectX 8最终删除了DirectDraw组件,并且被引入了vertex shader、pixel shaders和许多特效——bump mapping凸凹映射,自定义纹理映射等等……但是它只支持Shader Model 1.0,由于众多的API改变了,在一开始并不对用户很友好,并且编写汇编shaders 不是一件轻松的事。 DirectX 9添加了对HLSL的支持(High Level Shader Language高级着色语言,你将在本章中学习到),伴随着即将到来的图形卡的新特性,比如HDR渲染(high dynamic range)和多渲染目标,它允许像deferred shading延迟明暗处理的technique技法。 DirectX 9非常流行,许多开发者都从OpenGL转移到DirectX,这正是因为OpenGL在一开始不能恰当的支持shader,以及OpenGL 2.0 标准直到被认可走了太长的路。其对于shader的扩展模式也比使用Direct3D的shader 类更为逊色,并且直到花了很长时间OpenGL中才有更好的结构可用。 Direct3D 10也再一次大大领先于OpenGL,支持Shader Model 4.0和geometry shader至今已经有一年多了,OpenGL有大量的追赶工作要做。Direct3D 10目前只能工作在Windows Vista,不过你仍然可以在XP中使用OpenGL的扩展来做某些Shader Model 4.0的开发。但大多数的Windows游戏开发者仅仅为了Direct3D 10,在几年里也将转到Vista。和有些人的信念相反,DirectX 9中不支持Shader Model 4.0,即所谓的“DirectX 9.0L”只是一种在Vista 中支持DirectX 9.0游戏和应用程序的方式。在我看来,对于游戏开发者被如此之多不同的DirectX版本环绕,支持DirectX 9并且退回到可能只支持DirectX 7或8的旧硬件,然后为了Vista中的最新特效,不得不执行Direct3D 10并最终也使运行Windows XP的人有一个DirectX 9.0的选择,并不好。那听起来不像是一个愉快的旅行。对于你幸运的是XNA没有这些问题;这儿只有一个版本可用,Shader Models 1~3 (DirectX8和9),你不用担心任何其它的事。它将会在Windows XP、Vista、甚至在Xbox 360 上运行良好。 有关DirectX历史的有关信息,Windows版本和可用的硬件技术的关系,如图6-1。它显示了DirectX的版本经常以某些方式绑定到Microsoft 最新的操作系统,你能预期在将来也会实现的。DirectX 1 and 2没有真正的被使用。DirectX 3仍然支持保留模式(high level),但是这种方式没有被游戏开发者太多使用,并且在接下来的版本中被轻视。DirectX 5和Windows 98一起出现,并且被更广泛的运用( DirectX 4版本号被跳过)。DirectX几乎每年改进一个版本,直到DirectX 7。它被更广泛的使用,并且图形硬件每年都变得越来越快。和Windows XP一起,DirectX 8被发布,并且第一次引入shader。半年以后,DirectX 9已经可以使用,并且有许多版本。在头几年(2002,2003,2004),Microsoft 用一个新字母命名每个新版本(DirectX 9.0a、DirectX 9.0b等),并且每个版本都在最新的图形硬件上允许一个新shader model的应用;在9.0c之后,他们停止了对版本的命名,不过许多DirectX 9.0c的版本(现在已经有11个了)被发行,经常是每隔几个月。Direct3D10 在2005年末就已经在beta测试中,最终和Windows Vista一起在2007年初发布。支持Shader Model 4.0的图形硬件在2006年末(Nvidia GeForce 8800)已经可以获得了。   图 6-1 Shaders Everywhere in XNA 在前面几章你已经看到,要在XNA中渲染一切,shader非常重要;没有shader在屏幕上输出像素,你甚至不能把一条线放在屏幕上。当然你能对2D界面图形使用一些精灵类,也许你还使用SimpleEffect 类显示一些3D数据,不过我不喜欢SimpleEffect 类,本书中我也根本不会使用它。我认为它太复杂以至于不能使用,并且当你能够编写自己的shader的时候,它也确实没什么益处,编写自定义shader要简单得多,快得多,也灵活得多。使用这些简单特效意味着你必须使用预定义的特性,如果你再次需要某个事物你必须再次开始,总之编写你自己的shader吧。对于本书即将到来的游戏,你将主要使用normal mapping shader来显示3D数据,然后一些炫酷的post screen shader特效来生成最终的屏幕数据。这些shader都不可能仅仅使用SimpleEffect 类。 在你之前使用OpenGL 或者 DirectX ,并且仅仅使用固定功能管道的时候,你可能想知道为什么你把如此之多的特效放进shader,为什么你不能仅仅在屏幕上渲染某些3D数据,并且处理它呢。好的,比起使用,编写是有点儿难,虽然你在DirectX 中可能仅仅渲染.x模型,甚至不知道太多关于纹理,或者GPU内部如何工作,但是如果你从来没学过这些细节,你就不能真正做到重要的修改。学会所有关于shader也许比较难,不过至少你会更加走近图形硬件,你将能够更加深入理解多边形如何被渲染在屏幕上,为什么某些bug会发生,以及如何快速修正它。 本书接下来的两章之后,不再讨论shader,并且你很可能不再会花太多时间在你的游戏的shader上,以后你有重要的一些事要开始。然后你将仅仅使用这些shader,你甚至不需要再次考虑它们。 我想提及的最后一件事是有关shader使用增加的工作量。不但你作为一个程序员必须学习所有关于shader,你很可能要编写大多数shader,除非在你的团队中有一个专门写shader的家伙,而且你的图形艺术家(真希望不再是你)也必须知道关于shader。举个例子,为了实现像次时代游戏(例如,Doom3)那样宏伟的外观特效,如果你的3D数据组成不仅仅是基本几何体,还提供normal map(法线映射)来实现那些特效,使用一些normal mapping shader就很有意义。 不再牵扯下一章的内容了,第七章会深入细节讨论normal mapping shader,你通常需要一个3D对象的高多边形版本(多达几百万个多边形)和一个要渲染的低多边形版本(仅仅几百或者上千个多边形)。如你在图6-2所见,没有应用normal mapping的低多边形对象看上去相当糟糕,不过一旦激活之(看右边),球体看上去突然好得多了。 图 6-2 作为一个创建高细节模型的替代品,这些高细节模型通常大量的工作,你也能使用绘图程序来“paint”你自己的normal map ,通过首先画一个高度图,然后把它转换为一个法线映射图。通常这样的映射看上去很糟糕,用这种途径你不可能实现顶级游戏的图象质量,不过一次又一次地你能看到这些以这种方式实现normal map 的游戏节约很多钱。无论如何,你的3D图形艺术师应该意识到所有的新技术,并且在你开始编写你的引擎或者游戏代码之前,应该考虑到content(资源内容)被建立的方式。 范例游戏 如果你想看看shader的表现只需玩任何最近的游戏——它们几乎全都支持shader,即使你不能看到normal mapping 法线映射和post screen effects屏幕显示效果的大量使用,shader也许仍然被用来高效率的渲染3D模型或执行shadow mapping 阴影映射技法。 3D射击游戏像Doom 3、Half-Life 2、Far Cry和更多最近的游戏像Fear、Ghost Recon和Gears of War,展示了真正的顶级的图像,并且大量使用了shader特效。射击游戏从大多数的新图形技术中受益,因为你看见你的周围世界在如此多的细节水平。首先你看见你自己或者至少你的武器,然后你或许在室内,并且看到墙和近距离的周围物体,但是也有一些大的房间甚至是你能看到很远距离的外面世界。这意味着level关卡通常很大,你仍然可以仔细去看你所想看到的,因为你可以移动要你想去的任何地方。显然图形卡,尤其是早期的那些,不能够在显存中存储许多细节,仅仅是映射一张覆盖整个3D level关卡的大纹理,或者场所到处就使用同样的墙的纹理,不是一个真正的好选择。取而代之,游戏开发者们不得不思考许多炫酷的技术来改善视觉质量,通过使用细节映射、法线映射来使玩家认为几何体精细很多,混合进post screen effect(屏幕显示效果)来使整个游戏看起来更真实。 其他游戏比如策略游戏或角色扮演游戏,也能从shader中受益,不过之前的策略或者角色扮演游戏甚至花了更长时间实现shader,这些游戏更专注于游戏进行,如何一次性渲染许多单位、缩小和放大的问题。 总之,你很可能也想在你的游戏中用上炫酷的shader effect,并且如果你去看Rocket Commander的屏幕截图(如图6-3),你会看到一些法线映射,以及post screen shader在工作。没有shader激活(左面的屏幕)的游戏看起来真的很糟糕,并且这些小行星看起来也实在不真实,因为光源是不正确的。请注意这个没有shader的版本,只是提供了一种支持旧图形硬件的选择;游戏被设计为使用shader的。固定功能管道版本很可能创造一个较好的屏幕截图,但是如果光源没有改变,如果你没有任何的post screen effects(屏幕显示效果),在真实的时间里,游戏会看起来甚至更不真实。 图 6-3 至于在游戏中使用shader和固定功能管道之间更好的例子,看一看3D游戏中的water effects。水面已经在过去几年里进步很多了。 如果你想看看游戏中更多的范例或者想获得更多的讲解,我建议你看看我的Rocket Commander视频指南,你能www.RocketCommander.com找到的。或许你能在www.XnaRacingGame.com,看我的XNA Racing Game视频指南。那里也有更多的有用的shader资源站点的链接,更多的屏幕截图等等。

游戏组件——总结

clock 七月 29, 2010 15:40 by author alex
总结 你的小游戏引擎现在有了三个新命名空间,也有许多辅助类可以用在未来的项目中。新的TextureFont 类在下面几章尤其有用。它不仅仅在显示诸如记分板这样的游戏数据上有用,也在单元测试中被使用;为了告诉用户所有可用的热键,在单元测试中显示帮助文本非常有用;万一你遇到问题,显示测试数据或者Debug数据尤其有用。例如,一个camera类的单元测试能输出摄像机的位置和旋转值以便于看出当测试运行时发生了什么。 我希望这四个章节读起来有趣,你喜欢这里开发的简单游戏。街机游戏是有趣,不过只编写简单的2D游戏是没有艺术感的,如果你编写下一个Pong克隆,你肯定也不会更关注。学习的目的是介绍所有的辅助类,以及学习更多的敏捷方法学,单元测试,拥有这些真正的项目非常有用,而不是只谈一些运用上以及无聊的辅助类的理论。 无论如何,在本书下一章,你将投入3D图像的世界,学习如何在XNA中编写自己的3D图像引擎。你不是真的要成为一个数学教授,来理解如何在屏幕上渲染一个模型。归功于XNA和内容管道,不涉及太多关于底层的数学和3D几何学知识,在屏幕上渲染3D数据是可行的。如果你正式3D图像编程,你当然应该知道向量、矩阵,以及如何用正确的顺序操作它们。在3D游戏编程世界中,有许多技巧和知识在问题的周围,不但需要一个聪明的人,而且需要你不断获悉这个领域的新发展。 如果你是一个游戏程序员,你可能不太关心底层的图像引擎代码,只想关注在游戏本身。然而大多数人只想创建一些炫酷的游戏,这通常也意味着他们不得不创建图像引擎,因为没有更完美的3D引擎可以获得,游戏中的每个特效可能需要另外的方式渲染3D数据。在本书最后的部分讨论更多关于游戏编程之后,你的游戏引擎足够完成更复杂的游戏。 最后看一眼你在前几章学过了什么: 写下你的想法,并尽可能发展为一个限于一页纸的小构思是非常重要的。 为了解决问题,你使用了自顶向下的途径,并且你使用了单元测试。 你学习了处理用户输入,播放声音,绘制精灵。在XNA中所有这些相当容易完成,不过为了避免一遍遍的重复相同的代码,你把它们放到辅助类中 大量的辅助类被引入,以帮助你渲染字体纹理、处理字符串、遍历枚举、操作色彩,写下日志文件,以及更多。 在每个游戏中都需要你编写某种碰撞检测。在前两个游戏,你使用了BoundingBox 结构体来检查2D碰撞,本章你在Tetris grid中的MoveBlock方法里,亲自处理了所有碰撞。 单元测试的规则,我不再经常重复。尤其是静态单元测试帮了你很多,并且使本章创建的游戏好玩得多,并且比不使用单元测试更加富有成效。

友情链接赞助