
我想分享另一个晚上的长期建设,这表明即使在较弱的硬件上也可以制作游戏。
关于您必须做的事情,如何确定的事情以及如何做的事情不只是另一个Pong克隆而已-欢迎来到Cat。
警告:精美的文章,大量的访问量和多个代码插入!
简要介绍游戏
射击他们! -现在在AVR上。
实际上,这是另一张地图,因此主角
谢泼德(Shepard)必须再次拯救银河系,使其免受未知人物的突然袭击,使其穿越太空,穿过小行星的恒星和田野,同时清除每个恒星系统。
整个游戏都是用C和C ++编写的,而没有使用Arduino的Wire库。
该游戏有4艘船可供选择(通过后即可使用),每艘船都有自己的特点:
还实现了:
- 2D彩色图形;
- 为武器加电;
- 末级老板
- 小行星的水平(及其旋转动画);
- 背景颜色发生一定程度的变化(而不仅仅是黑色空间);
- 恒星在背景中以不同速度运动(以影响深度);
- 计分并保存在EEPROM中;
- 相同的声音(射击,爆炸等);
- 完全相同的对手。
平台平台
幽灵的归来。
我将事先澄清一下,该平台应该被视为第一代第三代(80年代, shiru8bit )的旧游戏机。
另外,禁止在原始硬件上进行硬件修改,以确保在其他任何相同的板上立即启动。
这款游戏是为Arduino Esplora主板编写的,但我认为转移到GBA或任何其他平台并不难。
但是,即使在此资源上,该委员会也只被覆盖了两次,尽管每个委员会的社区很大,但其他委员会根本不值得一提:
- GameBuino META:
- 波基托
- makerBuino;
- Arduboy;
- UzeBox / FuzeBox;
- 还有很多其他
首先,Esplora上没有的功能:
- 大量内存(ROM 28kb,RAM 2.5kb);
- 电源(16 MHz时为8位CPU);
- DMA
- 字符生成器;
- 分配的存储区或特殊寄存器。 目的地(调色板,图块,背景等);
- 控制屏幕的亮度(哦,垃圾桶里有这么多效果);
- 地址空间扩展器(映射器);
- 调试器(
但是在整个屏幕上谁需要它! )。
我将继续存在以下事实:
- 硬件SPI(可以以F_CPU / 2速度运行);
- 屏幕基于ST7735 160x128 1.44“;
- 少量计时器(仅4个);
- 一小撮GPIO;
- 少数几个按钮(5个+两轴操纵杆);
- 传感器很少(照明,加速度计,温度计);
- 压电蜂鸣器
刺激发射器。
显然那里几乎没有。 毫无疑问,除了傍克隆人和这段时间里的三场比赛之外,没有人愿意对她做任何事!
也许事实是,在ATmega32u4控制器(及类似产品)下进行编写类似于为Intel 8051编写程序(在发布时已使用了近40年),在该程序中您需要观察大量条件并采取各种技巧。
周边加工
一应俱全!
查看电路后,可以清楚地看到所有外设都通过GPIO扩展器(74HC4067D多路复用器和其他MUX)连接,并使用GPIO PF4,PF5,PF6,PF7或高级PORTF半字节进行切换,并且在GPIO-PF1上读取了MUX输出。
通过简单地通过掩码将值分配给PORTF端口来切换输入非常方便,绝不要忘记次要字节:
uint16_t getAnalogMux(uint8_t chMux) { MUX_PORTX = ((MUX_PORTX & 0x0F) | ((chMux<<4)&0xF0)); return readADC(); }
按钮点击民意调查:
#define SW_BTN_MIN_LVL 800 bool readSwitchButton(uint8_t btn) { bool state = true; if(getAnalogMux(btn) > SW_BTN_MIN_LVL) {
以下是端口F的值:
#define SW_BTN_1_MUX 0 #define SW_BTN_2_MUX 8 #define SW_BTN_3_MUX 4 #define SW_BTN_4_MUX 12
通过添加一些:
#define BUTTON_A SW_BTN_4_MUX #define BUTTON_B SW_BTN_1_MUX #define BUTTON_X SW_BTN_2_MUX #define BUTTON_Y SW_BTN_3_MUX #define buttonIsPressed(a) readSwitchButton(a)
您可以安全地采访正确的十字架:
void updateBtnStates(void) { if(buttonIsPressed(BUTTON_A)) btnStates.aBtn = true; if(buttonIsPressed(BUTTON_B)) btnStates.bBtn = true; if(buttonIsPressed(BUTTON_X)) btnStates.xBtn = true; if(buttonIsPressed(BUTTON_Y)) btnStates.yBtn = true; }
请注意,先前的状态不会重置,否则您可能会错过按下键的事实(它也可以防止颤动)。
特效
嗡嗡声。
如果没有DAC,没有Yamaha的芯片,并且只有1位PWM矩形,该怎么办?
乍一看似乎还不多,但是尽管如此,狡猾的PWM在这里还是用来重新创建“ PDM音频”技术的,您可以借助它来做到
这一点。Gamebuino的库提供了类似的功能,所需要的只是将弹出式生成器传输到另一个GPIO,并将计时器传输到Esplora(timer4和OCR4D输出)。 为了正确操作,timer1还用于产生中断,并用新数据重载OCR4D寄存器。
Gamebuino引擎使用声音模式(就像在跟踪器音乐中一样),可以节省很多空间,但是您需要自己进行所有采样,因为没有现成的库。
值得一提的是,该引擎的更新周期约为1/50秒或20帧/秒。
为了读取声音模式,在阅读了音频格式的Wiki之后,我在Qt上绘制了一个简单的GUI。 它不会以相同的方式输出声音,但是提供了有关音色如何发声的大致概念,并允许您加载,保存和编辑它。
图形
不朽的Pixelart。
显示器将颜色编码为两个字节(RGB565),但是由于这种格式的图像会占用大量空间,因此所有这些图像都已通过调色板进行索引以节省空间,这在我之前的文章中已经多次说明。
与Famicom / NES不同,图像没有颜色限制,调色板中有更多颜色可用。
游戏中的每个图像都是一个字节数组,其中存储以下数据:
- 宽度,高度;
- 开始数据标记;
- 字典(如果有的话,稍后再讲);
- 有效载荷
- 数据标记结束。
例如,这样的图片(放大10倍):

