我已经数次提到了游戏控制台。近年来,通过游戏Quake和Half-Life暴露控制台给玩家,控制台变得越来越流行了。即使你不希望在你的游戏中允许玩家访问控制台,你也应该为了自己使用而纳入控制台到游戏中。控制台在游戏开发过程中是一个强大的工具。有了控制台,我们能以合理和可控的方式在游戏中显示处理流程信息和统计信息到屏幕上。我们可以通过在游戏中(on the fly)设置变量的值,并且可以立马看到效果,这样来调整游戏。如果在发行时我们决定不让玩家访问控制台,我们只要简单的不映射显示控制台的命令。
控制台将被定义成静态类。我们只想要一个控制台存在,并且我们想要它在整个游戏中存在。我们已经看到过一些控制台代码示例了,我们发送的任意异常数据都是发送到控制台的。通过使控制台是静态的,我们可以在游戏引擎任何地方访问它,而不需要取得控制台类的一个特定实例。
为了访问控制台,我们映射打开控制台的函数到键盘上的F12键。你可以选择键盘上的任意键,但是在本例中我选择F12键是因为每个现代键盘上都有这个键,并且它一般不会映射到其他的函数上。
定义控制台组件
在我们设计控制台之前,还需要定义一个保存命令信息的类。为了让控制台不只是一个空有其名如列表框一样的东东(For the console to be more than a glorified list box),我们需要它能够接受和执行命令。类GameCommand将为每个命令保存所需的信息。要记住控制台命令不能只限于游戏引擎本身的需要。任意使用此游戏引擎的游戏将可以添加他们的命令。类GameCommand将需要保存三个信息:命令名字符串;命令的帮助字符串,它解释命令的用法和语法;当命令被输入时需要调用的函数。此控制台处理的每一个命令都接受唯一的一个字符串参数。这样一来,控制台的处理就一致了,在命令后跟一个文本一起传递给命令函数(即使因为命令后不带文本参数的情况,也需要传一个空的字符串)。两个只读的熟悉提供了对命令和帮助字符串的外部访问接口。类的声明如列表2-21所示。
列表2.21:GameCommand 声明
类GameCommand的构造函数(如列表2-22所示)只是接受三个所需要的信息,并且把他们复制到私有成员中去。
列表2.22:GameCommand 构造函数
public GameCommand(string sCmd, string sHelp, CommandFunction pFunc) { m_sCommand = sCmd; m_sHelp = sHelp; m_Function = pFunc; } |
此类需要一个Execute方法,当控制台认出此命令被调用,则调用此方法。此方法的参数是键入的字符串除掉命令的那一部分。在调用此函数前,我们必须验证开发者是否当真提供了回调函数(delegate)。如果是,我们则调用此方法并传递给它一个字符串数据。解析这个字符串并且取得其期望的信息则是此函数的职责了。参加列表2 -23 C #代码的实现。
列表2.23:GameCommand Execute方法
public void Execute( string sData ) { if ( m_Function != null ) { m_Function( sData ); } } |
至此,我们已经定义怎么处理控制台命令了,是时候创建控制台本身了。控制台需要一张图片作为文本的背景。这不仅使控制台好看,还有一个目的就是确保控制台文本和文本后的图片有非常好的对比度。因此控制台的图片则需要相当暗,因为准备为文本使用亮色。无论背景色如何变化,控制台都会保持正面(Any variations in the background color should be kept over to the right side of the console),因为文本很少布满屏幕。
我们可能仅仅当控制台被打开时在屏幕上弹出(pop)它,控制台被关闭时,清除它。虽然这是最简单的解决方案,但是如果在屏幕上显示[刮风1] 控制台会更好看些。对于这个控制台,我们在控制台被打开时从屏幕顶端滑出它来显示它,当它被关闭时又滑回去。控制台覆盖多少屏幕空间是可以调整的。我将设定控制台打开时默认覆盖一半的屏幕空间,稍后提供调整控制台尺寸的方法。
定义控制台类
类Console的属性如列表2-24所示。你将会注意到我们使用了两个不同的容器类。ArrayList类是一个简单的动态数组容器,适合存储在控制台显示的文本行。SortedList类会有一点点复杂。它存储着对象,每个对象与一个关键字关联,并且排序,可以通过关键字折半查找(binary search)来定位对象。使用了两个SortedList容器,每一个都保持了GameCommand类的实例。一组是控制台理解的命令,另一组是控制台能修改的参数。在实际中,参数将。。。(In reality, the parameters will just be more methods that happen to set a variable)。类Console的属性如列表2-24所示。
列表2.24:Console 类声明
public class Console { #region Attributes private static bool m_bVisible = false; private static bool m_bOpening = false; private static bool m_bClosing = false; private static ArrayList m_Entries = new ArrayList(); private static SortedList m_Commands = new SortedList(); private static SortedList m_Parameters = new SortedList(); private static GraphicsFont m_pFont = null; private static StringBuilder m_Entryline = new StringBuilder(); private static Image m_Image; private static float m_Percent = 0.0f ; private static float m_MaxPercent = 0.50f ; #endregion |
你首先注意到的应该是Console属性都是静态的。这支持了我们的设计决策,在应用程序中只存在一个控制台。你应该也注意到了,实体行(entry line)变量是一个StringBuilder对象,而不是一个字符串。在C#中字符串在创建之后不能更改。任何更改操作实际上会创建原始字符串的拷贝并且返回经过适当修改的字符串。StringBuilder类是为随时间变化的字符串而设计的。因为实体行每一次创建一个字符,所以我们需要这么做。
构造函数是此类唯一的非静态方法。它用提供的字体和背景图片初始化控制台。控制台的初始化的要重复的一部分代码,放在了Reset方法里。构造函数和Reset方法如列表2-25所示。
列表2.25:Console构造函数和Reset方法
public Console( GraphicsFont pFont, string sFilename) { m_pFont = pFont; m_Image = new Image( sFilename );
Reset();
AddCommand("SET", "Set a paramter to a value", new CommandFunction(Set)); AddCommand("HELP", "Display command and Parameter help", new CommandFunction(Help)); AddParameter("CONSOLESIZE", "The percentage of the screen covered by the console from 1.0 to 100.0", new CommandFunction(SetScreenSize)); }
public static void Reset() { m_Entries.Clear();
AddLine("type 'Help' or 'help command' for command descriptions");
m_Entryline = new StringBuilder(">");
} |
保持字体是为了后面渲染文本到控制台。背景图片是使用Image类来创建的,这个类我们应该已经很熟悉了。然后我们在控制台打印一行告诉玩家有帮助可以使用。当我们看到命令解析器时,我们会实现帮助。输入HELP 会使完整的命令列表显示在控制台。输入HELP 加上命令名会使与之关联的命令帮助字符串显示在控制台。输入行然后显示输入提示字符。最后需要做的是添加命令,这是硬编码到控制台的。CONSOLESIZE命令用来调整控制台大小。Set命令用来设置参数。
渲染控制台
现在我们来看看使用Render方法渲染控制台。这是另一个很长的方法,所以我们一次看一部分。此方法首先做的事情是获取无效状态并且关掉它。然后检查控制台当前是否需要显示。如果可见标志没有设置,Render方法不渲染任何东西就返回。如果控制台可见,我们必须确定控制台要显示多大。如果控制台正在打开,我们递增控制台的可见百分比。如果它正在关闭,我们递减它的可见百分比。一旦控制台完全关闭,可见标志就会被清除。此方法的开始部分如列表2 -26a 所示。
列表 2.26a :控制台Render方法
public static void Render() { if ( m_bVisible ) { bool fog_state = CgameEngine.Device3D.RenderState.FogEnable; CgameEngine.Device3D.RenderState.FogEnable = false; // Determine how much of the console will be visible based on // whether it is opening, // open, or closing. if ( m_bOpening && m_Percent <= m_MaxPercent ) { m_Percent += 0.05f ; } else if ( m_bClosing && m_Percent >= 0.0f ) { m_Percent −= 0.05f ; if ( m_Percent <= 0.0f ) { m_bClosing = false; m_bVisible = false; } } |
渲染控制台的下一步是渲染可见部分的背景图片。为了达到当控制台打开和关闭时滚动的效果,我们将调整定义控制台矩形的顶点。当控制台打开时,矩形将从屏幕顶端垂直移动下来。除了调整得到滚动效果,其他的和像渲染splash screen。代码如列表2-26b所示。
注解:记得到目前我们已经在其他的类里用过catch代码,我们总是错误信息文本发送到控制台以显示。但是如果控制台本身出现错误,再发送到控制台就不会有用了。我们改为使用微软在System.Diagnostics名字空间提供的调试类发送文本到Developer Studio输出窗口。
列表2.26b:控制台Render方法
// Render the console background. try { int line = (int)((m_Percent*CGameEngine.Device3D.Viewport.Height) - 5 m_pFont.LineHeight);
if ( line > 5 ) { // Draw the image to the device. try { CustomVertex.TransformedTextured[] data = new CustomVertex.TransformedTextured[4]; data[0].X = CGameEngine.Device3D.Viewport.Width; data[0].Y = 0.0f - ( 1.0f -m_Percent)* CGameEngine.Device3D.Viewport.Height; data[0].Z = 0.0f ; data[0].Tu = 1.0f ; data[0].Tv = 0.0f ; data[1].X = 0.0f ; data[1].Y = 0.0f - ( 1.0f -m_Percent)* CGameEngine.Device3D.Viewport.Height; data[1].Z = 0.0f ; data[1].Tu = 0.0f ; data[1].Tv = 0.0f ; data[2].Y = CGameEngine.Device3D.Viewport.Height - ( 1.0f - m_Percent)*CGameEngine.Device3D.Viewport.Height; data[2].Z = 0.0f ; data[2].Tu = 0.0f ; data [2],TV = 1.0f ; data[3].X = 0.0f ; data[3].Y = CGameEngine.Device3D.Viewport.Height - ( 1.0f -m_Percent)*CGameEngine.Device3D.Viewport.Height; data[3].Z = 0.0f ; data[3].Tu = 0.0f ; data[3].Tv = 1.0f ;
VertexBuffer vb = new VertexBuffer( typeof(CustomVertex.TransformedTextured), 4, CGameEngine.Device3D, Usage.WriteOnly, CustomVertex.TransformedTextured.Format, Pool.Default );
vb.SetData(data, 0, 0);
CGameEngine.Device3D.SetStreamSource( 0, vb, 0 ); CGameEngine.Device3D.VertexFormat = CustomVertex.TransformedTextured.Format; CGameEngine.Device3D.RenderState.CullMode = Microsoft.DirectX.Direct3D.Cull.Clockwise;
// Set the texture. CGameEngine.Device3D.SetTexture(0, m_Image.GetTexture() );
// Render the face. CGameEngine.Device3D.DrawPrimitive( PrimitiveType.TriangleStrip, 0, 2 ); } catch (DirectXException d3de) { Console.AddLine( "Unable to display SplashScreen " ); Console.AddLine( d3de.ErrorString ); } catch ( Exception e ) { Console.AddLine( "Unable to display SplashScreen " ); Console.AddLine( e.Message ); } |
至此控制台背景已经被渲染,准备加入文本。一个line变量用来指定每一行文本被绘制的垂直位置。因为控制台是从屏幕顶点掉落下来,我们将从顶端至上绘制控制台里的文本,直到超出控制台的空间或者显示的文本行数。第一行文本将被绘制在底端5像素加字体高度的地方。这会在屏幕底端给我们留有一个小的页边距。文本只有在这一行之上5像素的距离才会被绘制。我们绘制输入行作为开始,因为它总是控制台的最底端的一行。绘制这一行后,我们把line变量减掉字体的高度,以准备绘制下一行。然后对文本数组m_Entries里的每一项重复这一过程。此外,只有在控制台还有足够的空间包含5个像素的行距我们才绘制它。列表2 -26c 完成了Render方法的代码。
列表 2.26c :Console Render方法(结束)
m_pFont.DrawText( 2, line, Color.White, m_Entryline.ToString() ); line -= (int)m_pFont.LineHeight; foreach ( String entry in m_Entries ) { if ( line > 5 ) { m_pFont.DrawText( 2, line, Color.White ,entry); line -= (int)m_pFont.LineHeight(); } } }
} catch (DirectXException d3de) { Debug.WriteLine("unable to render console"); Debug.WriteLine(d3de.ErrorString); } catch ( Exception e ) { Debug.WriteLine("unable to render console"); Debug.WriteLine(e.Message); }
CGameEngine.Device3D.RenderState.FogEnable = fog_state; } } |
为Console定义额外的方法
至此,我们可以创建和渲染控制台了,我们还需要考虑与控制台交互的各种方法。开始的两个方法是两个版本的SetMaxScreenSize。第一个版本接受一个浮点值作为参数,用来编程设置控制台的尺寸。第二个版本接受一个字符串作为参数,用来作为set函数的参数,通过在控制台本身使用。在两个方法中,我们都需要小心屏幕的尺寸要在10到100的百分比有效范围内。这些函数如列表2-27所示。
列表2.27:Console SetMaxScreenSize和SetScreenSize方法
public void SetMaxScreenSize ( float fPercent ) { if ( fPercent < 10. of ) fPercent = 10.0f ; if ( fPercent > 100. 0f ) fPercent = 100.0f ; m_MaxPercent = fPercent / 100.0f ; }
public void SetScreenSize( string sPercent ) { float f; try { f = float.Parse(sPercent); } catch { f = 50.0f ; } SetMaxScreenSize ( f ); } |
我们使用下面的几个方法在打开的控制台里来添加文本,以及处理击键。列表2-28所示的AddLine方法用来把文本放到控制台显示。此方法在字符串数组m_Entries的开头插入一行文本。此数组只会保存最近的50行文本,发送到控制台。当第51行插入到数组的头部时,数组的最后一项被移除。
列表2.28:Console AddLine方法
public static void AddLine( string sNewLine ) { m_Entries.Insert(0,sNewLine); if ( m_Entries.Count > 50 ) { m_Entries.RemoveAt(50); } System.Diagnostics.Debug.WriteLine(sNewLine); } |
CD3DApplication类的OnKeyDown消息处理器调用下面的三个方法。如果收到KeyDown消息,这个方法要调用四个方法中一个。前三个方法用于数据条目,第四个方法控制控制台的打开状态。我们简单的看看这个方法。AddCharacterToEntryLine方法取得一个字符,并且追加它到输入行中,如果控制台当前是显示的话。当输入的字符是字母、数字、空格或者小数点,这个方法被调用。如果玩家按了Backspace键,Backspace方法被调用把输入行最后一个字符删除。如果按下了Enter键,便会处理输入行,并执行找到的命令。这是由ProcessEntry方法处理的,此方法把输入行从提示符那里分离出命令部分来。然后名字进入到控制台文本列表里,并且传递给ParseCommand方法来处理。然后输入行被重置为输入提示符,准备开始再一次接受文本。这些数据条目的方法如列表2-29所示。
列表2.29:Console 数据条目方法
public static void AddCharacterToEntryLine( string sNewCharacter ) { if (m_bVisible) m_Entryline.Append(sNewCharacter); }
public static void Backspace() { if ( m_Entryline.Length > 1 ) { m_Entryline.Remove(m_Entryline.Length-1,1); } public static void ProcessEntry() { string sCommand = m_Entryline.ToString().Substring(1,m_Entryline.Length-1); AddLine(sCommand); m_Entryline.Remove(1,m_Entryline.Length-1); ParseCommand(sCommand); } |
ParseCommand方法(如列表2-30所示)把命令字符串切分为两块。开始使用string类的Trim方法去掉字符串头尾的空格。任何多余的空格都可能使解析器迷惑。然后定位到字符串的第一个空格处。如果这是一个空格,那么我们知道需要把字符串拆分成两块。如果不存在空格,那么这个字符串是一个不包含数据的命令,我们将ProcessCommand方法的数据参数传入null。
列表2.30:Console ParseCommand方法
private static void ParseCommand( string sCommand ) { // Remove any extra white space. sCommand.Trim();
// Find the space between the command and the data (if any), int nSpace = sCommand.IndexOf(" ");
// Is there any data? if ( nSpace > 0 ) { string sCmd = sCommand.Substring(0,nSpace); string sData = sCommand.Remove(0,nSpace+1); ProcessCommand( sCmd, sData ); } else { ProcessCommand( sCommand, null ); } } |
ProcessCommand方法(如列表2-31所示)在m_Commands列表里二分查找,看是否有输入的命令存在列表中。如果命令没有被找到,“Unrecognized Command”消息被发送的控制台。如果找到命令,我们取得GameCommand的引用,调用它的Execute函数。部分输入的字符串被传递给注册的委托函数(The data portion of the entered string is forwarded on to the registered delegate function)。
列表2.31:Console ProcessCommand方法
private static void ProcessCommand (string sCmd, string sData) { int nIndex = m_Commands.IndexOfKey( sCmd ); if ( nIndex < 0 ) // Not found { AddLine("Unrecognized Command"); } else { GameCommand Cmd = (GameCommand)m_Commands.GetByIndex(nIndex); Cmd.Execute(sData); } } |
这就是所有的处理控制台命令的了。现在我们要考虑一下怎么管理控制台命令和参数。下面的几个方法用来增加和删除命令和参数。我们之前看到过Add方法的调用了。现在我们要看看他们到底在做什么。AddCommand方法接受创建GameCommand对象需要的数据作为参数。使用这些信息,创建新的GameCommand对象并以命令名字符串作为主键加入到m_Commands列表。从列表里移除一个命令也同样简单易懂。使用提供的字符串作为关键字搜索列表。如果存在(即返回的索引值非负),此条目将从列表中删除。添加和移除命令的代码如列表2-32所示。
列表2.32:Console 命令方法
public static void AddCommand( string sCmd, string sHelp, GameCommand.CommandFunction Func) { GameCommand Cmd = new GameCommand (sCmd, sHelp, Func); m_Commands.Add(sCmd, Cmd); public static void RemoveCommand( string sCmd ) { int nIndex = m_Commands.IndexOfKey(sCmd); if (nIndex >= 0) { m_Commands.RemoveAt(nIndex); } } |
AddParameter和RemoveParameter方法(见列表2-33)同理。唯一的重大区别是操作的列表不一样。
列表2.33:Console参数方法
public static void AddParameter( string sParam, string sHelp, GameCommand.CommandFunction Func ) { GameCommand Cmd = new GameCommand(sParam, sHelp, Func); m_Parameters.Add (sParam, Cmd); }
public static void RemoveParameter( string sParam ) { int nIndex = m_Parameters.IndexOfKey(sParam); if ( nIndex >= 0 ) { m_Parameters.RemoveAt(nIndex) ; } } |
下面的两个方法(见列表2-34)用来改变和检测控制台可见状态。ToggleState当F12键被按下就调用。如果控制台当前可见,Opening/Closing标志对被设置,使窗口开始慢慢关闭。相反的,如果控制台是关闭的,这个标志设置后会使控制台可见,开始慢慢打开。IsVisible属性提供其他方法测试控制台是否打开。例如本章开始讲的GameInput类。Poll方法中的action map代码段是位于一个if语句中的,如果控制台是打开状态,那么将不会处理action map。如果我们在控制台中输入命令,我们不会想这些击键动作也被当作游戏控制命令来处理。There’s no telling what state the game would be in when we finally close the console。当控制台打开时也被用来自动暂停游戏。
列表2.34:Console ToggleState 方法和 IsVisible属性
public static void ToggleState() { if ( m_bVisible ) { m_bClosing = true; m_bOpening = false; } else { m_bOpening = true; m_bVisible = true; } }
public static bool IsVisible() { get { return m_bVisible; } } |
为控制台添加帮助功能
对于Console类我们还剩两个方法要讨论。在构造里添加到命令列表里的两个命令函数:Help(见列表2-35)和Set(见列表2-36)。Help方法根据命令提供的数据有几种不同的方式。如果没有数据,此方法会显示所有命令的列表。如果是一个已知的命令或者参数名作为数据,将会显示这个命令或者参数的帮助字符串。如果既不是已知的命令或者参数,将会报告数据不可知。有一个特例是最后这个方法。如果命令是Set命令,和Set命令相关联的所有已知的参数和帮助字符串被显示。
列表2.34:Console Help方法
private void Help( string sData ) { StringBuilder sTemp = new StringBuilder(); { AddLine("Valid Commands"); foreach ( string sCmds in m_Commands.Keys ) { sTemp.Append(sCmds); sTemp.Append (" "); if ( sTemp.Length > 40 ) { AddLine(sTemp.ToString()); sTemp.Remove(0,sTemp.Length-1); } } if ( sTemp. Length > 0 ) { AddLine(sTemp.ToString()); } } else { int nIndex = m_Commands.IndexOfKey( sData );
if ( nIndex < 0 ) // Not found { nIndex = m_Parameters.IndexOfKey( sData ); if ( nIndex < 0 ) // not found { AddLine("Unrecognized Command"); } else { GameCommand Cmd = (GameCommand)m_Parameters.GetByIndex(nIndex); string sHelpText = sData + " − " + Cmd.GetHelp(); AddLine(sHelpText); } } else { GameCommand Cmd = (GameCommand)m_Commands.GetByIndex(nIndex); string sHelpText = sData + " − " + Cmd.GetHelp(); AddLine(sHelpText); } } { AddLine("Valid Parameters"); foreach ( string sCmds in m_Parameters.Keys ) { sTemp.Append(sCmds); sTemp.Append (" "); if ( sTemp.Length > 40 ) { AddLine(sTemp.ToString()); sTemp.Remove (0, sTemp.Length-1) ; } } if ( sTemp.Length > 0 ) { AddLine(sTemp.ToString()); } } } |
列表2.36:Console Set方法
private void Set( string data ) { StringBuilder sTemp = new StringBuilder();
int nSpace = data.IndexOf(" ");
if ( nSpace > 0 ) { string sCmd = data. Substring(0, nSpace); string sData = data.Remove(0,nSpace+1); int nIndex = m_Parameters.IndexOfKey( sCmd );
if ( nIndex < 0 ) // Not found { AddLine( "Unrecognized Parameter"); } else { GameCommand Cmd = (GameCommand)m_Parameters.GetByIndex(nIndex); Cmd.Execute( sData ); } } } |
Set方法(见列表2-36)用来修改参数值。为达到这个,它必须首先需要识别修改的参数。这个数据字符串被解析分解成一个参数名和一个数据字符串。用参数名去参数列表里查询。如果参数找到了,使用字符串的数据部分来调用它的相关函数。
[刮风1]显示,而不是前面的弹出(pop)