手工制作的Hero Mac OS平台第004天层,对后缓冲进行动画处理

现在该回到80年代了。 在上一篇文章中,我们完成了所有设置工作,以创建一个可以吸引到的缓冲区。 现在,我们实际上将要在该缓冲区中绘制一些东西,这将是惊人的。

我想指出一件事。 我们将介绍的内容与Casey的Handmade Hero Day 004流没有什么不同。 实际上,我们已经进入了大部分跨平台的系列文章的一部分。

我只是在介绍它,是因为我想为您提供从第一天到Casey完成Win32平台层足以开始游戏的无缝过渡。

遍历后缓冲

在上一篇文章中,我们详细介绍了创建缓冲区并为自己提供一些遍历缓冲区的方法。 现在,我们将其中的一些想法付诸实践,并开始编写像素遍历循环。

循环的结构

为了遍历像素缓冲区,我们将使用嵌套的for循环,其中外循环代表遍历一行,而内循环则代表遍历该行中的各个像素。

将此骨架粘贴到顶部的主运行循环内部,该部分最终将缓冲区的内容写入NSImage,然后写入窗口。

  int width = bitmapWidth; 
int height = bitmapHeight;
  for(int y = 0; y <height; ++ y){ 
  //遍历一行 
for(int x = 0; x <width; ++ x){
  //在行内遍历像素 
}
  } 

使用行指针

到目前为止还算不错,但是不是很实质。 毕竟,什么定义了行? 单行前进会如何?

可以将行指针定义为代表缓冲区中当前行的开始的地址。 遍历每一行时,将行指针向前移动间距量。 在上一篇文章中,我们将音高定义为单行中的字节数。

在外部for循环的正上方添加以下行。

  uint8_t *行=(uint8_t *)缓冲区; 

使用缓冲区指针作为起点,这实际上为当前行声明了一个单独的指针。

接下来,在内部for循环的正下方添加以下行。

 行+ =音高 

简而言之,每次增加y时,您还将行指针向前移动了音调量,因此它始终指向下一行的开始。

使用像素指针

当然,单行遍历是不够的。 我们实际上想在窗口上画些东西。 为此,我们需要使用内部循环并创建一个指向当前像素的指针。

在内部for循环的正上方添加以下行。

  uint8_t *像素=(uint8_t *)行; 

这为当前像素声明了一个单独的指针,从行的开头开始。

请注意,这是一个uint8_t指针,这意味着如果将其递增1,就不会向前移动一个像素。 相反,您将向前移动一个像素元素或颜色/ alpha通道值说明符。

此时,嵌套的for循环应如下所示。

  uint8_t *行=(uint8_t *)缓冲区; 
  for(int y = 0; y <height; ++ y){ 
  uint8_t *像素=(uint8_t *)行; 
  for(int x = 0; x <width; ++ x){ 
  / *内存中的像素:RR GG BB AA * / 
  } 
 行+ =音高; 
  } 

这是像素循环的主要骨架。 最里面的循环是有趣的部分,因为这是您进行所有绘制的地方。

绘制到像素缓冲区

如果取消引用像素指针(aka * pixel =)并向该地址写入一个无符号整数,则将修改其指向的颜色通道。 这具有绘制缓冲区的效果。

与Casey的教程不同,我们的像素以更直接的方式进行布局。 每个字母都以红色开头,然后是绿色,然后是蓝色,最后是字母。 这与以下事实有关:我们在设置NSBitmapImageRep对象时指定了RGB颜色空间。

要绘制到每个通道,我们只需写入已取消引用的像素指针,然后将指针加1。

将以下代码粘贴到最里面的循环中

  //红色 
*像素= 0;
++像素;
  //绿色 
*像素= 0;
++像素;
  //蓝色 
*像素= 255;
++像素;
  //Α 
*像素= 255;
++像素;

信不信由你,您几乎可以构建游戏并渲染缓冲区了。 只有最后一步。

将以下代码放在初始窗口设置的底部,紧接在设置窗口委托的那一行之后。 它使窗口的内容视图使用图层进行绘制。

  window.contentView.wantsLayer = YES; 

我不得不笑,因为我尝试为第4天教程制作视频,但没有看到任何绘图。 我花了半个小时才弄清楚这是为什么。