在代码中它将如下所示:
pic_t weaponLaserPic1[] PROGMEM = { 0x0f,0x07, 0x02, 0x8f,0x32,0xa2,0x05,0x8f,0x06,0x22,0x41,0xad,0x03,0x41,0x22,0x8f,0x06,0xa2,0x05, 0x8f,0x23,0xff, };
在没有这种类型的船只的地方? 经过数百个像素差异的测试草图后,只有这些船只可供玩家使用:

值得注意的是,船上的瓷砖没有火焰(此处为清楚起见),将其单独应用以创建引擎排气的动画。
不要忘了每艘船的飞行员:

敌方舰只的变异不会太大,但我要提醒你,空间不算太大,所以这里有三艘舰:

如果没有通过改善武器和恢复健康的形式获得正规奖励,玩家将不会持续很长时间:

当然,随着枪支功率的增加,发射的炮弹类型也会发生变化:

就像一开始写的那样,游戏有一个小行星的等级,它紧随第二个boss之后。 有趣的是,有许多不同大小的移动和旋转对象。 另外,当玩家击中它们时,它们会部分塌陷,尺寸变小。
提示:大型小行星可获得更多积分。



要创建此简单动画,只需12张小图像即可:

它们针对每种尺寸(大,中,小)分为三个,对于每个旋转角度,您还需要再旋转4个0、90、180和270度角。 在游戏中,以相等的间隔用图像替换指向数组的指针就足够了,从而产生旋转的幻觉。
void rotateAsteroid(asteroid_t &asteroid) { if(RN & 1) { asteroid.sprite.pPic = getAsteroidPic(asteroid); ++asteroid.angle; } } void moveAsteroids(void) { for(auto &asteroid : asteroids) { if(asteroid.onUse) { updateSprite(&asteroid.sprite); rotateAsteroid(asteroid); ...
这样做仅是由于缺乏硬件功能,并且像Affine转换这样的软件实现所花费的不仅仅是图像本身,而且非常慢。
对那些感兴趣的人来说是一块缎子。
您可以注意到部分原型,以及只有通过游戏后才能在积分中显示的内容。
除了简单的图形之外,为了节省空间并添加复古效果,还从字体中删除了小写的字形和最多30个及127个ASCII字节后的字形。
重要!
不要忘记AVR上的const和constexpr并不意味着数据将存储在程序存储器中,为此,您需要额外使用PROGMEM。
这是由于AVR内核基于哈佛体系结构,因此需要特殊的CPU访问代码来访问数据。
挤压银河系
打包最简单的方法是RLE。
研究了打包数据后,您会注意到未使用有效载荷字节中的最高有效位,范围从0x00到0x50。 这使您可以为重复的开始(0x80)添加数据和开始标记,以及下一个字节来指示重复的次数,这使您可以将257个相同的字节(实际上是两个字节的RLE愚蠢的事实加2)打包成两个相同的字节。
解包器的实现和显示:
void drawPico_RLE_P(uint8_t x, uint8_t y, pic_t *pPic) { uint16_t repeatColor; uint8_t tmpInd, repeatTimes; alphaReplaceColorId = getAlphaReplaceColorId(); auto tmpData = getPicSize(pPic, 0); tftSetAddrWindow(x, y, x+tmpData.u8Data1, y+tmpData.u8Data2); ++pPic;
最主要的是不要在屏幕外显示图像,否则将是垃圾,因为此处没有边框检查。
测试图像在约39ms内解压缩。 同时占用3040字节,而如果不进行压缩,则将占用11200字节或22400字节(不建立索引)。
测试图像(放大2倍):

在上图中,您可以看到隔行扫描,但是在屏幕上可以通过硬件对其进行平滑处理,从而产生类似于CRT的效果,同时大大提高了压缩率。
RLE不是万能药
我们接受了deja vu的待遇。
如您所知,RLE与类似LZ的包装机配合得很好。 WiKi提供了一系列压缩方法来进行救援。 推动力来自“ GameHut”的视频,内容涉及对
Sonic 3D Blast不可能的
介绍进行分析
。研究了许多包装工(LZ77,LZW,LZSS,LZO,RNC等)后,我得出的结论是他们的包装工:
- 需要大量RAM用于解压缩的数据(至少64kb。和更多);
- 笨重且缓慢(有些需要为每个亚基构建霍夫曼树);
- 压缩比低,窗口小(非常严格的RAM要求);
- 在许可方面含糊不清。
经过几个月的徒劳修改,决定修改现有的封隔器。
与类LZ的压缩程序类似,为了实现最大压缩,使用了字典访问,但是在字节级别上-字典中最频繁重复的字节对被一个字节指针替换。
但是有一个陷阱:如何区分“重复次数”和“词典标记”字节?
长时间坐在一张纸上,并用蝙蝠进行神奇的游戏后,出现了:
- “字典标记”是RLE标记(0x80)+数据字节(0x50)+词典中的位置号;
- 将字节“重复次数”限制为字典标记的大小-1(0xCF);
- 字典不能使用值0xff(用于图像结尾的标记)。
应用所有这些,我们得到一个固定的字典大小:不超过46个字节对,RLE减少到209个字节。 显然,并非所有图像都可以像这样打包,但它们将不再可用。
在这两种算法中,打包图像的结构如下:
- 每个宽度和高度1个字节;
- 1个字节,用于字典的大小,它是指向打包数据开头的标记指针;
- 从0到92个字节的字典;
- 1至N字节的打包数据。
D上产生的packer实用程序(pickoPacker)足以放入带有索引* .png文件的文件夹中,并从终端(或cmd)运行。 如果需要帮助,请使用选项“ -h”或“ --help”运行。
该实用程序运行后,我们将获得* .h文件,这些文件的内容可方便地传输到项目中的正确位置(因此没有保护)。
在打开包装之前,请准备好屏幕,字典和初始数据:
void drawPico_DIC_P(uint8_t x, uint8_t y, pic_t *pPic) { auto tmpData = getPicSize(pPic, 0); tftSetAddrWindow(x, y, x+tmpData.u8Data1, y+tmpData.u8Data2); uint8_t tmpByte, unfoldPos, dictMarker; alphaReplaceColorId = getAlphaReplaceColorId(); auto pDict = &pPic[3];
读取的数据可以打包在字典中,因此我们检查并解压缩它:
inline uint8_t findPackedMark(uint8_t *ptr) { do { if(*ptr >= DICT_MARK) { return 1; } } while(*(++ptr) != PIC_DATA_END); return 0; } inline uint8_t *unpackBuf_DIC(const uint8_t *pDict) { bool swap = false; bool dictMarker = true; auto getBufferPtr = [&](uint8_t a[], uint8_t b[]) { return swap ? &a[0] : &b[0]; }; auto ptrP = getBufferPtr(buf_unpacked, buf_packed); auto ptrU = getBufferPtr(buf_packed, buf_unpacked); while(dictMarker) { if(*ptrP >= DICT_MARK) { setPicWData(ptrU) = getPicWData(pDict, *ptrP); ++ptrU; } else { *ptrU = *ptrP; } ++ptrU; ++ptrP; if(*ptrP == PIC_DATA_END) { *ptrU = *ptrP;
现在,我们从接收到的缓冲区中以熟悉的方式解压缩RLE并将其显示在屏幕上:
inline void printBuf_RLE(uint8_t *pData) { uint16_t repeatColor; uint8_t repeatTimes, tmpByte; while((tmpByte = *pData) != PIC_DATA_END) {
令人惊讶的是,替换算法并没有显着影响拆包时间,大约为47ms。 这几乎是8毫秒。 更长,但测试映像仅占用1650个字节!
直到最后的措施
几乎所有事情都可以更快地完成!
尽管存在硬件SPI,但AVR内核在使用时仍令人头疼。
众所周知,AVR上的SPI除了在F_CPU / 2上运行外,还具有仅1字节的数据寄存器(无法一次加载2字节)。
此外,我遇到的AVR上几乎所有的SPI代码都根据以下方案工作:
如您所见,像在STM32上所做的那样,持续提供数据在这里没有味道。 但是,即使在这里,您也可以将两个拆包器的输出速度提高约3ms!
通过打开数据表并查看“指令集时钟”部分,您可以计算通过SPI传输字节时的CPU成本:
- 1个周期用于寄存器加载新数据;
- 每位2拍(或每字节16拍);
- 每时钟线魔术1 bar(稍后介绍“ NOP”);
- 1个时钟来检查SPSR中的状态位(或分支上的2个时钟);
总的来说,要传输一个像素(两个字节),应花费38个时钟周期或约425600个时钟周期用于测试图像(11,200个字节)。
知道F_CPU == 16 MHz,我们每个时钟周期得到
0.0000000625 62.5纳秒(
Process0169 ),将这些值相乘,就得出〜26毫秒。 问题来了:“我从哪儿写的,拆包时间是39ms。 和47毫秒。”? 一切都很简单-解包逻辑+中断处理。
这是中断输出的示例:

并且没有中断:

这些图表明,在VRAM屏幕中设置地址窗口与开始不中断的版本中的数据传输之间的时间较短,并且在传输期间字节之间几乎没有间隙(该图是均匀的)。
不幸的是,您不能为每个图像输出禁用中断,否则整个游戏的声音和核心将中断(稍后会详细介绍)。
上面写的是关于时钟线的某种“神奇的NOP”。 事实是,为了稳定CLK并设置SPIF标志,恰好需要1个时钟周期,并且在读取该标志时,它已经被设置,从而避免了在BREQ指令上分支成2条。
这是一个没有NOP的示例:

和他在一起:

差异似乎微不足道,只有几微秒,但是如果采用不同的比例:
大型NOP:

并且太大了:

则差异变得更加明显,达到〜4.3ms。
现在,让我们做以下肮脏的把戏:
我们交换加载和读取寄存器的顺序,您不能等待SPIF标志的第二个字节,而只能在加载下一个像素的第一个字节之前进行检查。
我们运用知识并部署函数“ pushColorFast(repeatColor);”:
#define SPDR_TX_WAIT(a) asm volatile(a); while((SPSR & (1<<SPIF)) == 0); typedef union { uint16_t val; struct { uint8_t lsb; uint8_t msb; }; } SPDR_t; ... do { #ifdef ESPLORA_OPTIMIZE SPDR_t in = {.val = repeatColor}; SPDR_TX_WAIT(""); SPDR = in.msb; SPDR_TX_WAIT("nop"); SPDR = in.lsb; #else pushColorFast(repeatColor); #endif } while(--repeatTimes); } #ifdef ESPLORA_OPTIMIZE SPDR_TX_WAIT("");
尽管计时器中断,使用上面的技巧仍可获得近6ms的增益。

这就是简单的铁知识如何使您从中榨出更多并输出类似的东西:

罗马竞技场碰撞
盒子之战。
首先,整个对象(船,壳,小行星,奖金)是具有以下参数的结构(子画面):
由于图像存储宽度和高度,因此无需重复这些参数,这样的组织在许多方面简化了逻辑。
基于矩形的交集,计算本身变得简单易行。 尽管它不够准确,也无法计算未来的冲突,但这绰绰有余。
验证是在X和Y轴上交替进行的,因此,X轴上没有相交会减少碰撞的计算。
首先,检查第一个矩形的右侧和第二个矩形的左侧是否为X轴的公共部分;如果成功,则对第二个矩形的左侧和右侧进行类似的检查。
成功检测到沿X轴的相交后,将以相同方式对沿Y轴的矩形的上下边进行检查。
上面看起来比看起来容易得多:
bool checkSpriteCollision(sprite_t *pSprOne, sprite_t *pSprTwo) { auto tmpDataOne = getPicSize(pSprOne->pPic, 0); auto tmpDataTwo = getPicSize(pSprTwo->pPic, 0); uint8_t objOnePosEndX = (pSprOne->pos.Old.x + tmpDataOne.u8Data1); if(objOnePosEndX >= pSprTwo->pos.Old.x) { uint8_t objTwoPosEndX = (pSprTwo->pos.Old.x + tmpDataTwo.u8Data1); if(pSprOne->pos.Old.x >= objTwoPosEndX) { return false;
仍然需要将其添加到游戏中:
void checkInVadersCollision(void) { decltype(aliens[0].weapon.ray) gopher; for(auto &alien : aliens) { if(alien.alive) { if(checkSpriteCollision(&ship.sprite, &alien.sprite)) { gopher.sprite.pos.Old = alien.sprite.pos.Old; rocketEpxlosion(&gopher);
贝塞尔曲线
太空轨。
与其他任何使用这种类型的游戏一样,敌舰必须沿着曲线移动。
决定实施二次曲线作为控制器和此任务的最简单方法。 三个点就足够了:初始(P0),最终(P2)和虚数(P1)。 前两个指定线的起点和终点,最后一个点说明曲率的类型。
关于曲线的好文章。由于这是贝塞尔曲线,因此还需要一个参数-起点和终点之间的中间点数。
总的来说,我们得到这样的结构:
typedef struct {
其中,position_t是坐标X和Y的两个字节的结构。
使用以下公式计算每个坐标的点(thx Wiki):
B =(((1.0-t)^ 2)P0 + 2t(1.0-t)P1 +(t ^ 2)P2,
t [> = 0 && <= 1]
长期以来,它的实现都是在没有定点数学的情况下直接解决的:
... float t = ((float)pItemLine->step)/((float)pLine->totalSteps); pPos->x = (1.0 - t)*(1.0 - t)*pLine->P0.x + 2*t*(1.0 - t)*pLine->P1.x + t*t*pLine->P2.x; pPos->y = (1.0 - t)*(1.0 - t)*pLine->P0.y + 2*t*(1.0 - t)*pLine->P1.y + t*t*pLine->P2.y; ...
当然,这不能离开。 毕竟,摆脱浮点数不仅可以提高速度,还可以释放ROM,因此发现了以下实现:
- avrfix;
- stdfix;
- libfixmath;
- fixedptc。
第一个仍然是一匹黑马,因为它是一个已编译的库,并且不想弄乱反汇编程序。
GCC捆绑包中的第二个候选者也没有解决,因为未使用所用的avr-gcc进行修补,并且“ short _Accum”类型仍然不可用。
第三种选择,尽管它有大量垫子。 功能,对Q16.16格式的特定位进行硬编码位操作,这使得无法控制Q和I的值。
后者可以看作是“ fixedmath”的简化版本,但是主要优点是不仅可以控制变量的大小(默认情况下为32位,格式为Q24.8),而且可以控制Q和I的值。
在不同设置下的测试结果:
型式 | 智商 | 附加标志 | ROM字节 | Tms。* |
---|
飘浮 | -- | -- | 4236 | 35 |
固定数学 | 16.16 | -- | 4796 | 119 |
固定数学 | 16.16 | FIXMATH_NO_OVERFLOW | 4664 | 89 |
固定数学 | 16.16 | FIXMATH_OPTIMIZE_8BIT | 5036 | 92 |
固定数学 | 16.16 | _NO_OVERFLOW + _8BIT | 4916 | 89 |
固定点 | 24.8 | FIXEDPT_BITS 32 | 4420 | 64 |
固定点 | 9.7 | FIXEDPT_BITS 16 | 3490 | 31 |
*检查是按照“ 195,175,145,110,170,70,170”和键“ -Os”执行的。
从表中可以看出,当使用float时,两个库占用的ROM都比GCC的编译代码还要多,并且显示出更差的结果。
还可以看到,对Q9.7格式进行了较小的修订,并将变量减小到16位,则加速了4毫秒。 并释放大约50个字节的ROM。
预期的效果是准确性下降,错误数量增加:

在这种情况下是不重要的。
分配资源
周二和周四仅工作一个小时。
在大多数情况下,所有计算都是在每帧进行的,这并不总是合理的,因为帧中可能没有足够的时间来计算某些东西,因此您将不得不选择交替,计数帧或跳过它们。 因此,我走得更远-完全放弃了人员配备。
将一切分解为小任务:计算碰撞,处理声音,按钮和显示图形,足以在一定的间隔内执行它们,而眼睛的惯性和仅更新部分屏幕的能力就可以解决问题。我们不仅使用OS来管理所有这些事情,还使用我几年前创建的状态机来管理所有这些事情,或者更简单地,不是用挤出的tinySM任务管理器来管理。我将重复使用它而不是任何RTOS的原因:- 较低的ROM要求(〜250字节内核);
- 较低的RAM要求(每个任务约9个字节);
- 简单易懂的工作原理;
- 行为确定性;
- 浪费更少的CPU时间;
- 不能接触铁;
- 平台无关;
- 用C编写,并且易于用C ++包装;
需要我自己的自行车。
正如我曾经描述的,针对它的任务被组织成一个指向结构的指针的数组,其中存储了指向函数及其调用间隔的指针。这种分组简化了游戏在不同阶段的描述,还使您可以减少分支数量并动态切换任务集。例如,在开始屏幕中,执行了7个任务,并且在游戏中,已经有20个任务(所有任务均在gameTasks.c文件中进行了描述)。首先,为了方便起见,您需要定义一些宏: #define T(a) a##Task #define TASK_N(a) const taskParams_t T(a) #define TASK(a,b) TASK_N(a) PROGMEM = {.pFunc=a, .timeOut=b} #define TASK_P(a) (taskParams_t*)&T(a) #define TASK_ARR_N(a) const tasksArr_t a##TasksArr[] #define TASK_ARR(a) TASK_ARR_N(a) PROGMEM #define TASK_END NULL
任务声明实际上是在创建一个结构,初始化其字段并将其放置在ROM中: TASK(updateBtnStates, 25);
每个这样的结构占用4字节的ROM(每个指针两个,每个间隔两个)。宏的一个好处是,它无法为每个函数创建多个唯一的结构。声明了必要的任务后,我们将它们添加到数组中,然后将它们放入ROM: TASK_ARR( game ) = { TASK_P(updateBtnStates), TASK_P(playMusic), TASK_P(drawStars), TASK_P(moveShip), TASK_P(drawShip), TASK_P(checkFireButton), TASK_P(pauseMenu), TASK_P(drawPlayerWeapon), TASK_P(checkShipHealth), TASK_P(drawSomeGUI), TASK_P(checkInVaders), TASK_P(drawInVaders), TASK_P(moveInVaders), TASK_P(checkInVadersRespawn), TASK_P(checkInVadersRay), TASK_P(checkInVadersCollision), TASK_P(dropWeaponGift), TASK_END };
将静态存储器的USE_DYNAMIC_MEM标志设置为0时,要记住的主要事情是初始化指向RAM中任务存储区的指针,并设置将要执行的最大指针数: ... tasksContainer_t tasksContainer; taskFunc_t tasksArr[MAX_GAME_TASKS]; ... initTasksArr(&tasksContainer, &tasksArr[0], MAX_GAME_TASKS); …
设置执行任务: ... addTasksArray_P(gameTasksArr); …
溢出保护由USE_MEM_PANIC标志控制,如果您确定任务数,可以禁用它以保存ROM。它仅保留运行处理程序: ... runTasks(); ...
内部是一个包含基本逻辑的无限循环。一旦进入堆栈,由于“ __attribute__((noreturn))”,堆栈也得以恢复。在循环中,在间隔过去之后,将交替扫描数组的元素以查找是否需要调用任务。间隔的倒计时是基于timer0作为一个具有1ms量子的系统进行的...尽管任务在时间上成功分配,但有时它们会重叠(抖动),这会导致游戏中所有事物的短期衰落。绝对必须决定,但是如何?关于下一次如何配置所有内容,但现在尝试在源代码中找到复活节彩蛋。结束
因此,使用了许多技巧(我还没有描述更多的技巧),结果证明所有内容都适合于24kb ROM和1500字节RAM。如果您有任何疑问,我将很乐意回答。对于那些没有找到或没有寻找复活节彩蛋的人::
void invadersMagicRespawn(void) { for(auto &alien : aliens) { if(!alien.alive) { alien.respawnTime = 1; } } }
, ?
invadersMagicRespawn: void action() { tftSetTextSize(1); for(;;) { tftSetCP437(RN & 1); tftSetTextColorBG((((RN % 192 + 64) & 0xFC) << 3), COLOR_BLACK); tftDrawCharInt(((RN % 26) * 6), ((RN & 15) * 8), (RN % 255)); tftPrintAt_P(32, 58, (const char *)creditP0); } } a(void) { for(auto &alien : aliens) { if(!alien.alive) { alien.respawnTime = 1; } } }
«(void)» , «action()» 10 , «disablePause();». «Matrix Falling code» . 130 ROM.
要构建和运行它,只需将文件夹(或链接)“ esploraAPI”放在“ / arduino / library /”中。参考文献:
附注:稍后,当我制作一个可以接受的视频时,您可以看到并听到一切。