08-对part5-breakout的搬运和翻译

所有图均为原文所有,并非我自己添加
非黑色字体均为我自己添加,
原文放在文章末尾
对Breakout的介绍
Breakout是一款经典的街机游戏,它有一个桨(或者说球拍),你可以在屏幕底部横向上移动它.屏幕的顶部是一行行砖头.一个球在屏幕上跳来跳去,在球拍和砖头之间反弹,当砖块被击中时就会被"撞碎".游戏目标是用球撞碎所有的砖头.如果你不及时接住球,那么它就会从屏幕下方出去,那你就死了一条命.一般情况下你死三次游戏就结束了.
对游戏引擎感兴趣的同学可以去听games104,b站上就有.
神奇的是,我们有所有用来构建我们的版本的 Steve Wozniak的经典作品的组件.

对我们绘图代码的调整
如果你也运行在1080p之下,你就会意识到8*8的字体对于屏幕来讲实在是太小了. 让我们修改drawChar,使其带上zoom参数,这样我们的游戏玩家就不用让鼻子碰到电视上了.
void drawChar(unsigned char ch, int x, int y, unsigned char attr, int zoom)
{
unsigned char *glyph = (unsigned char *)&font + (ch < FONT_NUMGLYPHS ? ch : 0) * FONT_BPG;
for (int i=1;i<=(FONT_HEIGHT*zoom);i++) {
for (int j=0;j<(FONT_WIDTH*zoom);j++) {
unsigned char mask = 1 << (j/zoom);
unsigned char col = (*glyph & mask) ? attr & 0x0f : (attr & 0xf0) >> 4;
drawPixel(x+j, y+i, col);
}
glyph += (i%zoom) ? 0 : FONT_BPL;
}
}
我们只是通过比例因子zoom来改变我们的循环从而改变我们的列高和行宽.比如说,当zoom设置为2的时候,我们的8*8位图实际上被渲染为16*16的位图.可视化的输出对我们的理解循环中所需要的逻辑有所帮助:
0 0 0 0 1 1 0 0 -> 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0
0 0 0 1 1 1 1 0 -> 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0
0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0
0 0 1 1 0 0 1 1 -> 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 1 1 0 0 1 1 -> 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 1 1 1 1 1 1 -> 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1
0 0 1 1 0 0 1 1 -> 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 1 1 0 0 1 1 -> 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 0 0 0 0 -> 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
很明显我们想要重复每个位zoom次来填满更宽的行(横向缩放).并且我们想要重复整行zoom次来填满更高的列(纵向缩放).
为了达成前一项,我们只需要在每次缩放而不是每次都左移一位掩码.我们利用整数四舍五入:
zoom=1 zoom=2
j=0 : (j / zoom) = 0 j=0 : (j / zoom) = 0
j=1 : (j / zoom) = 1 j=1 : (j / zoom) = 0
j=2 : (j / zoom) = 2 j=2 : (j / zoom) = 1
j=3 : (j / zoom) = 3 j=3 : (j / zoom) = 1
j=4 : (j / zoom) = 4 j=4 : (j / zoom) = 2
j=5 : (j / zoom) = 5 j=5 : (j / zoom) = 2
j=6 : (j / zoom) = 6 j=6 : (j / zoom) = 3
j=7 : (j / zoom) = 7 j=7 : (j / zoom) = 3
... ...
这样就解决了水平缩放问题.用类似的方法来解决纵向放大问题.我们使用模运算来完成glyph 指针的迭代递增(我认为这是"先除后余"),而不是在每次迭代时以每行的字节数递增指针.如果i被zoom除后没有余数,我们递增我们的 glyph 指针,否则我们就把它留在原来的位置.
zoom=2
i=1 : (i % zoom) = 1
i=2 : (i % zoom) = 0 -> advance the pointer
i=3 : (i % zoom) = 1
i=4 : (i % zoom) = 0 -> advance the pointer
i=5 : (i % zoom) = 1
i=6 : (i % zoom) = 0 -> advance the pointer
i=7 : (i % zoom) = 1
i=8 : (i % zoom) = 0 -> advance the pointer
...
你可能明白了为什么我们的外层循环从1开始而不是0?
如果你想,你可以现在就在你part5-framebuffer中的 fb.c中做出这些修改,并在kernel.c中执行它们来检查是否正确.别忘了更新 fb.h 中的函数定义来包含zoom参数.
现在修改drawString并且加上一个zoom参数并且传递它是很平凡的了.所以我不会在这里记录这些变化.
目标跟踪
现在我们可以画文本,(比如分数或者剩下几条命),矩形(桨或者砖)和圆形(球),我们可以在屏幕上复现Breakout这个游戏了.检查我们的新 kernel.c中的 initBall(), initPaddle(), initBricks() 和 drawScoreboard(score, lives) .
除了图形学代码,你可以看见我们在全局的 object 数组中记录了我们创造的每一个游戏中的物品.
它的 (x,y) 坐标
它的高和宽
物品的类型(球,牌子或者砖头)
它是否还"活着"
我们还存储了一个指向ball和paddle对象的全局指针,这样就很容易跟踪到它们
因为在游戏中我们需要撞碎我们的砖头,我创建了 removeObject(object),它只是把我们传进去的objec涂黑,然后把alive这个参数改为0来表示它不再参与游戏.
为了知道你的球将要碰撞到砖头(或者实际上是桨),我们需要碰撞检测(没检测碰撞一般会发生穿模).我们只需要对仍然存活(alive)的对象进行搜索,然后返回第一个坐标重叠的对象.如果没有物品被发现,我们就返回0.detectCollision(object, xoff, yoff) 实现了这些.注意 xoff 和 yoff 可能为负,因为球会往任何方向走.
键盘输入
我们将要用串口来接收输入,就像我们在 part4-miniuart里面做的那样.
getUart()只是检测是否有任何键被按下,如果有,它就返回字符,不然返回0.我们不希望这个函数一直等待一个键被按下,因为无论如何游戏都要继续.
让游戏动起来
让我们看看新的main()例程,它控制着游戏.大多数初始化代码对你来讲应该很熟悉,游戏结束时的Game over和 Well done 也应该很清楚.因此我将聚焦在内部循环的函数上.
首先,我们检测我们是否移动了paddle.按 "h"左移,"l"右移.我一般使用方向键,但是它们在UART上有些难以捕捉,所以让我们先保持简单(上下左右的方向键一般会转换成好几个字符,因此UART接收时需要一个状态机来进行判断)
现在我们实现了另一个函数 moveObject(object, xoff, yoff)来移动这些.这个从我在 fb.c中实现的另一个例程的调用开始,这个例程将矩形的位图从一个地方移动到另一个,只留下背景颜色.这是一个草率的实现,但现在可以了.我们只是更新object的(x,y)坐标,然后返回.
有了paddle的逻辑,我们现在来处理球.我们设置了一些初始化的速度,但是,在我们移动球之前,我们需要碰撞检测.当我们要撞到砖头的时候,我们:
使用removeObject移除砖头
反转我们的法向速度
增加分数
重新绘制得分板
当我们要碰撞到桨的时候,我们:
反转我们的法向速度
在水平方向上做出必要的改变(如果我们撞到了边缘)
我们可以在稍微延迟后移动我们的球(所以游戏速度不会很快).延迟代码在fb.c中实现,使用了arm的板上计时器.
最后,我们必须确保我们的球在游戏区域内活动.如果我们需要,我们可以反弹球,但是如果它从屏幕底下逃走了,我们就损失一条命,并且重置桨和球.这实际上十分简单,因为我们可以移除它们然后创造新的!
结论
我希望你编写你自己的第一个游戏比你自己想的简单的多 -- 在裸机上也是一样.它十分的简单,确实是的,但是一旦你完成它并且在树莓派4上运行,我打赌你会有些沉迷于它.我知道我是的!
好的,你现在完成了你的第一个游戏.
PS:如果你在使用arm gcc中遇到问题(即绘制第一块砖后意外崩溃),你可能需要试试把编译器优化从 Makfile中的 -O2 调整为 -O1.有些人报告了这个问题(https://github.com/isometimes/rpi4-osdev/issues/17),并且我能够复现.


Writing a "bare metal" operating system for Raspberry Pi 4 (Part 6)
Introducing Breakout
[Breakout](https://www.gameinformer.com/b/features/archive/2015/10/09/how-steve-wozniak-s-breakout-defined-apple-s-future.aspx) is a classic arcade game that has a paddle (or bat), which you move along a horizontal axis at the bottom of the screen. At the top of the screen are rows of bricks. A ball bounces around the screen, rebounding off the bat and off the bricks, which are "knocked out" when directly hit. The goal is to knock out all of the bricks with the ball. If you don't get to the ball in time, and it escapes the bottom of the screen, you lose a life. You typically have 3 lives before Game Over.
Amazingly, we have all the components we need to build our version of [Steve Wozniak](https://en.wikipedia.org/wiki/Steve_Wozniak)'s classic.

A tweak to our drawing code
If you're also running in 1080p, then you'll note that our 8x8 font is rather small on the screen. Let's tweak `drawChar` to take a `zoom` parameter so our game players don't need to have their noses against the TV!
```c
void drawChar(unsigned char ch, int x, int y, unsigned char attr, int zoom)
{
unsigned char *glyph = (unsigned char *)&font + (ch < FONT_NUMGLYPHS ? ch : 0) * FONT_BPG;
for (int i=1;i<=(FONT_HEIGHT*zoom);i++) {
for (int j=0;j<(FONT_WIDTH*zoom);j++) {
unsigned char mask = 1 << (j/zoom);
unsigned char col = (*glyph & mask) ? attr & 0x0f : (attr & 0xf0) >> 4;
drawPixel(x+j, y+i, col);
}
glyph += (i%zoom) ? 0 : FONT_BPL;
}
}
```
We're just changing our loops to increase the column height and row width by our scale factor `zoom`. With `zoom` set to 2, for example, our 8x8 bitmap will actually be rendered as a 16x16 bitmap. To understand the logic required within the loop, it helps me to visualise the desired output:
```c
0 0 0 0 1 1 0 0 -> 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0
0 0 0 1 1 1 1 0 -> 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0
0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0
0 0 1 1 0 0 1 1 -> 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 1 1 0 0 1 1 -> 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 1 1 1 1 1 1 -> 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1
0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1
0 0 1 1 0 0 1 1 -> 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 1 1 0 0 1 1 -> 0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 1 1 1 1 0 0 0 0 1 1 1 1
0 0 0 0 0 0 0 0 -> 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
```
It's clear that we want to repeat each bit `zoom` times to fill the wider row (horizontal zoom). And we then want to repeat the entire line `zoom` times to fill the taller column (vertical zoom).
To achieve the former, we simply advance our bitmask one in every `zoom` times instead of every time. We make use of integer rounding thus:
```c
zoom=1 zoom=2
j=0 : (j / zoom) = 0 j=0 : (j / zoom) = 0
j=1 : (j / zoom) = 1 j=1 : (j / zoom) = 0
j=2 : (j / zoom) = 2 j=2 : (j / zoom) = 1
j=3 : (j / zoom) = 3 j=3 : (j / zoom) = 1
j=4 : (j / zoom) = 4 j=4 : (j / zoom) = 2
j=5 : (j / zoom) = 5 j=5 : (j / zoom) = 2
j=6 : (j / zoom) = 6 j=6 : (j / zoom) = 3
j=7 : (j / zoom) = 7 j=7 : (j / zoom) = 3
... ...
```
So, that sorts out our horizontal zoom. To achieve the vertical zoom, it's a similar solution. Instead of advancing our glyph pointer by the number of bytes per line on each iteration, we do it one in every `zoom` iterations by making use of the **modulo** operator (I think of this as "remainder after division"). If `i` divided by `zoom` has no remainder then we advance our glyph pointer, otherwise we leave it where it is!
```c
zoom=2
i=1 : (i % zoom) = 1
i=2 : (i % zoom) = 0 -> advance the pointer
i=3 : (i % zoom) = 1
i=4 : (i % zoom) = 0 -> advance the pointer
i=5 : (i % zoom) = 1
i=6 : (i % zoom) = 0 -> advance the pointer
i=7 : (i % zoom) = 1
i=8 : (i % zoom) = 0 -> advance the pointer
...
```
Perhaps you can see why we changed our outer loop to count from 1 rather than 0?
If you want, you can now make these changes to _fb.c_ in your part5-framebuffer code and exercise them properly in _kernel.c_ to check that they work. Don't forget to update the function definition in _fb.h_ to include the `zoom` parameter too.
It is also now trivial to modify `drawString` to take a `zoom` parameter and pass it through, so I won't document the changes here.
Object tracking
As we can now draw text (e.g. a score/lives counter), rectangles (paddle & bricks) and circles (ball), we can recreate the Breakout game screen. Check out `initBall()`, `initPaddle()`, `initBricks()` and `drawScoreboard(score, lives)` in our new _kernel.c_.
In addition to the graphics code, you'll see that we're keeping a record of each game object we create in the global `objects` array:
* its (x, y) coordinates
* its width & height
* what type of object it is (ball, paddle or brick)
* whether it's "alive"
We also store a global pointer to the ball and paddle object, so they're easy to track down!
As we'll need to knock out our bricks during gameplay, we create `removeObject(object)`, which simply draws a filled black rectangle over the object we pass, and sets its `alive` parameter to 0 to signal that it's now out of play.
To know that our ball is about to hit a brick (or indeed the paddle), we'll need to detect **collisions**. We simply conduct a search of the alive objects and return the first object we find whose coordinates overlap. If no object is found, we return 0. `detectCollision(object, xoff, yoff)` implements this. Note that `xoff` and `yoff` can be negative since the ball could be travelling in any direction.
Keyboard input
--------------
We'll be using the UART to take input, just like we did in part4-miniuart.
`getUart()` simply checks if a key has been pressed and, if so, it returns the character, otherwise 0. We don't want this function to wait for a key, because gameplay needs to continue regardless.
Animating the gameplay
Let's look at the new `main()` routine which controls the gameplay. Most of the initialisation code should be familiar to you, and the endgame of "Game over" or "Well done!" should also be clear. I'll therefore focus on the function of the inner loop.
First, we check if we need to move the paddle. We'll move it left if we get a keypress of "h", and right if we get a keypress of "l". I would use the arrow keys normally, but they're a little harder to capture over the UART so we'll keep it simple for now.
We now implement another function `moveObject(object, xoff, yoff)` to make these moves. This starts by calling another routine I've implemented in _fb.c_ to move a rectangular bitmap from one screen location to another, leaving only the background colour behind. It's a sloppy implementation, but it will do for now. We then just update the object's (x, y) coordinates and return.
With our paddle logic in place, let's deal with the ball. We've set some initial velocities but, before we actually move the ball, we need to check for collisions. If we're about to hit a brick, we:
* remove that brick using `removeObject`
* reverse our vertical direction
* increment the score
* redraw the scoreboard
If we're about to hit the paddle, we:
* reverse our vertical direction
* make any necessary changes to our horizontal direction (in case we hit the side)
We can then move our ball after a slight delay (so gameplay isn't too fast). The delay code is implemented in _fb.c_ and uses the on-board timer on the ARM.
Finally, we just need to make sure the ball is in the game arena. We bounce it off the sides if we need to but, if it escapes the bottom of the screen then we lose a life and reset both the paddle & ball. This is easy enough since we can just remove them and create new ones!
Conclusion
I hope writing your first game was easier than you thought it might be - and on bare metal too. It's pretty simple, granted, but once you've built it and got it going on your RPi4, I bet you'll be just a little bit addicted. I know I am!
_Well done, you've just written your first game!_
PS: if you have issues using the Arm gcc compiler (namely an unexpected crash after painting the first brick), you might to try setting the compiler optimisation level to `-O1` instead of `-O2` in the _Makefile_. Some folks have [reported issues](https://github.com/isometimes/rpi4-osdev/issues/17), and I am able to reproduce these.