现在构建并运行。 这就是我得到的。

那就对了。 这是一个坚实的蓝色窗口。 当您意识到我们最初将窗口的背景指定为RED时,这一点尤其显着。

有用! 我们回到了80年代! 您可以通过将相应的像素设置为255,然后将其他像素(alpha除外)设置为零来测试其他通道。

渲染怪异的渐变

在Casey的视频中,他将绿色通道设置为y值,将蓝色通道设置为x值。 这会创建一个有趣的渐变色,其中每个通道的颜色会增加到最大,然后下降。

像这样修改内部循环,然后生成并运行。

  //红色 
*像素= 0;
++像素;
  //绿色 
* pixel =(uint8_t)y;
++像素;
  //蓝色 
* pixel =(uint8_t)x;
++像素;
  //Α 
*像素= 255;
++像素;

您应该在窗口中看到以下输出。

这开始变得有趣。 看到我们如何在短短四天内到达屏幕上,这真是太酷了。

人们说很难从头开始编写代码,但是我并没有发现那么困难,也没有太多的资源可以开始。 我必须去寻找我要呈现给您的大部分内容。

调整窗口大小

如果您尝试调整窗口的大小,您会注意到一些奇怪的行为(不确定是否有标准行为)。 渐变将拉伸并挤压。 为什么会这样呢?

之所以发生这种情况,是因为在调整窗口大小时主运行循环未运行,并且它正在使用窗口对象中的当前内容视图大小。 由于它使用的bitmapWidth和bitmapHeight不再映射到窗口的实际大小,因此它会失真。

注意完成调整窗口大小后会发生什么。 您应该看到图案返回到其标准的256 x 256像素配置。 这是因为主运行循环已恢复。

显然,如果我们打算制作视频游戏,这将在以后出现问题。 如果调整窗口大小会中断主运行循环,则需要执行某些操作来处理此类事件(例如暂停游戏等)。

现在,这将有助于了解在调整窗口大小时将调用什么OSX平台代码。 这样,我们至少可以在发生这种情况时调整缓冲区的大小。

回顾HandmadeMainWindowDelegate

回到第二篇文章,我们创建了一个名为HandmadeMainWindowDelegate的自定义对象。 它只有一个目的,即通过关闭NSWindowDelegate协议中的windowWillClose:委托方法,在游戏关闭时退出运行循环。

NSWindowDelegate相当长且涉及很多。 一旦了解了完全可交付使用的Mac OS平台层逻辑,我们可以处理许多其他事件。 但是目前,我们仅对调整窗口大小时调用的方法感兴趣。

windowWillClose:方法实现之后添加以下代码行。

  -(void)windowDidResize:(NSNotification *)notification { 
//处理窗口调整大小
}

很好。 您已设置为响应窗口调整大小事件。

那么,当窗口调整大小时我们要做什么? 我要说的是,我们要刷新缓冲区然后渲染渐变,这与我们在主运行循环中所做的基本相同。

将缓冲区和渲染代码移至某些功能

让我们将其中的一些代码移出到几个函数和全局变量中,而不是简单地复制和粘贴该代码。 这样,我们可以从主运行循环或windowDidResize:方法中调用相同的三个函数。

首先,让我们将缓冲区创建代码移到我们自己创建的称为refreshBuffer的函数中。 这是特定于平台的一点,因为它创建了将位图呈现到窗口所必需的Cocoa对象。

将以下代码行直接添加到用于运行和缓冲的全局变量下面。

  void macOSRefreshBuffer(NSWindow * window){ 

}

此函数获取当前窗口,并使用窗口的contentView大小刷新缓冲区。 它与我们一直在使用的缓冲区创建逻辑基本相同,只是移至其他位置。

释放缓冲区

只有一个收获。 如果我们计划使用不同的bitmapWidth和bitmapHeight重新创建缓冲区,则应首先从内存中释放它。

当您考虑到每次调整窗口大小时都会刷新此缓冲区时,这一点尤其重要。 如果不释放它,缓冲区将在每次调整大小的操作时泄漏,您可以想象,它将很快开始累加。

在refreshBuffer函数内添加以下代码,因此如下所示

  void macOSRefreshBuffer(NSWindow * window){ 
如果(缓冲区){
免费(缓冲区);
}
}

