仿制 Google Chrome 的恐龙小游戏
通过仿制 Google Chrome 的恐龙小游戏,我们可以掌握如下知识点:
- 灵活使用视口单位
- 掌握绝对定位
- JavaScript 来操作 CSS 变量
- requestAnimationFrame 函数的使用
- 无缝动画实现
页面结构
实现页面结构
通过上述的页面结构我们可以知道,此游戏中需要有如下的元素:
- 游戏世界
- 小恐龙
- 分数
- 游戏开始的信息提示
- 地面
- 仙人掌
然后构建对应的页面结构
<div class="world"><div class="score">0</div><div class="start-screen">按任意键开始</div><img src="./image/ground.png" class="ground" /><img src="./image/ground.png" class="ground" /><img src="./image/dino-stationary.png" class="dino" />
</div>
使用绝对定位完成页面元素的布局
定义好元素后,我们可以编写对应的样式:
.world {position: relative;overflow: hidden;/* 这里我们先使用固定值来设置,随后使用JS来动态设置值 */width: 100%;height: 300px;
}.score {position: absolute;top: 1vmin;right: 1vmin;font-size: 3vmin;
}.start-screen {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);font-size: 3vmin;
}.hine {display: none;
}/* 使用CSS变量进行占位,然后使用JS来控制变量计算 */
.ground {--left: 0;position: absolute;width: 300%;bottom: 0;left: calc(var(--left) * 1%);
}.dino {--bottom: 0;position: absolute;left: 0;height: 30%;bottom: calc(var(--bottom) * 1%);
}.cactus {--left: 0;position: absolute;left: calc(var(--left) * 1%);height: 30%;bottom: 0;
}
使用 JS 来监听视口大小改变,从而修改游戏世界元素的宽高
为了能够很好的适配所有的设备,我们需要使用 JS 来监听视口的大小改变,从而动态修改页面的元素大小具体的步骤如下。
首先在游戏世界元素中添加如下属性:
<div class="world" data-world></div>
编写 JS 代码:
const WORLD_WIIDTH = 100;
const WORLD_HEIGHT = 30;const worldElem = document.querySelector("[data-world]");setPixelToWorldScale(); // 初始化游戏世界的大小
window.addEventListener("resize", setPixelToWorldScale);function setPixelToWorldScale() {let worldToPixeScale = 0;/*** 判断视口的大小是否大于我们自定义的常量,这样做主要是保证我们的游戏世界的元素大小在视口的中央** 当视口宽度大于高度时,判断结果为false,就取高度做计算* 当视口宽度小于高度时,判断结果为true,就取宽度做计算*/if (window.innerWidth / window.innerHeight < WORLD_WIIDTH / WORLD_HEIGHT) {worldToPixeScale = window.innerWidth / WORLD_WIIDTH;} else {worldToPixeScale = window.innerHeight / WORLD_HEIGHT;}worldElem.style.width = `${WORLD_WIIDTH * worldToPixeScale}px`;worldElem.style.height = `${WORLD_HEIGHT * worldToPixeScale}px`;
}
使用 requestAnimationFrame 编写对应的更新动画函数
整体的页面布局好以后,我们就可以开始对恐龙、地面、仙人掌和分数进行动画的渲染。但是首先我们先用编写控制动画运行的函数。
我们要编写动画函数的话,可以使用requestAnimationFrame
函数来帮我们实现。
window.requestAnimationFrame()
函数它会告诉浏览器你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。具体的实现代码如下:
let lastTime; // 上一次动画执行时间
function update(time) {// 动画开始执行时,lastTime是为null,所以需要对齐赋值if (lastTime == null) {lastTime = time;window.requestAnimationFrame(update);return;}const delta = time - lastTime; // 计算上一次动画时间和本次动画时间差console.log(delta);lastTime = time;window.requestAnimationFrame(update);
}
实现地面动画
说明: 以下核心代码中会出现一些辅助函数,为了不让篇幅过长,所以这里就不在展示辅助函数代码,可以在下载完整的代码中进行查看。
实现地面移动的动画其实很简单,因为我们地面的样式是使用绝对定位,并且地面元素有两个,所以只要让两个地面元素交替向左移动就可以实现无缝的动画。
为了提示游戏的难度,游戏会随着时间的推移地面的移动速度会越来越快,具体核心代码如下:
// 地面相关JS
const SPEED = 0.05; // 地面移动量(移动量越大速度越快)
const grounds = document.querySelectorAll("[data-ground]");// 重置地面位置
export function setupGround() {setCustomProperty(grounds[0], "--left", 0);setCustomProperty(grounds[1], "--left", 300);
}/*** 修改每帧地面动画* @param {number} delta 每帧动画的时间差* @param {number} speedScale 难度系数*/
export function updateGround(delta, speedScale) {grounds.forEach((ground) => {incrementCustomProperty(ground, "--left", delta * speedScale * SPEED * -1); // 向左移动地面// 当地面元素左移到-300像素时,需要把对应的地面元素重置到第二个地面元素后if (getCustomProperty(ground, "--left") <= -300) {// 因为有两个地面元素,所以长度为两个地面元素的长度incrementCustomProperty(ground, "--left", 600);}});
}
// 核心游戏JS// 动画执行函数
function update(time) {if (lastTime == null) {lastTime = time;window.requestAnimationFrame(update);return;}const delta = time - lastTime;updateSpeedScale(delta);updateGround(delta, speedScale);lastTime = time;window.requestAnimationFrame(update);
}// 修改游戏难度系数(随着时间的推移难度系数值越大,地面的移动速度越快)
function updateSpeedScale(delta) {speedScale += delta * SPEED_SCALE_INCREASE;
}
实现小恐龙相关动画
实现小恐龙关键帧替换
在这个游戏中我们的小恐龙是由两个关键帧交替实现动画效果的,所以我们需要在一帧动画期间交替替换小恐龙的两个关键帧,从而绘制小恐龙跑步的动画,具体核心代码如下:
/*** 修改小恐龙的动画* @param {number} delta 每帧动画的时间差* @param {number} speedScale 难度系数*/
export function updateDino(delta, speedScale) {handleRun(delta, speedScale);
}/*** 小恐龙运动动画* @param {number} delta 每帧动画的时间差* @param {number} speedScale 难度系数* @returns*/
function handleRun(delta, speedScale) {// 判断小恐龙是否跳起,跳起的话关键帧只能固定一个if (isJumping) {dinoElem.src = `./imgs/dino-stationary.png`;return;}/*** 因为一帧动画可以执行很多次,所以我们需要对每帧执行完成后交替更换小恐龙的关键帧图片* FRAME_TIME用于每帧小恐龙交替拆分的关键值*/if (currentFrameTime >= FRAME_TIME) {dinoFrame = (dinoFrame + 1) % DINO_FRAME_COUNT;dinoElem.src = `./imgs/dino-run-${dinoFrame}.png`;currentFrameTime -= FRAME_TIME;}currentFrameTime += delta * speedScale;
}
实现小恐龙跳起动画
小恐龙的跳起主要是在 Y 轴上进行上下运动,并且为了达到最好的动画效果,我们会声明两个变量用于控制跳起动画的效果。具体核心代码如下:
/*** 跳起动画* @param {number} delta 每帧动画的时间差,* @returns 小恐龙跳起跳起的高度*/
function handleJump(delta) {if (!isJumping) return;incrementCustomProperty(dinoElem, "--bottom", yVelocity * delta);// 接触到地面后重置相关参数if (getCustomProperty(dinoElem, "--bottom") <= 0) {setCustomProperty(dinoElem, "--bottom", 0);isJumping = false;}yVelocity -= GRAVITY * delta;
}// 监听小恐龙跳起事件
function onJump(e) {if (e.code !== "Space" || isJumping) return;yVelocity = JUMP_SPEED;isJumping = true;
}
实现仙人掌动画
仙人掌是在一定时间间隔内在游戏世界中创建出来,并且动画移动效果跟地面一样。所以我们在实现此功能的时候最核心的业务就是在随机间隔内生成对应的仙人掌,并执行相应的动画。具体的核心代码如下:
// 创建仙人掌
function createCactus() {const cactus = document.createElement("img");cactus.dataset.cactus = true;cactus.src = "./imgs/cactus.png";cactus.classList.add("cactus");setCustomProperty(cactus, "--left", 100);worldElem.append(cactus);
}// 生成仙人掌并执行相应动画
export function updateCactus(delta, speedScale) {document.querySelectorAll("[data-cactus]").forEach((cactus) => {incrementCustomProperty(cactus, "--left", delta * speedScale * SPEED * -1);if (getCustomProperty(cactus, "--left") <= -100) {cactus.remove();}});// 判断是否要生成下一个仙人掌if (nextCactusTime <= 0) {createCactus();// 生成下一个仙人掌的时间间隔nextCactusTime =randomNumberBetween(CACTUS_INTERVAL_MIN, CACTUS_INTERVAL_MAX) /speedScale;}nextCactusTime -= delta;
}// 重置仙人掌
export function setupCactus() {nextCactusTime = CACTUS_INTERVAL_MIN;// 移除是有仙人掌document.querySelectorAll("[data-cactus]").forEach((cactus) => {cactus.remove();});
}
游戏结束评定
游戏的结束判断就是小恐龙是否碰到仙人掌,所以我们首先需要添加获取小恐龙和仙人掌的方法,具体函数如下:
// 获取仙人掌
export function getCactusRects() {return [...document.querySelectorAll("[data-cactus]")].map((cactus) => {return cactus.getBoundingClientRect();});
}// 获取小恐龙
export function getDinoRect() {return dinoElem.getBoundingClientRect();
}// 判断是否游戏结束
function checkLose() {const dinoRect = getDinoRect();return getCactusRects().some((rect) => isCollision(rect, dinoRect));
}// 通过判断小恐龙和仙人掌是否碰撞
function isCollision(rect1, rect2) {return (rect1.left < rect2.right &&rect1.top < rect2.bottom &&rect1.right > rect2.left &&rect1.bottom > rect2.top);
}
完整代码下载
完整代码下载
相关文章:

仿制 Google Chrome 的恐龙小游戏
通过仿制 Google Chrome 的恐龙小游戏,我们可以掌握如下知识点: 灵活使用视口单位掌握绝对定位JavaScript 来操作 CSS 变量requestAnimationFrame 函数的使用无缝动画实现 页面结构 实现页面结构 通过上述的页面结构我们可以知道,此游戏中…...
Redis面试题(五)
文章目录 前言一、使用过 Redis 做异步队列么,你是怎么用的?有什么缺点?二、 什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?总结 前言 使用过 Redis 做异步队列么,你是怎么用的…...

组队竞赛(int溢出问题)
目录 一、题目 二、代码 (一)没有注意int溢出 (二)正确代码 1. long long sum0 2. #define int long long 3. 使用现成的sort函数 一、题目 二、代码 (一)没有注意int溢出 #include <iostream&g…...

Swift SwiftUI 隐藏键盘
如果仅支持 iOS 15 及更高版本,则可以通过聚焦和取消聚焦来激活和关闭文本字段的键盘。 在最简单的形式中,这是使用 FocusState 属性包装器和 focusable() 修饰符完成的-第一个存储一个布尔值,用于跟踪第二个当前是否被聚焦。 Code struct C…...
Python与数据分析--Pandas-1
目录 1.Pandas简介 2.Series的创建 1.通过数组列表来创建 2.通过传入标量创建 3.通过字典类型来创建 4.通过numpy来创建 3.Series的索引和应用 1. 通过index和values信息 2. 通过切片方法获取信息 4.DataFrame的创建 1.直接创建 2.矩阵方式创建 3.字典类型创建 5.…...

如何完美通过token获取用户信息(springboot)
1. 什么是Token? 身份验证令牌(Authentication Token):在身份验证过程中,“token” 可以表示一个包含用户身份信息的令牌。 例如 Token(JWT)是一种常见的身份验证令牌,它包含用户的…...

2023 “华为杯” 中国研究生数学建模竞赛(B题)深度剖析|数学建模完整代码+建模过程全解全析
华为杯数学建模B题 当大家面临着复杂的数学建模问题时,你是否曾经感到茫然无措?作为2021年美国大学生数学建模比赛的O奖得主,我为大家提供了一套优秀的解题思路,让你轻松应对各种难题。 让我们来看看研赛的B题呀~! 问…...
文件相关工具类
文章目录 1.MultipartFile文件转File2.读取文件(txt、json)3.下载网络文件4.压缩文件 1.MultipartFile文件转File public File transferToFile(MultipartFile multipartFile) { // 选择用缓冲区来实现这个转换即使用java 创建的临时文件 使用 MultipartFile.transferto()…...

18795-2012 茶叶标准样品制备技术条件
声明 本文是学习GB-T 18795-2012 茶叶标准样品制备技术条件. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了各类茶叶(除再加工茶)标准样品的制备、包装、标签、标识、证书和有效期。 本标准适用于各类茶叶(除再加工茶)感官品质…...
C++11互斥锁的使用
是C11标准库中用于多线程同步的库,提供互斥锁(mutex)及其相关函数。 以下是一些基本的使用示例: 1.创建和销毁互斥锁 #include <mutex>std::mutex mtx;2.加锁 std::lock_guard<std::mutex> lock(mtx); // 加锁 // 或者 mtx.lock(); //…...
unity 桌面程序
using System; using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using UnityEngine; public class chuantou : MonoBehaviour { [DllImport(“user32.dll”)] public static extern int MessageBox(IntPtr hwnd,string t…...

echarts统一纵坐标y轴的刻度线,刻度线对齐。
要求: 纵坐标刻度线对齐;刻度间隔为5;去掉千位默认的逗号;刻度最小是0. 效果图: 代码: yAxis: [{type: "value",position: "left",name: "kW",offset: 100,nameTextStyle:…...
一个数据库版本兼容问题
mysql旧的版本号是:5.3.10 本机版本号是:8.0.22 报错:“com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create” 1.程序里做兼容: <dependency><groupId>mysql</groupId>…...
学习Nano编辑器:入门指南、安装步骤、基本操作和高级功能
文章目录 使用Nano编辑器入门指南引言1.1 关于Nano编辑器1.2 Nano的起源和特点 安装Nano2.1 在Debian/Ubuntu系统上安装Nano2.2 在CentOS/RHEL系统上安装Nano2.3 在其他Linux发行版上安装Nano 启动Nano3.1 命令行启动Nano3.2 打开文件 Nano的基本操作4.1 光标移动和选择文本4.2…...

在北京多有钱能称为富
背景 首先声明,此讨论仅限个人的观点,因为我本身不富嘛,所以想法应该非常局限。 举个栗子 富二代问我朋友,100~1000w之间,推荐一款车? 一开始听到这个问题的时候,有被唬住,觉得预…...
Chrome扩展程序开发随记
在Chrome浏览器向正被浏览的外网网页植入自定义JS脚本 为了实现如标题的目的,需要开发一个Chrome扩展程序。接下来内容是实现简要步骤: 一、新建文件夹,命名为项目名,如“MyPlugin”。 二、进入文件夹内,新建名为“…...

使用命令行快速创建Vite项目
一、构建项目 在终端中使用如下命令行: npm create vite 二、定义项目名称 三、选择项目类型 Vanilla是我们常用的JavaScript,Vue和React是常用前端框架,可以根据自己的需要进行选择 通过上下键进行选择,按下回车进行确认 创建…...
int *a, int **a, int a[], int *a[]的区别
int *a; ---定义一个指向整型变量的指针a int **a; ---定义一个指向整型变量指针的指针a int a[]; ---定义一个整型变量数组a int *a[]; ---定义一个指向整型变量指针的数组a...

leetcode100----双指针
283. 移动零 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 请注意 ,必须在不复制数组的情况下原地对数组进行操作。 示例 1:输入: nums [0,1,0,3,12] 输出: [1,3,12,0,0] 示例 2:输入: nums …...
ORM基本操作
ORM基本操作 基本操作包括增删改查操作,即(CRUD操作) CRUD是指在做计算处理时的增加(Create)、读取查询(Read)、更新Update)和删除(Delete) ORM CRUD 核心-> 模型类管理器对象 每个继承自 models.Model 的模型类,都会有一个 objects 对象被同样继…...

大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
mongodb源码分析session执行handleRequest命令find过程
mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程,并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令,把数据流转换成Message,状态转变流程是:State::Created 》 St…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...

C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...

vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
uniapp 集成腾讯云 IM 富媒体消息(地理位置/文件)
UniApp 集成腾讯云 IM 富媒体消息全攻略(地理位置/文件) 一、功能实现原理 腾讯云 IM 通过 消息扩展机制 支持富媒体类型,核心实现方式: 标准消息类型:直接使用 SDK 内置类型(文件、图片等)自…...

2025年- H71-Lc179--39.组合总和(回溯,组合)--Java版
1.题目描述 2.思路 当前的元素可以重复使用。 (1)确定回溯算法函数的参数和返回值(一般是void类型) (2)因为是用递归实现的,所以我们要确定终止条件 (3)单层搜索逻辑 二…...