C#有比Console.Write()更快的方法吗?

20

我正在制作一个游戏,使用Console.Write()重新绘制游戏场景不太好,有没有更快的方法可以重新绘制整个场景而不会让它看起来"卡顿"? 游戏场景中几乎所有的东西都在移动,但只有在与0不同的元素处才有物体...(如果我的描述不足以说明问题,您可以在http://pastebin.com/TkPd37xD中查看完整代码)

for (int Y = 0; Y < playfield.GetLength(0); Y++)
{
    for (int X = 0; X < playfield.GetLength(1); X++)
    {
        //destroying the row when it reaches the top
        if (playfield[0, X] != 0)
        {
            for (int i = 0; i < playfield.GetLength(1); i++)
            {
                playfield[0, X] = 0;
                Console.SetCursorPosition(X, 0);
                Console.Write(" ");
            }
        }
        if (playfield[Y, X] == 3)
        {
            playfield[Y - 1, X] = 3;
            playfield[Y, X] = 0;
        }
        else if (playfield[Y, X] == 1)
        {
            Console.SetCursorPosition(X, Y - 1);
            Console.Write("=");
            playfield[Y - 1, X] = 1;
            Console.SetCursorPosition(X, Y);
            Console.Write(" ");
            playfield[Y, X] = 0;
        }
        else if (playfield[Y, X] == 0)
        {
            Console.SetCursorPosition(X, Y);
            Console.Write(" ");
        }
    }
}

10
只需重绘发生变化的部分,而不是整个屏幕。 - Panagiotis Kanavos
我以为我在做这件事情,但显然我并不是,刚刚我注意到我最后一个“Else if”是不必要的,因为当 == 1 时我正在执行相同的操作,我删除了最后一个“Else if”,现在看起来好多了! :) - Darkbound
你尝试使用StringBuffer了吗? - Caramiriel
原始的Windows控制台可能一开始并不是最快的。 - Peter - Reinstate Monica
3个回答

22

基本上有两种方法:减少渲染和加快渲染速度。

减少渲染通常更棘手,但也往往不那么密集。经典的例子是Carmack的Keen游戏-PC没有勇气一次性重新渲染整个屏幕,因此Carmack确保只重新绘制实际更改的屏幕部分。在您的情况下,这可以简单地检查新屏幕与旧屏幕(当然不使用Console方法)之间的差异-根据您编写的游戏类型,这可以为您节省大量工作。

通常情况下,更快的渲染更容易实现。以前的常见方法是直接访问输出缓冲区 - 与将游戏场景存储在单独的内存中不同,您可以直接在图形卡中访问它 - 这样可以很容易地重新绘制整个屏幕,当然,否则您几乎看不到CRT屏幕上的任何内容。这个选项仍然可以作为向后兼容性访问,因此如果您使用Turbo Pascal编写应用程序,则仍然可以使用它,但在C#中并不是那么容易访问。渲染整个屏幕有一个选项是先在StringBuilder中渲染,然后一次性使用Console.Write输出。这会快得多,但并不完美。使用char[]会使性能提高一个额外的点 - 您可以将游戏场景直接表示为char[][],然后每次更改时不必重新创建StringBuilder - 您只需要为每个游戏场景行执行一次Console.Write即可。

当然,你可以在更改发生时直接编写更改内容;这取决于你正在编写的游戏,这可能从“简单但效果显著”到“相当困难且不理想”。由于控制台的缓冲区域可以比窗口尺寸更大,因此甚至可以将其绘制到缓冲区的隐藏部分,然后使用Console.MoveBufferArea一次性绘制整个更改-这通常称为“后备缓冲”。不过我不确定它的外观是否良好-控制台窗口现在允许您在缓冲区中滚动,这可能会对您的用例造成负面影响。