这只是说“如果缓冲区已经分配,​​则将其释放。”

使更多变量成为全局变量

全局变量通常不是一个好习惯,但是由于我们仅限于一个文件,而且我们知道所编写的代码是临时的,出于演示目的,所以可以。 无论如何,我们稍后将把所有这些转移到其他地方。

使用global_variable声明将bitmapWidth,bitmapHeight,bytesPerPixel和pitch移动到全局范围。 您可以将它们直接放在缓冲区的声明下。

  global_variable int bitmapWidth; 
global_variable int bitmapHeight;
global_variable int bytesPerPixel = 4;
global_variable int pitch;

现在,无论何时调用refreshBuffer函数,它都可以更新全局bitmapWidth,bitmapHeight和pitch。

使用refreshBuffer函数

在释放缓冲区的行之后,在refreshBuffer函数内部添加以下行,因此该函数看起来像这样

  void macOSRefreshBuffer(NSWindow * window){ 
 如果(缓冲区){ 
免费(缓冲区);
}
  bitmapWidth = window.contentView.bounds.size.width; 
bitmapHeight = window.contentView.bounds.size.height;
间距= bitmapWidth * bytesPerPixel;
缓冲区=(uint8_t *)malloc(pitch * bitmapHeight);
}

而已。 现在,您可以在代码中的任何位置调用refreshBuffer函数。

返回到主运行循环开始之前的代码行,并用一次对refreshBuffer函数的调用替换负责分配缓冲区的行。

这是其中的一部分。

  window.contentView.wantsLayer = YES; 
  macOSRefreshBuffer(window); 
 在跑步的时候) { 
...

现在返回windowDidResize:方法,并像这样添加对refreshBuffer函数的调用。

  -(void)windowDidResize:(NSNotification *)notification { 
NSWindow * window =(NSWindow *)notification.object;
macOSRefreshBuffer(window);
}

只有一行代码是新的,它将通知对象强制转换为NSWindow指针。

如果您阅读有关windowDidResize:方法的文档,Apple表示您可以保证通知的对象将是(NSWindow *)。 为什么不编写此api来传递显式(NSWindow *),这超出了我的范围,但是通知中可能包含其他相关信息。

移动renderWeirdGradient代码

总而言之,我们在渲染过程中发生了两个关键的事情。 首先,我们将要渲染的内容写入像素缓冲区。 然后,我们获取像素缓冲区的内容,创建(NSBitmapImageRep *)并从中创建一个NSImage,然后将其渲染到窗口的内容视图中。

我听说有传言说,在Mac上有一种更快的渲染位图的方法,但是对于渲染器的第一遍,就足够了。 如果评论中的任何人有一些想法,我很想听听他们的意见。

让我们解决将写入缓冲区的代码移动的问题。 在Handmade Hero Day 004视频中,Casey调用了该方法renderWeirdGradient,因此我们将采用相同的方法。

将以下函数直接放在refreshBuffer函数的下面。

  void renderWeirdGradient(){ 
  int width = bitmapWidth; 
int height = bitmapHeight;
  uint8_t *行=(uint8_t *)缓冲区; 
  for(int y = 0; y <height; ++ y){ 
  uint8_t *像素=(uint8_t *)行; 
  for(int x = 0; x <width; ++ x){ 
  / *内存中的像素:RR GG BB AA * / 
  //红色 
*像素= 0;
++像素;
  //绿色 
* pixel =(uint8_t)y;
++像素;
  //蓝色 
* pixel =(uint8_t)x +(uint8_t)offsetX;
++像素;
  //Α 
*像素= 255;
++像素;
}
 行+ =音高; 
}
}

您是否注意到此函数中的代码很酷或独特? 我会给你一个提示。 是否有对NSWindows,NSApplications,NSEvent或任何与Apple相关的参考?

不。 没有。 此代码是完全独立于平台的。 一旦我们完成了Mac OS平台层,您就可以将其塞入游戏的代码束中并在任何平台上运行。

在调用之后立即从windowDidResize:方法调用renderWeirdGradient方法以刷新缓冲区。 另外,请务必在主运行循环的顶部调用它。

  -(void)windowDidResize:(NSNotification *)notification { 
NSWindow * window =(NSWindow *)notification.object;
macOSRefreshBuffer(window);
renderWeirdGradient();
}

现在这是其中包含调用的主运行循环。

 在跑步的时候) { 
renderWeirdGradient()
  ...主运行循环的其余部分 
}

