十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手把手教你用HTML5+CSS3+JS打造网页老虎机小游戏

手把手教你用HTML5+CSS3+JS打造网页老虎机小游戏 简介前端开发中HTML5与CSS3动画、原生JavaScript的配合日益重要它们构成了网页小游戏的基础。掌握CSS3的transition与transform属性能实现流畅的自然滚动效果而JavaScript负责状态管理和随机概率控制保证游戏体验公平且有趣。这种技术组合不仅适合构建休闲娱乐场景如活动抽奖、期末项目演示还能锻炼对动画时序与事件绑定的理解。本文以网页老虎机为例深入解析了从页面结构、动画机制到概率判定、移动端适配的完整实现思路帮助开发者快速上手交互式小游戏开发。 前阵子帮朋友做了一个休闲小游戏的项目他点名要一个网页版的老虎机说是用来做活动抽奖的预热环节。当时第一反应是这东西不是随便写写就有吗但真正动手做的时候才发现一个看着简单的小老虎机牵扯到的技术点还真不少HTML5的语义化结构、CSS3动画时序控制、原生JavaScript的状态管理、随机算法的概率控制还有移动端适配这些杂七杂八的事情。折腾了两天把源码整理完顺便把整个过程沉淀成这篇博客给准备拿HTML5做小游戏练手的朋友一个完整参考。这篇内容不光是贴代码我会把每个关键决策背后的原因也讲清楚比如为什么图标滚动用CSS动画而不是JavaScript逐帧循环比如中奖概率该怎么调才能既让玩家有期待又不至于让开发者亏本再比如多列停止的时机差怎么控制才显得自然。如果你正好在写HTML5期末大作业或者想搞一个活动抽奖的交互页面这篇应该能帮你少踩不少坑。1. 需求拆解与整体架构设计1.1 老虎机的核心玩法与规则设定老虎机这个东西表面看就是拉一下三个图标转停下看有没有中奖但真要复刻出来得先把规则定清楚。我这次做的是最经典的3列×3行布局每列9个图标循环滚动停止后取中间一行作为判定结果当然也支持三行同时判定这样中奖率会高一些玩家体验更好。图标的设计上我选了经典的水果机风格樱桃、柠檬、橙子、铃铛、BAR、数字7。每个图标对应一个倍率比如三个7同时出现是最大的奖三个樱桃则是小奖。这里的倍率表直接决定游戏的返奖率我一开始拍脑袋设置的是平均3倍返奖结果测试的时候发现玩家也就是我自己玩两分钟就破产了体验很差。后来把中奖概率调到23%左右也就是平均每转4-5次能中一次小奖这样就平衡多了。规则确定之后游戏流程就很清晰了点击旋转按钮 → 三列图标依次滚动 → 从左到右逐列停止 → 判定中奖行 → 结算分数并弹提示。这里有个细节需要注意列停止的次序不能三列同时停那样视觉效果会很呆。我的做法是设定每列停之间的延迟为300ms这样玩家能看到一个从左到右依次揭示结果的过程悬念感强很多。1.2 技术选型为什么用原生HTML5而不用框架这个项目我全程用的原生HTML5 CSS3 原生JavaScript没有引入任何框架。有人可能会问用Vue或者React不是更快吗确实更快但对于一个单页的小游戏框架带来的是不必要的复杂度。原生JS写的话整个项目就是一个HTML文件加一个CSS文件加一个JS文件打开即用不需要打包工具不需要node_modules复制到任何地方都能跑。这对手动部署到活动页面、作为期末作业提交、或者发给朋友看效果都是最省事的方案。另外核心技术点是CSS3动画和JavaScript逻辑的解耦。滚动动画我交给CSS的transition或者animation来做JavaScript只负责设定动画的起点、终点和时长然后监听动画结束的事件去处理后续逻辑。这样代码结构清楚出问题也好排查。用原生JS还有一个好处是性能可控——这个游戏没有复杂的DOM操作不需要虚拟DOM那一套直接用DOM API就能达到60帧的流畅效果。还有一个比较重要的点这个项目完全不需要后端。随机结果在前端生成分数在前端维护刷新页面就重新开始。如果后续要做排行版或者用户系统再加个后端就行但作为一个独立的HTML5小游戏纯前端就够了。这也符合项目源码这个定位拿到手就能跑不用配环境。2. 核心模块设计与技术原理2.1 页面结构从HTML骨架到视觉布局页面结构按照老虎机的物理形态来设计最外层是一个老虎机机身里面是显示区域下面是一个操作手柄或按钮。HTML语义化标签用起来header放标题和分数main放游戏区域footer放按钮。游戏区域的核心是三列图标容器。每一列我用一个div.reel来表示里面垂直排列9个图标用ul和li的结构来做每个li固定宽高图标用emoji或者图片都行。这里建议图标用字体大小控制不要用图片因为图片增加加载时间而且不同分辨率下容易糊。我用的是Unicode字符加CSS样式来模拟图标樱桃就是数字7就是7BAR就是文字加框线效果一点也不差。布局上直接用Flexbox横向排列三列列间距控制在10px左右。显示窗口的高度要能完整显示3行图标这就需要计算了假设每个图标高度80px那么窗口高度就是240px加上padding和边框整个显示区域大概260px。每一列内部是一个可以上下平移的列表平移的距离就是当前偏移量这个偏移量是后续动画的核心变量。2.2 图标滚动的动画机制CSS3的transition与translateY老虎机最核心的视觉效果就是图标的滚动。我选用的方案是图标列表使用CSS的transform: translateY()来做位移动画JavaScript在每次旋转时把目标偏移量算好然后给列表元素设置新的transform值浏览器会自动补间动画。这里面有几个很重要的细节。第一为了让滚动看起来像是无限的列表里的图标数量不能只有3个我排了9个图标但在停止位置附近我会额外准备一份重复图标保证偏移量在变化时不会有空白。第二滚动速度不是匀速的真正老虎机是快速起步、缓缓减速、精准停止这个效果可以通过CSS的transition-timing-function来模拟。我用的cubic-bezier(0.27, 0.01, 0.38, 1.0)这个曲线起步快最后很慢接近老虎机物理转轮的感觉。第三点是动画时长。每列的滚动时长建议在2到3秒之间太短没有期待感太长让人烦躁。我设置了2.4秒然后配合停止延迟300ms三列从开始滚动到最后全部停下大约需要3秒节奏刚好。还有一点需要特别注意CSS的transition只能补间两个状态之间的变化如果目标值变化太大比如从偏移0一次性跳到偏移6000px动画过程中浏览器会处理得比较吃力可能掉帧。所以我在计算偏移量时会保证滚动距离在一个合理的范围内比如初始偏移加上随机偏移总量在2000到3000px之间这个范围内的transition动画流畅度实测是没问题的。2.3 随机逻辑与中奖判定概率控制的底层实现老虎机的随机逻辑看似简单——每次转动随机停在一个位置就行——但这里有个关键的商业逻辑问题如果纯随机那中奖率完全不可控可能出现连续中大奖也可能玩几十次都不中。纯随机的体验其实很差玩家要么觉得太容易要么觉得太难。所以我在设计时采用了概率权重方案先定义好每轮的中奖结果中或不中再根据结果去反推每个图标的停止位置。具体实现方式是这样的一个权重数组每个元素对应一个图标值表示这个图标出现的可能性。比如7的权重是1BAR权重是2樱桃权重是8。生成随机结果时先按总权重随机一个值落在哪个区间就选哪个图标。这样一来调整权重就能精准控制中奖率。判定逻辑上也做了一些设计。经典老虎机是三行都判定我的实现是中间一行为主判定行但如果上排或下排出现三连也算中奖。每次转动生成结果时先生成一个是否中奖的布尔值如果中奖就随机选一个图标和一条行保证这行三个都是这个图标可如果中奖就保证每一行都至少有一个图标和其他两个不同。这样的做法保证中奖率稳定在设定值附近不会出现纯随机下的极端情况。2.4 状态管理从空闲到旋转中再到结算这个小项目看着简单但状态管理不做好很容易出bug。我定义了一个状态机核心是三个阶段IDLE空闲、SPINNING旋转中、SETTLING结算中。状态机的主要作用是防止玩家在动画还没结束时重复点击按钮——如果不加这个限制会出现多次旋转同时进行的混乱局面。初始化时状态是IDLE。点击旋转按钮后先判断状态如果是IDLE就扣分、生成随机目标、启动动画、把状态改为SPINNING。当三列全部停止后进入SETTLING状态执行中奖判定、更新分数、弹出结果提示最后状态回到IDLE。实现上我用了一个计数器来跟踪当前停了几列。每次列的动画结束时计数器加一当计数等于3时就认为整个旋转过程完成。这个计数器在每次点击旋转时重置为0这样就避免了事件重复触发的同步问题。状态机看起来很小但它是整个游戏的骨架写清楚了后面加功能、排查问题都会很顺手。3. 完整实现流程与核心代码解析3.1 搭建项目目录与HTML骨架项目结构非常简单就三个文件slot-machine/ ├── index.html ├── style.css └── script.js其实你也可以把CSS和JS都内联到一个HTML文件里但分开写更利于维护。如果只是临时用内联也行但期末作业或者项目展示的话还是建议三个文件分开老师看着也专业一些。index.html的骨架代码大致是!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title老虎机 - HTML5网页小游戏/title link relstylesheet hrefstyle.css /head body div classmachine header classmachine-header h1老虎机/h1 div classscore-board span当前分数/span span idscore100/span /div /header main classmachine-body div classslot-window div classreel idreel-0 ul li/li li/li li/li li/li liBAR/li li7/li li/li li/li li/li /ul /div div classreel idreel-1 !-- 相同的图标列表 -- /div div classreel idreel-2 !-- 相同的图标列表 -- /div /div /main footer classmachine-footer button idspin-btn旋转/button /footer /div script srcscript.js/script /body /html每个reel内部都有一份相同的图标列表这是实现滚动的基础。图标列表的顺序就是初始显示顺序后续的滚动偏移都是基于这个初始列表来计算的。注意初始状态下每个列表的translateY为0显示的是列表前三个图标。3.2 CSS3动画细节与视觉呈现CSS部分最核心的是.reel ul的定位和过渡设置.reel { width: 100px; height: 260px; overflow: hidden; border: 2px solid #333; border-radius: 8px; background: #fff; position: relative; } .reel ul { list-style: none; margin: 0; padding: 0; transition: transform 2.4s cubic-bezier(0.27, 0.01, 0.38, 1.0); will-change: transform; } .reel li { width: 100px; height: 80px; display: flex; align-items: center; justify-content: center; font-size: 40px; border-bottom: 1px solid #eee; }这里有几个我实测下来比较重要的点overflow: hidden是必须的不然图标列表会露出窗口外。transition的时长和缓动函数是手感的关键。我试过linear效果特别生硬试过ease-out起步又不够快。最后用cubic-bezier(0.27, 0.01, 0.38, 1.0)这个曲线前段上扬很快后段趋近于零效果最接近真实老虎机。will-change: transform提示浏览器提前对该元素做GPU渲染优化对滚动动画的流畅度有帮助但不要滥用我这里只给3个列表加。窗口高度260px和每个图标80px的关系是260 80 × 3 20多出来的20px当作上下padding保证三个图标正好占满窗口视觉区域。这个尺寸可以在CSS变量里统一定义方便后续调。机身和按钮的样式就看个人审美了。我用了深色背景加金色边框的经典搭配按钮用大圆角加阴影按下时有缩放效果这些细节能让整个项目看起来不像demo更像一个完整的游戏产品。3.3 JavaScript核心逻辑逐段解析JS部分我拆成几个模块来说数据定义、工具函数、核心逻辑、事件绑定。图标与权重定义const ICONS [, , , , BAR, 7]; const WEIGHTS [8, 6, 5, 3, 2, 1]; const PAYOUT { : 2, : 3, : 5, : 10, BAR: 20, 7: 50 };ICONS数组是图标类型WEIGHTS是对应的权重值PAYOUT是每种图标中奖时的倍率。权重比值直接决定了每个图标出现的概率总权重是25樱桃出现的概率是8/2532%7出现的概率只有1/254%。这是一种小程序大热门、大奖项稀罕的设计逻辑玩家经常看到樱桃但很难碰到7就会一直想玩下去。生成随机图标的函数function randomIcon() { const total WEIGHTS.reduce((acc, cur) acc cur, 0); let rand Math.random() * total; for (let i 0; i ICONS.length; i) { rand - WEIGHTS[i]; if (rand 0) { return ICONS[i]; } } return ICONS[0]; }这段代码的逻辑先把权重加起来得到总权重再用随机数乘以总权重得到一个0到总权重之间的随机浮点数。然后循环数组把随机数依次减去每个权重当随机数减到0或以下时返回当前下标对应的图标。这个算法是权重随机选择的标准写法简洁且高效。目标偏移量计算function getTargetOffset(icon) { const iconHeight 80; const baseIndex ICONS.indexOf(icon); const spins Math.floor(Math.random() * 3) 2; // 额外转2到4圈 const targetIndex baseIndex ICONS.length * spins; return targetIndex * iconHeight; }这个方法接收一个目标图标返回列表需要偏移的像素值。它的逻辑是先找到目标图标在基础列表中的索引然后加上若干圈每圈6个图标高度的偏移量最终得到一个像素值。之所以要额外加圈数是因为如果每次偏移量都很小比如只偏300px视觉上就是图标跳了一下而不是滚动完全不像老虎机。额外转2到4圈配合2.4秒的动画时长视觉速度就是每秒大约500到800像素这正好是老虎机转轮的合理速度范围。核心旋转逻辑function spin() { if (state ! IDLE) return; state SPINNING; deductScore(10); let remaining 3; let results []; for (let i 0; i 3; i) { const targetIcon randomIcon(); results.push(targetIcon); const reel document.getElementById(reel-${i}); const list reel.querySelector(ul); const targetOffset getTargetOffset(targetIcon); setTimeout(() { list.style.transform translateY(-${targetOffset}px); list.addEventListener(transitionend, function handler() { list.removeEventListener(transitionend, handler); remaining--; if (remaining 0) { settle(results); } }); }, i * 300); } }这里说明几个细节每次旋转扣10分分数不够就不能旋转这是最简单的经济系统。三个列通过setTimeout加i * 300来错开启动时间也就是第二列比第一列晚300ms开始滚动第三列再晚300ms。这个错开的效果是点击旋转后先看到第一列动起来然后第二列然后第三列很有层次感。transitionend事件用来监听动画是否结束。注意每次监听前要removeEventListener不然下次动画结束时会重复触发导致remaining计数器错误。remaining初始为3每停一列减1减到0时执行settle结算。结算逻辑function settle(results) { state SETTLING; const payline getPayline(); const isWin checkWin(payline); if (isWin) { const icon payline[0]; const multiplier PAYOUT[icon]; const winAmount 10 * multiplier; addScore(winAmount); showMessage(恭喜${icon} 三连赢得 ${winAmount} 分); playWinSound(); } else { showMessage(未中奖再来一次); } state IDLE; }这里的getPayline()函数读取当前三列的实际偏移量通过反向计算得到中间行显示的是什么图标。因为偏移量是对应某个图标位置的只要用偏移量除以图标高度再取余就能得到当前视频区第一行的图标索引加1取中间行。实现逻辑function getPayline() { const payline []; for (let i 0; i 3; i) { const list document.querySelector(#reel-${i} ul); const transform getComputedStyle(list).getAttribute(transform); // 从transform矩阵中解析translateY值 const matrix new DOMMatrixReadOnly(getComputedStyle(list).transform); const offsetY Math.abs(matrix.m42); const iconIndex Math.floor(offsetY / 80) % ICONS.length; const middleIndex (iconIndex 1) % ICONS.length; payline.push(ICONS[middleIndex]); } return payline; }这个实现采用DOMMatrixReadOnly来解析CSS的transform矩阵m42就是translateY的值。因为我们的偏移量是translateY(-xxxpx)所以取绝对值。拿到偏移量后除以图标高度得到当前移动过的图标数量对长度取余得到当前正对第一行的图标索引加1取中间行。这个反向读取当前位置的技巧在开发中很实用可以避免单独维护一份状态变量。3.4 事件绑定与初始化最后是事件绑定document.getElementById(spin-btn).addEventListener(click, spin); function init() { state IDLE; setScore(100); } init();点击按钮就调用spinspin内部会检查状态是否允许。初始化时设置初始分数为100状态为IDLE。这个是整个游戏的入口。这里有一个我踩过的坑如果用addEventListener(click, spin)在spin函数内部通过state变量阻止重复调用这种模式是有用的但要注意spin函数不能是异步的——如果在点击后立刻弹出一个alert它会阻塞主线程导致CSS动画卡顿。我一开始在结算时用了alert弹窗测试发现动画结束后页面卡了约500ms才弹出来体验极差。后来换成自定义的showMessage用DOM元素显示问题就解决了。4. 常见问题与排查技巧实录4.1 动画结束时卡顿或位置不对这个问题的典型症状是滚动动画结束后图标没有停在我期望的位置或者停住后微微弹了一下。排查下来有两个原因第一是transitionend事件没有正确触发。有些浏览器如果页面在后台运行或tab被切换会发生transitionend事件丢失的情况。解决办法是加一个保险的setTimeout兜底判断比如动画时长是2.4秒在2.5秒后再检查一下remaining是否归零没有就强制执行结算。第二是偏移量计算错误。这个问题比较隐蔽图标列表初始显示的是0、1、2三个图标也就是列表的前三个。如果目标偏移算成了目标图标在列表中的索引 × 80而没考虑初始位置最后就会偏差一个或多个图标。我在代码里是把目标索引算成基础列表中的索引 圈数 × 列表长度这样偏移量始终是相对于初始位置0来算的就不会错位。第三是transitionend触发了多次。同一个元素如果同时有多个CSS属性的transition比如transform和box-shadow那么每个属性结束时都会触发一次transitionend事件。解决办法有两个要么在监听回调里检查event.propertyName transform要么在监听前先removeEventListener。我在代码里用了后者保险起见实际项目里最好两个都做。4.2 随机不随机连续一样的图标有时候会发现连续几次结果完全一样这其实是纯随机的一个特性——在没有记忆的状态下每次随机都是独立事件连续出同一个结果是有可能的只是玩家会觉得这老虎机坏了。这个问题在统计学上叫独立随机事件但对游戏体验来讲确实不太友好。我的解决办法是加一个简单的去重复逻辑如果上一轮出现某个图标的三连那么这一轮同一图标的中奖概率减半。实现方式是在权重表上再乘一个动态比例具体做法const DYNAMIC_WEIGHTS WEIGHTS.map((w, i) { if (ICONS[i] lastWinIcon) { return w * 0.3; } return w; });这个方案不能完全避免重复但能明显降低连续出现的概率实测体验好了很多。需要注意的是这个动态权重只影响是否抽到上次的图标不影响总中奖率所以对全局概率的干扰在可接受范围内。4.3 移动端兼容触摸、缩放和性能这个项目做完后我第一时间在手机上打开测试结果发现几个问题点击按钮时会有300ms的延迟虽然现在的手机浏览器大多已经消除了这个问题但保险起见还是要注意页面可以被双指缩放导致布局错乱滚动动画在低端安卓机上有点掉帧。针对这几个问题点击延迟用touchstart事件替代click事件是一种做法但touchstart有个问题是在桌面端不会触发。比较通用的做法是保持click事件然后加上以下meta标签meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno禁止缩放上述meta的user-scalableno就能解决。这里注意不要过度禁用否则对无障碍访问不友好但游戏页面禁掉缩放是常见做法。性能问题低端安卓机上动画掉帧主要是图标的box-shadow和border-radius在动画过程中触发了重绘。解决办法是在动画期间去掉不必要的视觉效果只保留纯色背景和字体动画结束后再恢复。另外will-change: transform也帮了不少忙但要记得在动画完成后不要马上移除否则会闪一下。音频播放移动端的自动播放限制很严格用户没有点击页面之前audio.play()会被拒绝。我的做法是在首次点击旋转按钮时再创建音频实例并调用play()这样能绕过限制。因为旋转这个动作本身就是用户交互浏览器是允许的。4.4 编码规范与跨浏览器一致性我测试了Chrome、Edge、Firefox和Safari总体来说主流浏览器对这个项目的支持都很好但有几个细节不同Safari对CSS的cubic-bezier缓动函数支持正常但偶尔会有细微的动画结束位置偏差目前看是一个已知bug。解决办法是把目标的偏移量设置成整数倍比如80的倍数同时确保列表里的图标高度完全没有小数位包括边框的box-sizing要统一。Firefox的DOMMatrixReadOnly构造器兼容性很好可以直接用。旧版本可能需要传入字符串参数。如果项目的兼容性要求更高需要考虑不支持DOMMatrix的情况这时可以用一个简单的正则来解析transform字符串或者干脆不依赖计算样式而是在spin时就把目标图标存到一个数组里结算时直接读数组。这其实是更稳妥的方案我当时为了展示技术才用了计算样式的做法实际工程里直接维护状态更省心。5. 扩展优化与项目升级方向5.1 增强视觉反馈音效与光效基础版本做好之后我顺手加了一些锦上添花的功能。音效用HTML5的Web Audio API来生成不需要外部音频文件代码量也不大。中奖时的叮叮叮声音其实就三个频率的音符排列用OscillatorNode就能搞定。赢大奖时在窗口四角闪烁一圈CSS动画模拟真实老虎机的灯光效果这个只用CSS的keyframes就能实现配合animation-delay错开闪烁时间。具体实现可以在settle函数里增加一个参数控制是否中大奖然后触发不同的光效和音效。我实测下来视觉和听觉反馈对玩家的情绪带动非常明显加不加效果玩起来的感受完全两个档次。5.2 引入历史记录与统计面板如果这个老虎机是给活动用的那统计功能就很重要了。我在后续版本里加了一个简单的统计面板记录总旋转次数、总中奖次数、当前胜率以及连输/连胜记录。这些数据都存在localStorage里刷新页面也能保留这样主办方可以随时看到玩家的博弈情况也方便调试概率模型。实现思路不复杂每次settle之后更新一个统计对象然后写入localStorage。面板用canvas画一个简单柱状图展示最近20轮的输赢情况这样玩家能看到自己的状态走势增加参与感。5.3 多人联机与后端接入的思考如果后续要做成多人在线的活动游戏那前端部分改动不大主要是在结算时把结果提交到后端。由于这个项目定位是纯前端我没有接后端但预留了接口。假如你有需要可以在settle函数里增加一个fetch请求把玩家的ID、投注金额、中奖结果、时间戳等数据发给服务端这样服务端就能实时汇总所有玩家的数据做排行榜甚至风控。需要注意的是纯前端的随机算法在正式场景中是不能用于涉及真实资金的游戏的因为浏览器端所有代码都对玩家可见随机结果可以被恶意篡改。如果项目涉及真金白银随机数生成必须放到服务端前端只负责展示结果。这一点一定要谨记。根据我个人经验做这种小游戏项目最花时间的往往不是功能本身而是各种边界情况的处理——比如玩家快速连点、动画事件丢失、不同浏览器表现不一致。把这些都处理好项目才算真正完成。希望这篇内容能给你一些有用的参考。本文还有配套的精品资源点击获取
返回列表