现在可以创建XNA Shooter游戏了。你有了所有的三维模型,所有的效果文件和纹理和声音效果文件,也不必担心场景,因为它工作得很好场景本身是在表面之下的(即z值小于0),这意味着只需将物体放置在z高度为0的地方,这样能使添加特效,碰撞检查和测试变得更轻松。
现在,你可以通过Misson类的场景渲染方法添加自己的飞船并在Player类中控制它。渲染只需以下代码:
Player.shipPos = new Vector3(Player.position, AllShipsZHeight) + levelVector; AddModelToRender( shipModels[(int)ShipModelTypes.OwnShip], Matrix.CreateScale(ShipModelSize[(int)ShipModelTypes.OwnShip]) * Matrix.CreateRotationZ(MathHelper.Pi) * Matrix.CreateRotationX(Player.shipRotation.Y) * Matrix.CreateRotationY(Player.shipRotation.X) * Matrix.CreateTranslation(Player.shipPos)); // Add smoke effects for our ship EffectManager.AddRocketOrShipFlareAndSmoke( Player.shipPos + new Vector3(-0.3f, -2.65f, +0.35f), 1.35f, 5 * Player.MovementSpeedPerSecond); EffectManager.AddRocketOrShipFlareAndSmoke( Player.shipPos + new Vector3(0.3f, -2.65f, +0.35f), 1.35f, 5 * Player.MovementSpeedPerSecond);
所有的缩放和旋转仅仅是让飞船有正确的大小并旋转到正确的方向。绕z轴旋转使飞船飞向前方,而绕x轴和y轴旋转让飞船在前进后退或左右平移是扭动船身。然后,在飞船引擎后部添加火焰和烟雾特效。EffectManager类马上就会讨论到。
控制飞船的代码在Player类中,大多数游戏逻辑都在那处理,包括武器射击,移动飞船,得分以及处理道具:
// From Player.HandleGameLogic: // [Show victory/defeat messages if game is over] // Increase game time gameTimeMs += BaseGame.ElapsedTimeThisFrameInMs; // Control our ship position with the keyboard or gamepad. // Use keyboard cursor keys and the left thumb stick. The // right hand is used for fireing (ctrl, space, a, b). Vector2 lastPosition = position; Vector2 lastRotation = shipRotation; float moveFactor = mouseSensibility * MovementSpeedPerSecond * BaseGame.MoveFactorPerSecond; // Left/Right if (Input.Keyboard.IsKeyDown(moveLeftKey) || Input.Keyboard.IsKeyDown(Keys.Left) || Input.Keyboard.IsKeyDown(Keys.NumPad4) || Input.GamePad.DPad.Left == ButtonState.Pressed) { position.X -= moveFactor; } // if if (Input.Keyboard.IsKeyDown(moveRightKey) || Input.Keyboard.IsKeyDown(Keys.Right) || Input.Keyboard.IsKeyDown(Keys.NumPad6) || Input.GamePad.DPad.Right == ButtonState.Pressed) { position.X += moveFactor; } // if if (Input.GamePad.ThumbSticks.Left.X != 0.0f) { position.X += Input.GamePad.ThumbSticks.Left.X;// *0.75f; } // if // Keep position in bounds if (position.X < -MaxXPosition) position.X = -MaxXPosition; if (position.X > MaxXPosition) position.X = MaxXPosition; // [Same for Down/Up changes position.Y, see Player.cs] // Calculate ship rotation based on the current movement if (lastPosition.X > position.X) shipRotation.X = -0.5f; else if (lastPosition.X < position.X) shipRotation.X = +0.5f; else shipRotation.X = 0; // [Same for shipRotation.Y, see above] // Interpolate ship rotation to be more smooth shipRotation = lastRotation * 0.95f + shipRotation * 0.05f;
HandleGameLogic首先检查游戏是否结束,并在屏幕上显示一条消息告诉你如果你输或赢。然后,当前游戏的时间增加。之后,处理飞船控制,然后发射武器和道具处理,代码最后处理击落敌人后的得分。
你可以使用键盘和gamepad控制飞船。当不支持鼠标,因为对射击游戏来说调试很难,我个人也不喜欢用鼠标控制射击游戏。通过常量MaxXPosition和MaxYPosition可以确保你的飞船没有移动到屏幕之外,shipRotation是根据你的移动计算出来的,如果飞船没有运动它会慢慢恢复到零。
以下代码显示了如何发射武器。更多细节在Player类中。
// Fire? if (Input.GamePadAPressed || Input.GamePad.Triggers.Right > 0.5f || Input.Keyboard.IsKeyDown(Keys.LeftControl) || Input.Keyboard.IsKeyDown(Keys.RightControl)) { switch (currentWeapon) { case WeaponTypes.MG: // Shooting cooldown passed? if (gameTimeMs - lastShootTimeMs >= 150) { // [Shooting code goes here ...] shootNum++; lastShootTimeMs = gameTimeMs; Input.GamePadRumble(0.015f, 0.125f); Player.score += 1; } // if break; case WeaponTypes.Plasma: // [etc. all other weapons are handled here] break; } // switch } // if
当你按下gamepad的A键或右板机或键盘的Ctrl键时就输入了发射指令,但武器不会开火,直到你达到了下一个降温阶段(每件武器都有不同长短的冷却时间)。接着武器被射击并处理武器碰撞检测,lastShootTimeMs被重置为当前的游戏时间等待下一次降温过程。
处理完武器后只剩一件事,就是创建胜负条件。如果你成功到达关尾你就胜利了,而在这之前生命值用完则失败。
if (Player.health <= 0) { victory = false; EffectManager.AddExplosion(shipPos, 20); for (int num = 0; num<8; num++) EffectManager.AddFlameExplosion(shipPos+ RandomHelper.GetRandomVector3(-12, +12)); Player.SetGameOverAndUploadHighscore(); } // if
游戏逻辑的其余部分是敌人,道具和弹药,这些都各自的类中被处理,这些类会在本章后面看到。
回到3D特效。你已经学学习了很多3D特效的知识,借助于Billboard类,在3D中渲染纹理也不难。但现在特效被提升到更高层次,你关心特效长度,动画步骤,淡入淡出。所有的3D多边形生成和渲染都是由Billboard和Texture纹理类处理的,而所有特效是通过EffectManager类管理的(见图11-15),借助于很多静态Add方法让你从代码的任何地方添加特效。
在Effect类你可以看到许多字段,这是用来创建许多不同种类的3D特效的。爆炸效果优化不同于灯光效果。例如常规特效只是混合了一个alpha值,但灯光效果这样做无法工作,因为如果Alpha值被修改会使光线变得更暗,这样看起来非常奇怪,对于灯光特效要在最后使用更小的值才行。其他枚举帮助你快速定义效果类型并通过AddEffect方法很容易地添加它们。
了解EffectManager类最好的办法就是看TestEffects单元测试,然后添加新的效果并加以实施,或为测试更多特效编写新的单元测试。
public static void TestEffects() { TestGame.Start("TestEffects", delegate { // No initialization code necessary here }, delegate { // Press 1-0 for creating effects in center of the 3D scene if (Input.Keyboard.IsKeyDown(Keys.D1) && BaseGame.EveryMs(200)) AddMgEffect(new Vector3(-10.0f, 0, -10), new Vector3((BaseGame.TotalTimeMs % 3592) / 100.0f, 25, +100), 0, 1, true, true); if (Input.Keyboard.IsKeyDown(Keys.D2)) { AddPlasmaEffect(new Vector3(-50.0f, 0.0f, 0.0f), 0.5f, 5); AddPlasmaEffect(new Vector3(0.0f, 0.0f, 0.0f), 1.5f, 5); AddPlasmaEffect(new Vector3(50.0f, 0.0f, 0.0f), 0.0f, 5); } // if (Input.Keyboard.IsKeyDown(Keys.D2]) if (Input.Keyboard.IsKeyDown(Keys.D3)) { AddFireBallEffect(new Vector3(-50.0f, +10.0f, 0.0f), 0.0f, 10); AddFireBallEffect(new Vector3(0.0f, +10.0f, 0.0f), (float)Math.PI / 8, 10); AddFireBallEffect(new Vector3(50.0f, +10.0f, 0.0f), (float)Math.PI * 3 / 8, 10); } // if (Input.Keyboard.IsKeyDown(Keys.D3]) if (Input.Keyboard.IsKeyDown(Keys.D4)) AddRocketOrShipFlareAndSmoke( new Vector3((BaseGame.TotalTimeMs % 4000) / 40.0f, 0, 0), 5.0f, 150.0f); if (Input.Keyboard.IsKeyDown(Keys.D5) && BaseGame.EveryMs(1000)) AddExplosion(Vector3.Zero, 9.0f); // etc. // Play a couple of sound effects if (Input.Keyboard.IsKeyDown(Keys.P) && BaseGame.EveryMs(500)) PlaySoundEffect(EffectSoundType.PlasmaShoot); // etc. // We have to render the effects ourselfs because // it is usually done in RocketCommanderForm (not in TestGame)! // Finally render all effects before applying post screen shaders BaseGame.effectManager.HandleAllEffects(); }); } // TestEffects()
也许这个类应该叫做EnemyUnit类,因为它是只用于敌人的船舶,你自己的船已经在Player类中被处理。起先我想整合所有单位(你自己的飞船和敌人的)在这个类中,但它们的行为方式非常不同,这样做只会使代码更加混乱和复杂。在较为复杂的游戏中也许你应该为单位建立一个基类,然后从基类敌方单位和我方友方单位(例如,多人游戏中要确保所有其他玩家的飞船和你的飞船应有同样的运动方式)。看看图11-16中的这个类。
这个类使用了很多字段去跟踪各单位的生命值,射击时间,位置等,但从外部看此类非常简单,你每帧只需调用一个方法:Render。构造函数只是创建了单位并指定单位类型、位置、默认生命值和伤害值(这些都是从类中的常数数组中获得的——更复杂的游戏应该从xml文件或外部数据源中获得这些值)。
一如往常你应该看一下单元测试,以了解更多这个类的知识:
public static void TestUnitAI() { Unit testUnit = null; Mission dummyMission = null; TestGame.Start("TestUnitAI", delegate { dummyMission = new Mission(); testUnit = new Unit(UnitTypes.Corvette, Vector2.Zero, MovementPattern.StraightDown); // Call dummyMission.RenderLandscape once to initialize everything dummyMission.RenderLevelBackground(0); // Remove the all enemy units (the start enemies) and // all neutral objects dummyMission.numOfModelsToRender = 2; }, delegate { // [Helper texts are displayed here, press 1-0 or CSFRA, etc.] ResetUnitDelegate ResetUnit = delegate(MovementPattern setPattern) { testUnit.movementPattern = setPattern; testUnit.position = new Vector2( RandomHelper.GetRandomFloat(-20, +20), Mission.SegmentLength/2); testUnit.hitpoints = testUnit.maxHitpoints; testUnit.speed = 0; testUnit.lifeTimeMs = 0; }; if (Input.KeyboardKeyJustPressed(Keys.D1)) ResetUnit(MovementPattern.StraightDown); if (Input.KeyboardKeyJustPressed(Keys.D2)) ResetUnit(MovementPattern.GetFasterAndMoveDown); // [etc.] if (Input.KeyboardKeyJustPressed(Keys.Space)) ResetUnit(testUnit.movementPattern); if (Input.KeyboardKeyJustPressed(Keys.C)) testUnit.unitType = UnitTypes.Corvette; if (Input.KeyboardKeyJustPressed(Keys.S)) testUnit.unitType = UnitTypes.SmallTransporter; if (Input.KeyboardKeyJustPressed(Keys.F)) testUnit.unitType = UnitTypes.Firebird; if (Input.KeyboardKeyJustPressed(Keys.R)) testUnit.unitType = UnitTypes.RocketFrigate; if (Input.KeyboardKeyJustPressed(Keys.A)) testUnit.unitType = UnitTypes.Asteroid; // Update and render unit if (testUnit.Render(dummyMission)) // Restart unit if it was removed because it was too far down ResetUnit(testUnit.movementPattern); // Render all models the normal way for (int num = 0; num < dummyMission.numOfModelsToRender; num++) dummyMission.modelsToRender[num].model.Render( dummyMission.modelsToRender[num].matrix); BaseGame.MeshRenderManager.Render(); // Restore number of units as before. dummyMission.numOfModelsToRender = 2; // Show all effects (unit smoke, etc.) BaseGame.effectManager.HandleAllEffects(); }); } // TestUnitAI()
该单元测试可以让你按C,S,F,R或A改变五种敌人中的一个:Corvette,小型运输舰,Firebird,Rocket-Frigate和小行星。按1-0你能够改变这个敌人的AI行为。这里谈到AI有点疯狂,因为所有的人工智能做的只是处理不同的运动形式。该敌人只遵循特定的运动模式,一点儿也不聪明。例如,GetFasterAndMoveDown代码看起来像这样:
case MovementPattern.GetFasterAndMoveDown: // Out of visible area? Then keep speed slow and wait. if (position.Y - Mission.LookAtPosition.Y > 30) lifeTimeMs = 300; if (lifeTimeMs < 3000) speed = lifeTimeMs / 3000; position += new Vector2(0, -1) * speed * 1.5f * maxSpeed * moveSpeed; break;
其他运动模式更简单。每个运动模式的名称告知了行为的足够信息。在游戏中随机分配一种运动模式给每一个新的敌人。你还可以创建一个关卡,包含预定义的位置和运动AI的敌人信息,但我想保持简单。单元测试的其余部分只是渲染敌人和背景。这被用来创建整个Unit类,覆盖了所有东西除了射击和敌人的死亡,这直接在游戏中测试。
刚才我提到敌人的射击,所需的代码让Corvette的射击不那么复杂,因为如果你的飞船处在它正下方时会被立即击中(它只检查它和你的x和y位置并采取相应的行动)。大部分Corvette的射击都会落空。
所有其他敌人不发射即时武器而发射火箭弹,火球或等离子球。这些弹丸持续一段时间飞往目标。Projectile类(见图11-17 )帮助你管理这些对象,简化了武器的逻辑,让你发射出一颗弹丸后就无需管理,由Projectile类代劳。Projectile类将处理碰撞检测,在燃料耗尽或飞出屏幕后自动移除弹丸。
有三种不同弹药,它们的行为有所不同:
等离子球只能被自己的飞船发射,前提是你拥有这个等离子武器。等离子球速度快,比MG威力大,但Gatling-Gun和火箭发射器威力更大,虽然它们也有各自的缺点。
你和敌方的火箭护卫舰都能发射火箭。你的火箭能造成更大伤害,但只会直行。敌人的火箭更加智能,会根据你的位置调整目标,更难躲避。
相对于Unit类的渲染方法,Projectile类的渲染方法不是很复杂。其中最有趣的部分是更新位置和绘制弹丸后的碰撞处理。
public bool Render(Mission mission) { // [Update movement ...] // [Render projectile, either the 3D model for the rocket or just the // effect for the Fireball or Plasma weapons] // Own projectile? if (ownProjectile) { // Hit enemy units, check all of them for (int num = 0; num < Mission.units.Count; num++) { Unit enemyUnit = Mission.units[num]; // Near enough to enemy ship? Vector2 distVec = new Vector2(enemyUnit.position.X, enemyUnit.position.Y) new Vector2(position.X, position.Y); if (distVec.Length() < 7 && (enemyUnit.position.Y - Player.shipPos.Y) < 60) { // Explode and do damage! EffectManager.AddFlameExplosion(position); Player.score += (int)enemyUnit.hitpoints / 10; enemyUnit.hitpoints -= damage; return true; } // if } // for } // if // Else this is an enemy projectile? else { // Near enough to our ship? Vector2 distVec = new Vector2(Player.shipPos.X, Player.shipPos.Y) new Vector2(position.X, position.Y); if (distVec.Length() < 3) { // Explode and do damage! EffectManager.AddFlameExplosion(position); Player.health -= damage / 1000.0f; return true; } // if } // else // Don't remove projectile yet return false; } // Render()
如果这是你自己的弹丸(等离子或火箭),你必须检查与正在活动的敌方飞船的碰撞。如果出现碰撞在造成伤害并添加爆炸效果,此外你还可以获得一些分数(10%的敌方剩余生命值)。然后,返回true,告知调用函数你已经处理了这个弹丸,可以从目前活动的弹丸列表中移除了,这也发生在弹丸飞出边界的情况中。
如果被敌方弹药击中,碰撞检测更简单,你只需要检查己方飞船的碰撞,并以相同方式造成伤害。你的和敌方的飞船的死亡都是由各自的Render方法处理的。前面你已看到当你的飞船生命值耗尽时就会触发死亡。敌人单位的死亡条件看起来非常相似,而且你也返回true把这个敌人从目前的敌方列表中移除。
Item类用来处理所有道具。如图11-18可见这是非常简单的,但没有道具游戏会少很多乐趣。从类中的ItemTypes枚举可看出有6中道具。其中四个是武器,一个是生命值,可完全回复你的飞船的生命值,一个是电磁脉冲炸弹,按空格键可以消灭屏幕上的所有敌人,你最多能同时拥有三枚炸弹。
Render方法与Projectile类中的类似,只是与道具碰撞不会杀死任何东西,相反可以收集这些物品,其效果是立即处理的。这意味着你可以得到新的武器,或回复到100%的生命值,或获得另一枚电磁脉冲炸弹。
/// <summary> /// Render item, returns false if we are done with it. /// </summary> /// <returns>True if done, false otherwise</returns> public bool Render(Mission mission) { // Remove unit if it is out of visible range! float distance = Mission.LookAtPosition.Y - position.Y; const float MaxUnitDistance = 60; if (distance > MaxUnitDistance) return true; // Render float itemSize = Mission.ItemModelSize; float itemRotation = 0; Vector3 itemPos = new Vector3(position, Mission.AllShipsZHeight); mission.AddModelToRender(mission.itemModels[(int)itemType], Matrix.CreateScale(itemSize) * Matrix.CreateRotationZ(itemRotation) * Matrix.CreateTranslation(itemPos)); // Add glow effect around the item EffectManager.AddEffect(itemPos + new Vector3(0, 0, 1.01f), EffectManager.EffectType.LightInstant, 7.5f, 0, 0); EffectManager.AddEffect(itemPos + new Vector3(0, 0, 1.02f), EffectManager.EffectType.LightInstant, 5.0f, 0, 0); // Collect item and give to player if colliding! Vector2 distVec = new Vector2(Player.shipPos.X, Player.shipPos.Y) new Vector2(position.X, position.Y); if (distVec.Length() < 5.0f) { if (itemType == ItemTypes.Health) { // Refresh health Sound.Play(Sound.Sounds.Health); Player.health = 1.0f; } // if else { Sound.Play(Sound.Sounds.NewWeapon); if (itemType == ItemTypes.Mg) Player.currentWeapon = Player.WeaponTypes.MG; else if (itemType == ItemTypes.Plasma) Player.currentWeapon = Player.WeaponTypes.Plasma; else if (itemType == ItemTypes.Gattling) Player.currentWeapon = Player.WeaponTypes.Gattling; else if (itemType == ItemTypes.Rockets) Player.currentWeapon = Player.WeaponTypes.Rockets; else if (itemType == ItemTypes.Emp && Player.empBombs < 3) Player.empBombs++; } // else Player.score += 500; return true; } // else // Don't remove item yet return false; } // Render()
Render代码做的只是在给定位置放置道具,并添加了两个灯光效果是道具有点发光。如果你不看道具可能无法注意到发光效果,但是没有发光效果就更难看到它们。
如果你接近道具小于5个单位时就会自动收集它。你的飞船大约有五个单位的半径,这意味着你可以用飞船的仍以部分去接触道具。然后,这个道具被处理,给你生命值或是武器或是炸弹,同时还有额外的奖励分。此道具现在可以被移除了。如果你没有与道具碰撞,它会留在原地,直到你接触它们或道具飞出边界。
耶,终于完成了。这就是XNA Shooter游戏需要的一切,见图11-19的结果。我希望这一章比第8章更有用,因为在第8章我不想重复原始版本的Rocket Commander的教程。
我希望你喜欢XNA Shooter,如果你想创建自己的射击游戏会发现这很有用。请记住,这个游戏只用了几天时间就做好了,或许你可以使它更好,添加更多的关卡,敌方飞船,或更好的人工智能。玩得快乐!