手工制作的Hero Mac OS平台层,第2天:打开NSWindow

用Cocoa框架编译Clang

如果您尝试运行我们已经设置的build.sh脚本,它将失败。 这是因为,如果我们在osx_main.mm文件和构建脚本中均未包含Apple的AppKit框架,则所有这些都无法编译。

在包含stdio.h的行下,添加以下行

  #include  

现在,打开build.sh文件。 您想插入一个命令来告诉clang您计划使用AppKit框架进行编译。

通常,您可以通过添加-framework选项来实现,例如

 框架AppKit 

但是,如果这样做,则可以开始看到compile命令变得非常冗长。 将来,我们将使用更多的Apple框架,如果我们继续添加该行,将很难看到clang命令在做什么。

那么,为什么不提取-framework选项并将它们放在shell脚本变量中,以便将它们全部放在一个位置? 我们称它们为链接器标志,或简称为OSX_LD_FLAGS(lld是链接器)。

在您回显“ Building Handmade Hero”的行之后,将以下行添加到build.sh脚本中

  OSX_LD_FLAGS =“-framework AppKit” 

这将创建Shell脚本稍后将使用的变量。

这里有一个小警告,这有点使我早些不高兴。 在shell脚本中创建变量时, 间距很重要 。 如果您在第一个单词和等号之间有一个空格,则意味着与您没有空格时有所不同,并且该行也不会被解释为变量。

现在,您只需要在compile命令中引用链接器标志变量即可。 在“ -g”之后添加它,这样您的clang命令如下所示。

 铛-g $ OSX_LD_FLAGS -o手工../handmade/code/osx_main.mm 

美元符号仅引用变量。

您可以看到一旦开始添加更多Mac OS框架,这将如何使我们的生活更轻松。 如果我们决定添加Cocoa或其他东西,我们可以将其添加到上面的变量中,而无需使用实际的编译命令。

现在是关键时刻。 现在是时候通过简单的运行循环来构建和运行游戏了。

您已经知道如何执行此操作,因此我不会感到困惑。 运行您的shell脚本!

创建一个窗口(实际上会一直存在)

只要您的Shell脚本成功编译了运行循环且没有错误,您就可以顺利进行。 如果愿意,您可以运行游戏,但这实际上不会做任何事情,因为它基本上是一个无限循环,您不能退出。

您已经解决了立即退出游戏的问题。 真好。 但是现在您希望您的游戏在运行此无限循环时实际执行某些操作。 让我们从创建一个持久窗口开始。

您能猜出我们将用来创建窗口的类的名称吗? 您想打赌它以NS开头多少?

对。 NSWindow是我们要使用的东西。 我们要使用initWithContentRect:styleMask:backing:defer:构造函数创建一个。

在运行循环之前,输入以下内容作为占位符

  NSWindow *窗口= [[NSWindow分配] initWithContentRect: 
styleMask:
后盾:
推迟:];

根据Apple的文档,我们需要内容rect,样式掩码(具有某些选项的位字段,可以将OR或在一起),后备存储类型以及是否要推迟创建窗口设备。

确定显示窗口的位置(内容矩形)

让我们从内容矩形开始。 这就是苹果对此要说的。

屏幕坐标中窗口内容区域的原点和大小。 请注意,窗口服务器将窗口位置坐标限制为±16,000,大小限制为10,000

首先,窗口服务器到底是什么? 挖了一点点(大部分都埋在历史和档案中),但我收集到它是一个全局应用程序,Mac OS运行该全局应用程序来管理显示当前打开的每个应用程序的窗口(即大小,位置,内容更新)等)

因此,考虑到这一点,苹果不希望您将窗口放置在没有映射到任何现实屏幕的地方,或者它们的窗口服务器进程无法应付的大得可笑的地方似乎是合乎逻辑的。 保持简单的孩子。

苹果如何定义屏幕坐标?

窗口必须位于屏幕自己的坐标系中,但是如果您不知道该坐标系是如何工作的,那么您或多或少都在猜测。 是从上到下还是从下到上?

这是一个有趣的事实,会让大多数iOS开发人员感到困惑。 Mac上的屏幕坐标系是iOS的反向版本,这意味着它是自下而上运行的。

因此,如果您说“将该窗口置于y位置0”,则实际上是在告诉系统将窗口的下角放在屏幕的底部。 您添加到y位置的所有内容都会向上移动窗口。