还有一些方法可以更快地访问控制台缓冲区,但是不能完全依靠 .NET - 你需要使用 P/Invokes。这个主题的一个很好的答案在这里 - 如何快速编写彩色控制台输出?。在现代系统上,这几乎相当于使用后备缓冲区并一次性“绘制”所有内容 - 这非常快。而且,您可以直接将后备缓冲区用于游戏数据 - 这在20-30年前就行得通,今天仍然有效;它是玩弄有限资源的好方法。您能否编写一个只使用控制台文本缓冲区来实现所有功能,或者至少几乎所有功能的游戏?像这样尝试各种东西非常有趣;您可以编写一整套这样的游戏,包括俄罗斯方块或洛德跑酷等游戏。当然,这只适用于 Windows,因此如果您想支持其他系统,则会更加棘手。
最后,你可以编写自己的控制台(或更好地使用已经编写和测试过的控制台)。如果你想逐渐挑战更强大的技术,这是一个很好的实践,它将使你有机会尝试更强大的技术。典型的例子是像矮人要塞这样的游戏-仍然基于文本,仍然类似于控制台,但实际上是用SDL等技术绘制图形。这不仅在现代系统上速度更快(因为你没有直接访问文本缓冲区的简单方法),而且还可以轻松地转换到图形平铺游戏选项。这是通向酷炫东西的另一个阶梯 :))

3

这个不完整的答案关于如何快速在控制台输出彩色文本?(不能实现颜色),对于全窗口更新非常快,仅需要约0.8毫秒来处理标准大小为120x30的System.Console窗口:

int cols = Console.WindowWidth, rows = Console.WindowHeight;
//some sample text
byte[] buffer = Enumerable.Repeat((byte)'=', cols * rows).ToArray();
//because output appends, ensure the window is reset
Console.SetCursorPosition(0, 0);
using (Stream stdout = Console.OpenStandardOutput(cols * rows)) {
    stdout.Write(buffer, 0, buffer.Length);
}

这是我调用Write 1000次的性能表现:

enter image description here


如果您愿意进行p-invoke,可以像this other answer中所示使用CharInfoWriteConsoleOutput实现颜色。这两种方法都适用于“后备缓冲”(即构建整个场景然后显示)。

更新:以下是我处理颜色的方法(另请参见):

[StructLayout(LayoutKind.Explicit, CharSet=CharSet.Unicode)]
public struct CharUnion {
    [FieldOffset(0)] public char UnicodeChar;
    [FieldOffset(0)] public byte AsciiChar;
}

[StructLayout(LayoutKind.Explicit, CharSet=CharSet.Unicode)]
public struct CharInfo{
    [FieldOffset(0)] public CharUnion Char;
    [FieldOffset(2)] public ushort Attributes;

public ConsoleColor ForegroundColor => (ConsoleColor)((this.Attributes & 0x0F));
public ConsoleColor BackgroundColor => (ConsoleColor)((this.Attributes & 0xF0) >> 4)

    public CharInfo(char character, ConsoleColor? foreground = null, ConsoleColor? background = null) {
        this.Char = new CharUnion() { UnicodeChar = character };
        this.Attributes = (ushort)((int)(foreground ?? 0) | (((ushort)(background ?? 0)) << 4));
    }
    public CharInfo(byte character, ConsoleColor? foreground = null, ConsoleColor? background = null) {
        this.Char = new CharUnion() { AsciiChar = character };
        this.Attributes = (ushort) ((int)(foreground ?? 0) | (((ushort)(background ?? 0)) << 4));
    }

    public static bool Equals(CharInfo first, CharInfo second) {
        return first.Char.UnicodeChar == second.Char.UnicodeChar
            && first.Char.AsciiChar == second.Char.AsciiChar
            && first.Attributes == second.Attributes;
    }
}

0

另一种选择是使用链接中指定的API调用https://msdn.microsoft.com/en-gb/library/windows/desktop/aa363362%28v=vs.85%29.aspx

[DllImport("kernel32.dll")]
static extern void OutputDebugString(string lpOutputString);

您还可以通过将循环中的输出缓冲到 StringBuilder 中,并在每个循环后输出来获得轻微的性能提升。这取决于您想要实现的程序目标,可能并非必需。


2
不需要那样做,托管的 Debug.WriteLine 在托管环境中可以完成同样的工作。 - Blindy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接