手撸俄罗斯方块(四)——渲染与交互

手撸俄罗斯方块(四)——渲染与交互

如何渲染游戏界面

我们知道,当我们看到页面先呈现图像时,实际上看到的是一张图片,多张图片按照一定的刷新频率进行切换,则变成了动态的视频。当刷新频率超过24Hz时,人眼不会察觉到卡顿情况。

因此,一个简单的方案呼之欲出,我们只需要按照1000 / 24 = 41.7ms的时间间隔整体刷新一下频率即可。
在这里插入图片描述

对于俄罗斯方块而言,从上到下,我们可以将整个操作界面分为如下区域:

  • 外框区域: 包括外框的颜色和整体的背景,以及外框的样式;

  • 分数区域: 用于显示分数;

  • 当前图形: 显示当前正在移动的方块;

  • 下一图形: 显示接下来要出现的方块;

  • 游戏状态: 显示当前游戏的状态,如: 游戏暂停;

  • 已填充图形: 显示已填充的图形;

我们有两种方式进行处理:

  1. 每次刷新时重新渲染所有部分。该方法处理逻辑简单,清空全局区域,渲染有效区域。

  2. 每次刷新时仅渲染更新区域。该方法能减少页面整体重绘,提交渲染效率,但相对来说处理复杂,控制逻辑较多。

但是,有些场景却无法使用局部刷新,如控制台渲染。

下面我分别以控制台渲染和DOM渲染为例,分别讲述如何实现渲染。

控制台渲染

按照上文描述,我们将渲染过程进行了抽象,包括了首次渲染和更新渲染,如下:

import { Canvas } from '@shushanfx/teris-core';
class ConsoleCanvas extends Canvas {render(): void {}update(): void {}
}

对于控制台而言,每次更新实际上也是渲染全部区域,因此我们可以定义update为:

class ConsoleCanvas extends Canvas {update(): void {this.render();}
}

那么接下来的问题就是如何实现render方法。

render实现

render为全局渲染,在渲染之前需要首先清除控制台。我们可以使用如下代码表示渲染逻辑:

render() {const printArray = [];// 1. 清空可视区域console.clear();// 2. array填充// ...// 3. 打印arrayprint(printArray);
}

array的填充

将整个渲染区域分解成一个个字符,多个字符组合组装成画面。这个是控制台游戏的特点。

我们俄罗斯方块游戏的特点:

  1. 第一行显示为外边框的上边框;

  2. 第二行显示为分数;

  3. 第三行为内边框的上边框;

  4. 游戏核心区域渲染,包括填充方块、下一个方块、游戏帮助、游戏状态等字符;

  5. 内边框的下边框;

  6. 外边框的下边框。

  7. 渲染外边框的上边框时:

const outLine1 = this.getOutterLine(this.leftTopChar +this.createChar(xSize + 2 + this.rightWidth, this.horizonalChar) +this.rightTopChar
);
printArray.push(outLine1);

包括leftTopCharhorizonalCharrightTopChar

  1. 渲染分数:
// 2. 渲染score
const scoreText = this.theme.scoreTemplate(score);
const scoreConsoleChar = ConsoleChar.create(scoreText);
this.theme.scoreStyle(scoreConsoleChar);
// 计算左侧需要补充的空格
const leftSpace = this.rightWidth - scoreText.length - 3;
// 右侧需要补充的空格
const rightSpace = 3;
let scoreLine =this.getOutterLine(this.verticalChar) +this.createChar(xSize + 2 + leftSpace) +scoreConsoleChar.ch +this.createChar(rightSpace) +this.getOutterLine(this.verticalChar);
printArray.push(scoreLine);
  • 1~3行,用于生成score的文本和样式,后续在theme中会描述。
  • 5行,计算左侧补充的空格数
  • 7行,右侧补充的空格数
  • 添加scoreLine,包括verticalChar,左侧空格,分数,右侧空格,verticalChar。
  1. 渲染内边框的上边框
// 3. 渲染内边框的上边框
let line1 =this.getOutterLine(this.verticalChar) +this.getInnerLine(this.leftTopChar);
for (let x = 0; x < xSize; x++) {const oneBlockItem = current?.points.find(item => item.x === x);if (oneBlockItem) {line1 += this.getInnerLine(bold(this.horizonalChar))} else {line1 += this.getInnerLine(this.horizonalChar)}
}
line1 +=this.getInnerLine(this.rightTopChar) +this.createChar(this.rightWidth) +this.getOutterLine(this.verticalChar);
printArray.push(line1);

