右击Form1.cs,在弹出的快捷菜单中选择View Code命令。需要为这个文件添加游戏循环和初始化代码,如下所示。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using Engine; using Engine.Input; using Tao.OpenGl; using Tao.DevIl; namespace Shooter { public partial class Form1 : Form { bool _fullscreen = false; FastLoop _fastLoop; StateSystem _system = new StateSystem(); Input _input = new Input(); TextureManager _textureManager = new TextureManager(); SoundManager _soundManager = new SoundManager(); public Form1() { InitializeComponent(); simpleOpenGlControl1.InitializeContexts(); _input.Mouse = new Mouse(this, simpleOpenGlControl1); _input.Keyboard = new Keyboard(simpleOpenGlControl1); InitializeDisplay(); InitializeSounds(); InitializeTextures(); InitializeFonts(); InitializeGameState(); _fastLoop = new FastLoop(GameLoop); } private void InitializeFonts() { // Fonts are loaded here. } private void InitializeSounds() { // Sounds are loaded here. } private void InitializeGameState() { // Game states are loaded here } private void InitializeTextures() { // Init DevIl Il.ilInit(); Ilu.iluInit(); Ilut.ilutInit(); Ilut.ilutRenderer(Ilut.ILUT_OPENGL); // Textures are loaded here. } private void UpdateInput(double elapsedTime) { _input.Update(elapsedTime); } private void GameLoop(double elapsedTime) { UpdateInput(elapsedTime); _system.Update(elapsedTime); _system.Render(); simpleOpenGlControl1.Refresh(); } private void InitializeDisplay() { if (_fullscreen) { FormBorderStyle = FormBorderStyle.None; WindowState = FormWindowState.Maximized; } else { ClientSize = new Size(1280, 720); } Setup2DGraphics(ClientSize.Width, ClientSize.Height); } protected override void OnClientSizeChanged(EventArgs e) { base.OnClientSizeChanged(e); Gl.glViewport(0, 0, this.ClientSize.Width, this.ClientSize. Height); Setup2DGraphics(ClientSize.Width, ClientSize.Height); } private void Setup2DGraphics(double width, double height) { double halfWidth = width / 2; double halfHeight = height / 2; Gl.glMatrixMode(Gl.GL_PROJECTION); Gl.glLoadIdentity(); Gl.glOrtho(-halfWidth, halfWidth, -halfHeight, halfHeight, -100, 100); Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glLoadIdentity(); } } }
在这个Form.cs的代码中,创建了一个Keyboard对象,并把它指派给了Input对象。为使这段代码可以工作,必须在Input类中添加一个Keyboard成员,如下所示。
public class Input { public Mouse Mouse { get; set; } public Keyboard Keyboard { get; set; } public XboxController Controller { get; set; }
需要把下面列出的DLL文件添加到bin\Debug和bin\Release文件夹中:alut.dll、DevIL.dll、ILU.dll、OpenAL32.dll和SDL.dll。现在,这个项目就可以用于开发游戏了。
这是我们创建的第一个游戏,当游戏运行时,如果窗体的标题栏显示的不是Form1,而是其他内容,那就更好了。在Visual Studio中修改这些文本很容易。在Solution Explorer中双击文件Form1.cs,打开窗体设计器。单击窗体,并进入Properties窗口(如果没有看到Properties窗口,则从菜单栏中选择View|Properties Window命令)。Properties窗口中列出了窗体的全部属性。找到Text属性,将其值改为Shooter,如图2所示。
图2 修改窗体标题

1. 开始菜单的状态

第一个要创建的状态是开始菜单。在第一遍编码中,菜单只提供了两个选项:Start Game和Exit。这些选项是一种按钮,所以这个状态需要两个按钮和一些标题文本。图3显示了这个屏幕的模拟图。
图3 屏幕的模拟图
标题将使用本书前面定义的Font类和Text类创建。本书配套光盘中有一个字体叫做title font,这是48像素的字体,具有适合在视频游戏中使用的外形。将.fnt和.tga文件添加到项目中,并设置它们的属性,以便在生成项目时它们会被复制到bin目录中。
需要把字体文件加载到Form.cs文件中。如果要处理的字体很多,可能有必要创建一个FontManager类,但是因为我们只会使用一种或两种字体,所以可以把它们存储为成员变量。加载字体文件的代码如下。
private void InitializeTextures() { // Init DevIl Il.ilInit(); Ilu.iluInit(); Ilut.ilutInit(); Ilut.ilutRenderer(Ilut.ILUT_OPENGL); // Textures are loaded here. _textureManager.LoadTexture("title_font", "title_font.tga"); } Engine.Font _titleFont; private void InitializeFonts() { _titleFont = new Engine.Font(_textureManager.Get("title_font"), FontParser.Parse("title_font.fnt")); }
字体纹理在InitializeTextures函数中加载,当在InitializeFonts方法中创建字体对象时将使用这个纹理。
可以把标题字体传递到StartMenuState构造函数中。将下面的新的StartMenuState添加到Shooter项目中。
class StartMenuState : IGameObject { Renderer _renderer = new Renderer(); Text _title; public StartMenuState(Engine.Font titleFont) { _title new Text("Shooter", titleFont); _title.SetColor(new Color(0, 0, 0, 1)); // Center on the x and place somewhere near the top _title.SetPosition(-_title.Width/2,300); } public void Update(double elapsedTime) { } public void Render() { Gl.glClearColor(1, 1, 1, 0); Gl.glClear(Gl.GL_COLOR_BUFFER_BIT); _renderer.DrawText(_title); _renderer.Render(); } }
StartMenuState使用传入构造函数的字体创建标题文本。文本的颜色为黑色,水平居中显示。渲染循环将屏幕清除为白色,然后绘制文本。为了运行这个状态,需要把它添加到状态系统中,并设置为默认状态。
private void InitializeGameState() { // Game states are loaded here _system.AddState("start_menu", new StartMenuState(_titleFont)); _system.ChangeState("start_menu"); }
运行程序后,看到的结果如图4所示。
图4 渲染标题
这个阶段只是第一遍处理。在以后可以细化标题,使其更加美观。就现在而言,功能是最重要的。为了完成标题屏幕,需要添加开始和退出选项。这两个选项都是按钮,意味着需要创建一个按钮类。
按钮将在垂直列表中表示。任何时候,都会有一个按钮是选中的按钮。如果用户按下了键盘上的Enter键,或者游戏手柄上的A按键,那么当前选择的按钮将被按下。
按钮需要知道自己何时被选中,也就是获得焦点。它们还需要知道自己被按下时应该采取什么操作,这是一个非常适合使用委托的场合。在构造函数中可以向按钮传递一个委托,当按下按钮时就调用这个委托。按钮还需要设置自己位置的方法。使用代码表示对按钮的这些需求后,得到的类如下所示。Button类是可以重用的,所以可以把它添加到Engine项目中,以便将来的项目也可以使用该类。
public class Button { EventHandler _onPressEvent; Text _label; Vector _position = new Vector(); public Vector Position { get { return _position; } set { _position = value; UpdatePosition(); } } public Button(EventHandler onPressEvent, Text label) { _onPressEvent = onPressEvent; _label label; _label.SetColor(new Color(0, 0, 0, 1)); UpdatePosition(); } public void UpdatePosition() { // Center label text on position. _label.SetPosition(_position.X - (_label.Width / 2), _position.Y + (_label.Height / 2)); } public void OnGainFocus() { _label.SetColor(new Color(1, 0, 0, 1)); } public void OnLoseFocus() { _label.SetColor(new Color(0, 0, 0, 1)); } public void OnPress() { _onPressEvent(this, EventArgs.Empty); } public void Render(Renderer renderer) { renderer.DrawText(_label); } }
Button类不直接处理用户输入,相反,它依赖于使用它的代码向它传递相关的输入事件。OnGainFocus和OnLoseFocus方法根据焦点的状态修改按钮的外观,这样用户就可以知道当前选中了哪个按钮。当按钮的位置改变时,标签文本的位置也会更新并居中。EventHandler中包含当按下按钮时调用的函数,它描述了一个接受对象和事件参数枚举作为参数的委托。
玩家输入由Menu类检测,它通知相关按钮已被选中或按下。Menu类包含一个按钮列表,任何时候只有一个按钮可以拥有焦点。用户可以使用手柄或者键盘导航菜单。OnGainFocus和OnLoseFocus会修改按钮的标签文本,这样用户就可以知道当前哪个按钮拥有焦点。
获得焦点时,字体的颜色变为红色,否则字体的颜色为黑色。还有其他进行区分的方法,例如可以放大文本、修改背景图片或者使用补间显示或者删除其他某个值,但是现在只是第一遍编码,所以不需要进行这些改进。
菜单将把按钮垂直排列在一列中,所以将它命名为VerticalMenu很合适。