如果超出任何轴上的边界,则窗口将仅在屏幕的远端显示。 将y坐标设置为负数会将窗口置于屏幕底部,因为OS始终渲染该窗口,因此从一开始就完全可见。

如何在屏幕中间渲染窗口

假设我们要在屏幕中间渲染窗口。 我们将如何处理?

首先,我们需要确定屏幕的宽度和高度。 您可以通过使用NSScreen类来获取屏幕的宽度和高度。

在主函数顶部添加以下行

  NSRect screenRect = [[NSScreen mainScreen]框架]; 

这为您提供了与用户屏幕相对应的矩形。

接下来,我们需要确定要用于窗口的大小。 首先,让我们使用1024 x 768 (大致与iPad大小相同)。

我们很有可能会重用此窗口的高度和宽度,因此让我们为其创建全局变量。

在主要功能上方添加以下几行

 静态浮动GlobalRenderWidth = 1024; 
静态浮点数GlobalRenderHeight = 768;

现在,我们只需要计算游戏窗口的底角,即可将其放置在屏幕中间。

那就是屏幕的高度减去窗口的高度乘以2。 这也适用于宽度。

说完所有内容后,为您的窗口生成initialContentRect的代码应如下所示

大! 那是一个窗口设置变量。 让我们快速解决其他问题。

设置样式蒙版

如果您阅读NSWindowStyleMask的文档,将会看到很多不同的选项。 其中一些适用于游戏。 其他人没有。

您可能想要一个可以关闭,调整大小和最小化的窗口。 如果窗口可以显示游戏标题,那也很好。

这给了我们需要设置的四个位字段。 您可以像这样直接在NSWindow初始化程序中对它们进行OR操作

  styleMask:NSWindowStyleMaskTitled | 
  NSWindowStyleMaskClosable | 
  NSWindowStyleMaskMiniaturizable | 
  NSWindowStyleMaskResizable 

如果您以前没有使用过位字段,那么使用一组选项是一种紧凑的方法。 不必将每个选项存储在单独的变量中(从而降低效率和速度),您可以使用单个无符号整数,然后在其上设置位。 将它们进行或运算意味着您要启用所有指定的选项。

NSBackingStoreType

苹果过去曾为此提供多种选择,但似乎他们已经简化了api。 唯一不建议使用的选项是NSBackingStoreBuffered,因此我们将继续讨论。

我们确实计划将游戏内容渲染到缓冲区中。

延后旗

这就是苹果对此要说的

指定窗口服务器是否立即为窗口创建窗口设备。 如果为true ,则窗口服务器将推迟创建窗口设备,直到窗口在屏幕上移动为止。 发送到窗口或其视图的所有显示消息都被推迟到创建窗口时才将其移动到屏幕上。

我要说的是我们不想推迟任何显示消息。 让我们选择立即选项,并将其设置为“否”。

最终设置并显示窗口

在构建和运行游戏之前,我们还要做几件事。

首先,让我们为窗口提供红色背景,以帮助我们在进入教程的那部分后调试游戏的显示缓冲区。

在创建窗口的行之后添加以下行

  [window setBackgroundColor:NSColor.redColor]; 

优秀! 现在,给窗口命名。

  [window setTitle:@“手工英雄”]; 

最后,让操作系统显示窗口

  [window makeKeyAndOrderFront:nil]; 

在Mac OS中,键窗口是接收键盘事件的窗口。 我怀疑它也会收到其他事件,但是通过在NSWindow上调用此函数,您正在设置游戏窗口,使其可以通过键盘或游戏板进行控制。

作为参考,下面是所有说完之后创建窗口的代码部分的外观(是的,我正在使用vim使其变得更老旧了)。

请记住,这是主运行循环之前 ,而不是在其内部或内部。

窗口关闭时关闭应用程序

继续并运行您的构建脚本,然后运行游戏。 您应该在屏幕中间看到一个红色窗口,该窗口与我们之前设置的全局宽度和高度匹配。

您可以调整大小,最小化,移动和关闭此窗口。 但是,您会注意到,如果关闭窗口,该应用程序不会随之关闭。 让我们修复它。

退出游戏

为了响应关闭的窗口而退出游戏,我们首先需要知道如何退出游戏。