内边框包括,verticalCharleftTopCharhorizonalChar、空格和verticalChar

  1. 核心区域渲染

游戏核心区域主要是一些方块,包括当前方块currentBlock,下一个方块nextBlock,已经落地的方块以及左右边框。

基本思路为:

4.1. 对于游戏区域,按照xSize x ySize维度,遍历每个点

* 如果当前点存在points(已经固定的点)中,则渲染固定点;
* 如果当前点包含在currentBlock中,则渲染当前活动的block。
* 否则渲染空点```javascript
for (let y = 0; y < ySize; y++) {let rowLength = 2;let row = this.getOutterLine(this.verticalChar)+ this.getInnerLine(this.verticalChar);for (let x = 0; x < xSize; x++) {const point = stage.points[y][x];const currentPoint = current? current?.points.find((p) => p.x === x && p.y === y): null;if (currentPoint || !point.isEmpty) {let consoleChar = new ConsoleChar(this.blockChar);this.theme.blockPointStyle(consoleChar, currentPoint || point);row += consoleChar.ch;} else {row += " ";}}rowLength += xSize;row += this.getInnerLine(this.verticalChar);rowLength += 1;// 渲染其他点位// 扣除末尾的结束符号const leftLength = outLength - rowLength - 1;if (leftLength > 0) {row += new Array(leftLength).fill(" ").join("");}row += this.getOutterLine(this.verticalChar);printArray.push(row);
}
```从上述代码来看,渲染逻辑很简单,先是渲染左侧外边框`verticalChar`、内边框`verticalChar`,之后渲染具体的方块,即如果方块包含在currentBlock或者不为空,则渲染方块,否则渲染空字符。之后,渲染内边框`verticalChar`,补充空格和外边框`verticalChar`。

4.2. 渲染nextBlock

从游戏区第一行开始,我们需要渲染`nextBlock`部分,因此实现逻辑如下:```javascript
// drawNext
if (y >= 0 && y <= 4) {row += this.createChar(1);rowLength += 1;if (next) {let xStart = 0;let xEnd = 0;next.points.forEach((point) => {if (xStart === 0 || point.x < xStart) {xStart = point.x;}if (xEnd === 0 || point.x > xEnd) {xEnd = point.x;}});for (let x = xStart; x <= xEnd; x++) {const point = next.points.find((p) => p.x === x && p.y === y);let consoleChar: ConsoleChar | null = point? new ConsoleChar(this.blockChar): null;if (point) {this.theme.nextPointStyle(consoleChar, point);}row += consoleChar ? consoleChar.ch : " ";rowLength += 1;}}
}
```
需要说名的是`nextBlock`的x坐标并不是从0开始,需要先找到x坐标的最小值和最大值,然后再依次渲染对应的行。比如Block T,如果形状如下:```javascript
// 口
// 口口
// 口
```那么,`xStart`和`xEnd`的差值为1。y只有0-2是有效的行,其余均渲染为空格。

4.3. 渲染游戏状态

第7行,渲染游戏状态

```javascript
else if (y === 6) {const { status } = game;if (status === GameStatus.PAUSE) {row += this.createChar(1) + this.getStatusLine("游戏暂停");rowLength += 5;} else if (status === GameStatus.OVER) {row += this.createChar(1) + this.getStatusLine("游戏结束");rowLength += 5;} else if (status === GameStatus.STOP) {row += this.createChar(1) + this.getStatusLine("游戏停止");rowLength += 5;}
}
```

4.4. 渲染游戏帮助

从第9行开始,渲染游戏的帮助信息:

```javascript
else if (y >= 8) {const messsage = this.createChar(1) + this.getHelpMessage(y - 8);row += messsage;rowLength += messsage.length;
}
```
  1. 渲染内边框的下边框