将绘图缓冲区移动到窗口代码

您还需要将其移到一个单独的函数中,这是将像素缓冲区绘制到窗口的代码。

在renderWeirdGradient函数正下方声明以下函数。

  void macOSRedrawBuffer(NSWindow * window){ 
  } 

现在,使用由autoreleasepool声明(如果您决定使用其中一个)包围的漂亮的大代码块,并将其粘贴到函数内部。 供参考,这就是这段代码。

太好了,现在您可以在任意位置调用此代码块。 我们将从windowDidResize:方法和主运行循环中调用它,紧接在写入像素缓冲区的代码之后。

再次,以供参考。

  -(void)windowDidResize:(NSNotification *)notification { 
NSWindow * window =(NSWindow *)notification.object;
macOSRefreshBuffer(window);
renderWeirdGradient();
macOSRedrawBuffer(window);
}

并在主运行循环中。

  macOSRefreshBuffer(window); 
 在跑步的时候) { 

renderWeirdGradient();
macOSRedrawBuffer(window);
  ...主运行循环的休息 
}

一些快速测试

生成并运行该应用程序。 然后调整窗口大小。 您应注意,调整窗口大小时,窗口的内容不再拉伸或变形。 这是因为每次调整窗口大小时,应用程序都会执行一次渲染过程。

动画缓冲

只是为了好玩,让我们做Casey在第4天视频中的操作,并添加一个X偏移量以动画化后缓冲区,并查看渲染循环的实际效果。

在音调变量的正下方声明以下global_variable。

  global_variable int offsetX = 0; 

接下来,进入渲染路径之后,进入主运行循环并增加x偏移量的一行。

 在跑步的时候) { 
  renderWeirdGradient(); 
macOSRedrawBuffer(window);
  offsetX ++; 
  ...主运行循环的休息 
}

每次应用渲染缓冲区时,x偏移量都会增加。 要查看其结果,只需将其添加到蓝色像素中(因为它是当前与x属性关联的像素)。

在renderWeirdGradient函数内部(您在其中写入蓝色像素的位置),像这样添加x偏移量。

  * pixel =(uint8_t)x +(uint8_t)offsetX; 

现在,如果您构建并运行游戏,您应该会看到一个动画的渐变,该渐变一​​直向左移动。

很酷! 我们已经从没有图形变成了用跨平台C编写的有趣的动画渐变。这是一个好的开始。

一些家务

在此视频中,Casey做了一些额外的整理工作,并决定声明自己的整数和无符号整数类型。 我喜欢这个主意。 每当声明一个无符号整数时,键入多余的_t就会很麻烦。

在osx_main.mm文件顶部附近的以下行中,将内部,local_persist和global_variable映射到静态的#define行下。

  typedef int8_t int8; 
typedef int16_t int16;
typedef int32_t int32;
typedef int64_t int64;
  typedef uint8_t uint8; 
typedef uint16_t uint16;
typedef uint32_t uint32;
typedef uint64_t uint64;

现在,不必使用uint8_t,只需将其称为uint8。 继续,并用uint8替换所有对uint8_t的引用(或者只是从已经完成的Github存储库的Day 4文件夹中下载源代码)。

我还自由地标准化了一些变量名。 GlobalRenderWidth现在将变为globalRenderWidth等。

支持此内容

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

任何数量的帮助。

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

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

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

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

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

下一步是什么?

现在我们有了将图形渲染到缓冲区的方法,可以休息一下。 在该系列的下一部分中,我将解决一些代码清理和我的一个宠物项目,即在Xcode中进行调试。

由于这不是实时直播,因此我一直在回答问题。 如果您对这些教程中的代码或总体上对Mac OS有疑问,请在下面留下评论。

可以在公共Mac OS平台层Github存储库中找到本文中使用的所有代码。

请务必订阅我的YouTube频道并预订您的Handmade Hero副本。 您的支持使这成为可能。

下次见!