似乎我们想调用一些高级操作系统命令,但实际上比这要简单得多(特别是因为我们已经控制了主运行循环)。

请记住,早些时候,该应用程序将立即退出我们,因为我们没有运行循环。 好吧,如果您从那个循环中挣脱出来怎么办?

这意味着我们要替换它,

  while(true){ 
//主循环
}

引用我们可以控制的全局变量

 在跑步的时候) { 
//主循环
}

运行中的全局”将默认为true,然后在每次要退出游戏时将其设置为false。

在GlobalRenderHeight和GlobalRenderWidth变量下面声明另一个静态变量

 静态布尔运行=真 

然后,使用Running全局变量替换while循环中的条件。 这将主运行循环与正在运行的全局变量联系在一起。

但是,等等,全局变量不是一件坏事吗?

如果只有少数几个用于某些高级事件处理,则不会。 在这种情况下,我们没有其他选择,因为主函数只有一个入口点。 我们无法调用其他代码来退出主运行循环。

同样,您通常希望避免在编程中抱有任何教条。 总是有例外,大多数“最佳实践”都应受到质疑。 当然,出现“无全局变量”教条是有原因的,但这并不意味着我们不应该使用全局变量。

将窗口的关闭事件绑定到游戏的主运行循环

现在,您已经具备了在关闭主窗口时结束游戏的结构,您只需要弄清楚单击那个红色小x会调用什么代码。

除非有人告诉您,否则这是您不知道要搜索的另一件事(如果您以前为iOS构建应用程序或与Objective-C一起使用,则可能会这样)。

在这种情况下,我们正在寻找的对象是实现NSWindowDelegate协议的某个对象。 实际上,没有默认对象可以执行此工作。 我们必须创造自己。

因此,让我们继续阅读NSWindowDelegate文档。

它说NSWindowDelegate是方法的集合,这些方法对在游戏/应用程序中存在于NSWindow生命周期中的各种事件做出响应。

啊,天哪,有很多NSWindow事件。

目前,我们仅对这些事件之一感兴趣,那就是windowWillClose:事件。 一直向下滚动到“ 关闭Windows”部分,您将看到它。

windowWillClose:单击窗口中的红色x时被调用。

创建一个NSWindowDelegate对象

太酷了,既然您知道要查找的方法,那么如何确保关闭窗口时调用代码?

您可以通过创建自定义对象来实现NSWindowDelegate协议来做到这一点。 然后,该对象将选择加入windowWillClose:方法。

将以下代码行添加到osx_main.mm文件中,就在全局Running变量声明的正下方。

  @interface HandmadeMainWindowDelegate:NSObject  
@结束

如果您以前从未使用过Objective-C,那么您很有可能从未见过这样的代码。 没关系。 我会引导您完成。

这基本上就是您要说的。

  1. 我正在定义一个自定义对象,该对象继承自NSObject类(所有Objective-C类的基类)。
  2. 我的自定义对象实现了NSWindowDelegate协议(也就是它使用了您刚刚阅读的文档中的某些方法)。

在某个时候,我将为本课程的目标C编写一个入门。 可以说,Objective-C在某些方面类似于其他面向对象的编程语言。 它具有类和继承的概念。 它还具有接口的概念,本质上是协议表示的接口。

上面定义中的冒号表示“继承自阶级”。 尖括号表示正在采用的协议。

现在您可能会问:“等等,windowWillClose:方法在哪里?”一个好问题! 紧随其后,在实现部分

将以下代码粘贴到刚粘贴的代码下面

  @implementation HandmadeMainWindowDelegate 
  -(void)windowWillClose:(id)sender { 
运行=假;
}
  @结束 

本节将说明以下内容:“我们的自定义NSWindow委托对象仅处理一种方法,这就是windowWillClose:方法。 它通过将全局Running标志设置为false来处理它”

我们一定会在稍后再次访问NSWindowDelegate,因为它可以处理诸如调整窗口大小之类的其他事情。 目前,我们可以将其连接到主游戏窗口。

设置主窗口的委托

您可能未曾听说过称为“委派”的“设计模式”。

委派只是意味着:“我将让其他事情处理一些工作。”您知道,就像将衣服的洗涤委派给洗衣机一样。

我认为设计模式通常被用作拐杖,以避免不得不自己考虑应用程序的体系结构。 您应该能够超越其他人的思维范围,用自己的推理来解决问题。