let line2 =this.getOutterLine(this.verticalChar) +this.getInnerLine(this.leftBottomChar);
for (let x = 0; x < xSize; x++) {const oneBlockItem = current?.points.find(item => item.x === x);if (oneBlockItem) {line2 += this.getInnerLine(bold(this.horizonalChar))} else {line2 += this.getInnerLine(this.horizonalChar)}
}
line2 += this.getInnerLine(this.rightBottomChar) +this.createChar(this.rightWidth) +this.getOutterLine(this.verticalChar);
printArray.push(line2);

逻辑与渲染内边框的上边类似。

  1. 渲染外边框的下边框
const outLine2 = this.getOutterLine(this.leftBottomChar +this.createChar(xSize + 2 + this.rightWidth, this.horizonalChar) +this.rightBottomChar
);
printArray.push(outLine2);

小结

本章主要讲述如何将游戏数据渲染成画面,以及渲染的一些通用的原理。渲染的本质是绘图,即告诉显示器如何绘制图像,通过不停的更新绘图实现动态的交互的效果。

详细内容可以关注在git上查看: https://github.com/shushanfx/tetris
也可以关注我的git账号: https://github.com/shushanfx

接下来我将从如下几个方面来阐述:

  • 手撸俄罗斯方块——游戏主题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/45519.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DWG文件发布至IIS后无法下载和预览解决办法

问题描述 DWG文件发布至IIS后无法下载和预览 原因分析&#xff1a; iis里面需要添加扩展 解决方案&#xff1a; 在服务器端IS属性的HTTP头下的MIME内容中添加扩展名“.dwg” MIME类型填入application/acad

29.PLL(锁相环)-IP核的调用

&#xff08;1&#xff09;PLL IP核的简介&#xff1a; Phase Locked Loop&#xff0c;即锁相环&#xff0c;是最常用的IP核之一&#xff0c;其性能强大&#xff0c;可以对输入到FPGA的时钟信号进行任意分频、倍频、相位调整、占空比调整&#xff0c;从而输出一个期望时钟。锁相…

JVM学习(day1)

JVM 运行时数据区 线程共享&#xff1a;方法区、堆 线程独享&#xff08;与个体“同生共死”&#xff09;&#xff1a;虚拟机栈、本地方法栈、程序计数器 程序计数器 作用&#xff1a;记录下次要执行的代码行的行号 特点&#xff1a;为一个没有OOM&#xff08;内存溢出&a…

C语言:指针详解(4)

作者本人由于大一下学期事情繁多&#xff0c;大部分时间都在备赛&#xff0c;没有时间进行博客撰写&#xff0c;如今已经到了暑假时间&#xff0c;作者将抓紧每一天的时间进行编程语言的学习&#xff0c;由于目前作者已经进行到了C的学习&#xff0c;C语言阶段的学习与初阶数据…

Tensorflow入门实战 T08-Vgg16网络进行猫狗识别

目录 1、前言 2、代码 3、运行结果 4、反思 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 1、前言 本周学习内容为&#xff0c;采用自己设置的vgg-16网络进行猫狗识别&#xff0c;并非官网提供的…

【公益案例展】亚运天穹——践行亚运理念,筑牢安全防线

‍ 安恒信息公益案例 本项目案例由安恒信息投递并参与数据猿与上海大数据联盟联合推出的 #榜样的力量# 《2024中国数据智能产业最具社会责任感企业》榜单/奖项”评选。 大数据产业创新服务媒体 ——聚焦数据 改变商业 杭州第19届亚运会是中国第三次举办亚洲最高规格的国际综合…

217.贪心算法:加油站(力扣)

代码解决 class Solution { public:int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {int curtotol 0; // 当前累积油量int tatol 0; // 总的油量减去总的花费油量int start 0; // 起始加油站的索引// 遍历所有加油站for (int i 0; i &…

5款文案生成神器,自动一键生成原创文案

文案在我们的生活中随处可见&#xff0c;好的文案内容不仅可以为企业带来销售转化&#xff0c;而且还能提升品牌的影响力&#xff0c;因此文案的重要性可想而知&#xff0c;对于文案创作者来说&#xff0c;写作好的文案不是轻松容易的事&#xff0c;但如果把这个任务交给文案生…

Python中的null是什么?

在知乎上遇到一个问题&#xff0c;说&#xff1a;计算机中的「null」怎么读&#xff1f; null正确的发音是/n^l/&#xff0c;有点类似四声‘纳儿’&#xff0c;在计算机中null是一种类型&#xff0c;代表空字符&#xff0c;没有与任何一个值绑定并且存储空间也没有存储值。 P…

MySQL CONCAT函数的简单使用

CONCAT函数用于将mysql中查询多列的值拼成一列显示&#xff0c; 使用示例&#xff1a; SELECT CONCAT(attr_name,"&#xff1a;",attr_value) FROM pms_sku_sale_attr_value WHERE sku_id1; 上面SQL语句使用CONCAT函数将attr_name、attr_value两列的值拼成一列&am…

动态sql 单选变多选

实体类 添加数组存储值 private ArrayList tssjfjList; <!-- <if test"tssjfj ! null and tssjfj ! ">and tssjfj #{tssjfj}</if>--><if test"tssjfjList ! null and tssjfjList.size() > 0">AND tssjfj IN<fo…

JeecgBoot 前端 vue3 项目,配置项目多页面入口

前端 vue3配置项目多页面入口 1.项目根目录新建home.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><…

Rust Hello

首先还是安装&#xff1a; 一定要换源&#xff0c;否则真的太慢了。 curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh 就是~/.cargo/config [source.crates-io] # 替换成你偏好的镜像源 replace-with tuna# 清华大学 5mb [source.tuna] registry "htt…

数字信号处理教程(4)—— 离散傅里叶变换DFT

今天继续数字信号处理的学习&#xff0c;本次我们将来聊聊离散傅里叶变换DFT(Discrete Fourier Transform)。DFT是数字信号处理领域中应 用最为广泛的离散变换。DFT将一个序列x(n)映射到频率域。DFT 的许多性质都与对模拟信号进行傅里叶变换的性质相同。里面包含着大量的算法值…

论文研读:ViT-V-Net—用于无监督3D医学图像配准的Vision Transformer

目录 摘要 介绍 方法 VIT-V-Net体系结构 损失函数 图像相似性度量 变形场正则化 结果与讨论 摘要 在过去的十年里&#xff0c;卷积神经网络(ConvNets)在各种医学成像应用中占据了主导地位并取得了最先进的性能。然而&#xff0c;由于缺乏对图像中远程空间关系的理解&a…

Gitlab CI/CD介绍

基本概念 GitLab CI/CD&#xff08;持续集成/持续部署&#xff09;流水线是GitLab平台提供的一项强大功能&#xff0c;旨在通过自动化构建、测试和部署过程&#xff0c;提高开发团队的效率和软件发布的质量。 CI&#xff08;Continuous Integration&#xff09;&#xff1a;持续…

QT5.12.9 通过MinGW64 / MinGW32 cmake编译Opencv4.5.1

一、安装前准备: 1.安装QT,QT5.12.9官方下载链接:https://download.qt.io/archive/qt/5.12/5.12.9/ QT安装教程:https://blog.csdn.net/Mark_md/article/details/108614209 如果电脑是64位就编译器选择MinGW64,32位就选择MinGW32,我的是MinGW64。 2.opencv源码下载:h…

linux登入提示信息

目录 1.Linux 登录提示信息在操作系统中扮演着重要的角色 安全性提醒 欢迎信息 系统状态通知 政策和使用条款 技术支持信息 更新和变更通知 2.配置文件介绍 3.编辑配置文件 4.效果展示 修改前 修改后 “如果您在解决类似问题时也遇到了困难&#xff0c;希望我的经…

14-50 剑和诗人24 - 开源 AI 的下一个重大飞跃:多个小模型与大模型相媲美

介绍 大型语言模型 (LLM) 领域最近取得了快速进展&#xff0c;GPT-4、PaLM-2、Llama-2 等模型正在突破 AI 对语言处理能力的界限。然而&#xff0c;只有少数几家大型科技公司拥有训练包含数千亿个参数的模型所需的大量计算资源&#xff0c;才能使用最大的模型。 作为回应&…

鼠标怎么挑选

我们平时工作中经常要用到鼠标&#xff0c;那么哪种鼠标比较好。我们今天就来研究下。 手感要好 鼠标最重要的就是手感。这个看参数看不出来。最好能到实体店里面去体验一下&#xff0c;自己上手试一下。 如果现在都是在网上买了&#xff0c;去实体店过于麻烦&#xff0c;并且…