话虽如此,了解什么是委派以及如何使用委派仍然很重要,因为在苹果代码中到处都使用委派。

在这种情况下,窗口委托只是窗口告诉其处理它不负责的事情的某个对象。 苹果之所以这样设置,是因为他们希望NSWindow成为纯UI类。 您的应用程序通过处理窗口事件本身来完成所有自定义工作。

现在,您已经定义了自定义的HandmadeMainWindowDelegate,您只需要在main函数中创建一个,然后将其设置为主窗口的委托即可。

将此行添加到主要功能的顶部

  HandmadeMainWindowDelegate * mainWindowDelegate = [[HandmadeMainWindowDelegate alloc] init]; 

不必担心本文需要两行内容。 在文本编辑器中只需要一行。

接下来,为窗口设置委托,并在窗口上调用makeKeyAndOrderFront:的行之后添加此行。

  [window setDelegate:mainWindowDelegate]; 

这将使用HandmadeMainWindowDelegate的实例,并将其设置为游戏主窗口的委托。

测试关闭视窗

运行您的构建脚本并对其进行测试。 您应该看到与以前相同的窗口。

现在,当您单击红色的x时,该应用程序会将Running global设置为false,然后脱离主运行循环并完成运行。 您应该在终端上看到“手工完成运行”消息,确认您的应用已退出。

在你用干草叉向我奔跑之前…

我毫不怀疑,通过这种风格的编程,我可能会冒犯一些铁杆苹果编程爱好者。 您甚至可能有一些疑问。

我在想什么,制作没有应用程序委托的应用程序?

我怎么没打电话却没有打电话[NSApp finishLaunching]方法来告诉系统我的应用程序已启动完毕?

为什么我不使用自动释放池来包装主运行循环以进行内存管理?

原因很简单。 我们实际上并不需要这些,创建它们只会消耗更多的系统资源。

我们没有应用程序委托,因为除了关闭窗口之外,还没有我们希望游戏响应的应用程序级事件。

我们之所以没有调用[NSApp finishLaunching],是因为我们没有一个系统托盘图标需要从选定更改为取消选定。

最后,我们不使用autoreleasepool,因为一旦应用程序退出,操作系统将回收在主运行循环中声明的项的内存。

它需要资源来声明一个autoreleasepool,然后在主运行循环结束时耗尽它(也称为释放内存)。 让操作系统回收内存是“最便宜的”路径。

以后我们会用这些东西吗? 大概。 但是当我们决定使用它们时,我们将确切知道为什么要这样做,而不仅仅是遵循约定。

支持此内容

如果您发现此内容有价值,并且希望看到更多内容,可以在Patreon上进行支持。

任何数量的帮助。

也许您正在尝试打入游戏行业,或者您只是想成为一个更好的程序员。 在本地大学注册高级编程课程需要花费多少钱? 您可以为发布跨平台游戏的Unity许可证支付什么费用?

您几乎可以从那里看到的每个教程都是从一个谜团开始的。 您不知道构建系统如何工作。 您只需要使用它。 您不知道这个或那个框架如何解决您的问题。 您只需要使用它们。

我们在这里做的事情完全不同。 我们没有质疑现有系统的价值,而是质疑我们所看到的并从头开始构建更好的东西。 如果我们不了解它,我们会戳它并刺刺它,直到我们明白为止。

我会在业余时间免费进行100%的操作。 每一项贡献都有助于我微不足道地全职从事这项工作。 如果我能够通过这项工作来支持自己,那将释放出大量的内容,我认为这些内容将使我们所有人都变得更好。

所以这又是那个链接。 在Patreon上支持此内容。 感谢您的阅读,如果您学到了一些有价值的东西,请在下面评论。

下一步是什么?

至此,我们大致处于Casey的第二个视频的结尾。 在下一篇文章中,我们将分配一个后备缓冲区并用普通的旧C渲染一些颜色。这将有效地将我们带回到1980年,在那里我们可以从头开始制作我们的游戏100%。

如果您想查看我们今天创建的任何代码,只需转到本教程系列的Github存储库中的Day 2文件夹。

另外,一定要支持Handmade Hero,并感谢Jeff Buck的Handmade Hero Mac OS回购。 这使我在将本系列放在一起时有极大的帮助。

再见!