关于实战:实战在线浏览pdf文档

需要:在线浏览pdf文档. 解题:拿到pdf文档后,应用pdf.js库,将其转换成canvas,不就可以看了吗. 思路: 读取指定的pdf文档,获取文档的总页数;依据总页数,创立对应数量的canvas元素,并以id标识;将每页内容渲染到对应id的canvas中;代码如下: <!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>在线浏览pdf文档</title> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.13.216/pdf.min.js"></script></head><body> <div id="myContainer"></div> <script> function loadPDF (url) { // 思路: // 读取指定的pdf文档,获取文档的总页数 // 依据总页数,创立对应数量的canvas元素,并以id标识 // 将每页内容渲染到对应id的canvas中 pdfjsLib .getDocument('./a.pdf') .promise .then(function(pdf) { let num = pdf.numPages const id_prefix = 'ok_' createCanvas(num, id_prefix) renderCanvas(pdf, num, id_prefix) }) } // 创立画布容器 function createCanvas (num, idPrefix) { const container = document.getElementById('myContainer') for (let i = 1; i <= num; i++) { let canvas = document.createElement('canvas') canvas.id = `${idPrefix}${i}` container.appendChild(canvas) } } // 将文档内容渲染到画布 function renderCanvas (pdf, num, idPrefix) { for (let i = 1; i <= num; i++) { pdf .getPage(i) .then(function(page) { var scale = 2 var viewport = page.getViewport({ scale: scale, }) var canvas = document.getElementById(`${idPrefix}${i}`) var context = canvas.getContext('2d') canvas.height = viewport.height canvas.width = viewport.width var renderContext = { canvasContext: context, viewport: viewport } page.render(renderContext) }) } } loadPDF('./a.pdf') </script></body></html>

July 5, 2022 · 1 min · jiezi

Docker入门基础之应用实战

Docker入门基础之应用实战当我们掌握了Docker镜像和容器的基本用法后,我们现在能做些什么事情呢?现在我们就来看看使用Docker容器如何安装常见的软件,然后运行一个动态网站。 下面我们来学习: 1、安装Nginx2、安装PHP3、使用MySQL服务4、运行wordpress博客安装Nginx运行一个Alpine的容器,选择Alpine作为系统基础镜像是因为Alpine轻巧的体积,基础镜像只有5.53MB,相比ubuntu镜像的88.9MB要小十几倍。 root@ubuntu:~# docker run -it -p 8080:80 alpine sh安装nginx apk add nginx修改nginx配置 vi /etc/nginx/conf.d/default.confdefault.conf内容如下: server { listen 80 default_server; root /home/www; index index.php index.html; }创建Hello World mkdir /home/www && echo "Hello World" > /home/www/index.html创建/run/nginx目录 mkdir /run/nginx启动nginx nginx在浏览器中访问 http://192.168.43.122:8080 nginx安装成功,WEB服务访问正常! 安装PHP现在我们来安装PHP,方法还是一样,使用 apk add 命令来安装php7,php-fpm以及相关扩展。 apk add --no-cache php7 php7-fpm php7-ftp php7-pdo php7-mysqli php7-simplexml php7-xmlwriter php7-zlib php7-imagick php7-memcached php7-sockets php7-mcrypt php7-zip php7-pgsql php7-pdo_odbc php7-odbc php7-curl php7-iconv php7-xml php7-json php7-gd php7-session php7-opcache php7-pdo_sqlite php7-mbstring php7-common php7-pdo_mysql以上顺带安装了很多php扩展,可根据实际需求增减。 ...

July 16, 2019 · 1 min · jiezi

ShareBook1后台框架与小程序用户登录接口实战

本博客 猫叔的博客,转载请申明出处阅读本文约 “5分钟”适读人群:Java后端、Java初级、小程序前端前后端项目的地址ShareBookServerShareBookClient小程序前端 先看一下本节的学习目录,我们项目的小程序会在首次登录的时候自动获取用户的OpenId,并作为系统的注册信息,这里只是获取其OpenId或者SessionId,还没有获取用户信息(比如头像、Id、性别等)。 而在获取前,大家可能还要了解一下小程序的用户注册流程,这里我就不具体说了。 我们会在app.js(这个是所有小程序首次都会执行的js,我们会对小程序的缓存区Storage进行校验,并确定是否存在有效token),做校验,如果没有就进行首次注册。 注册流程是从小程序(简称vx,以下vx替代)获取用户的code,给到服务器,服务器会用code还有自己的appId等信息一起去微信服务器请求用户数据,注意每一个vx所对应的用户openid都是不一样的。 config.js是存放整个vx的统一基层API地址。 就如上图写的,前端在第一节的内容较为简单,大家可以在GitHub看到源码。 针对适读人群的合理学习时间是:45分钟,并了解vx官网部分简易API。 Java服务端 本节的服务端是重点,除了搭建整个SpringBoot基本开发框架以外(好像不用很久)还要准备一些基本的公关类和工具类,这个大家学习起来可能有点费劲,不过干货很多。 首先是技术栈,SpringBoot+MySQL+MyBatis,这个我就不具体说怎么搭建了,详情看源码,或者我的官网也有搭建教程。 在pom文件中,我引入了Swagger,这会方便我们与前端对接API的信息,你仅需要在启动类加上一个@EnableSwagger2的注解即可。 访问:http://localhost:8080/sharebook/swagger-ui.html(注意我的application-dev.yml中给项目起了名字叫sharebook,如果你是其他名字,请修改) 需要介绍的是,大家可以看看resources文件夹我采用application.yml、application-dev.yml,这样可以方便我们快速切换开发、生产、测试等多种环境的项目配置,希望大家可以养成习惯。 在项目的实体类上,我采用Lombok快速生成get/set方法,你仅需要加一个@Data的注解,这里你需要注意还要加无参和全参的构造函数,例如我一开始没有全参的构造,在读取生成User实体类的时候,MyBatis会报java.lang.NoSuchMethodException。 同时实体类需要序列号,我这里就采用默认的Serializable,对实体类序列化是因为它可能需要进行网络通信或者数据持久化。对于加了Serializable的实体类,最好有一个对应的UID。 因为我是用IDEA(推荐使用),所有如果要生成UID,可以在配置勾选以上的选项,然后点击实体类按“Alt+Enter”,然后选择生成UID即可。 common & util对于AppMessage,大家可能会吐槽,因为其实可以优化,不过我从ssm迁移过来就偷懒了,大家可以fork后自己改为SpringBoot的yml配置形式,它其实就是一些静态配置。 HttpService写的不好,大家可以修改优化,是一个普通的Http请求工具类。 主要是ResponseCode和ServerCache,对API接口返回层做了统一处理,vx前端程序员可以更好的调试工作,推荐大家模仿优化。 TokenCache使用了Google的guava做了本地缓存,缓存vx登录的token,一定要设定有效时间。 其实util包和common包一开始拆分的不好,所以大家可以优化。 MD5Util就是一个MD5的加解密处理。 业务处理就如上面vx环节说的,我们API获取到code后会进行校验处理。 我在接口实现使用了很古老的方式,代码是很久以前的了,见谅,介绍流程为主。大家可以去修改优化。 我对从微信服务器获取到的结果进行校验和数据获取,得到的openid先到数据库校验,用户是否存在,存在就生成Token,不存在就注册后生成Token。流程很简单。 补充以下,vx的API路径我推荐:http://localhost:8080/sharebook/api/v1/ 这里采用v1命名。是因为后续升级后v2,这样有时可以保证老版本API可以继续使用或者停用。 针对适读人群的合理学习时间是:115分钟,推荐自己模仿敲一遍。 实战调试vx首次登录调用成功。 vx缓存区Storage存储token数据 后端服务器日志打印正常。 SQL数据录入正常,这里sessionId为null是正常的,项目业务没有要求存储sessionId,注意对于在统一公众号下的不同小程序的openid是不同的,但是sessionId是一致的(不知道近期vx官方是否更改规则)。 目录链接没有实战经验?从零敲一个企业级共享项目前后端!公众号:Java猫说学习交流群:728698035 现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

May 15, 2019 · 1 min · jiezi

Flex常见布局实例

如果对flex不是很熟悉的同学,可以看一下我的另一篇文章Flex 布局1、网格布局1.1、基本网格布局最简单的网格布局,就是平均分布。HTML代码如下。<div class=“Grid”> <div class=“Grid-cell”>1/2</div> <div class=“Grid-cell”>1/2</div> </div> <div class=“Grid”> <div class=“Grid-cell”>1/3</div> <div class=“Grid-cell”>1/3</div> <div class=“Grid-cell”>1/3</div> </div>CSS代码如下。.Grid { display: flex;}.Grid-cell { flex: 1; background: #eee; margin: 10px;}这里最关键的就是flex:1使得各个子元素可以等比伸缩1.2、百分比布局某个网格的宽度为固定的百分比,其余网格平均分配剩余的空间。HTML代码如下。<div class=“Grid”> <div class=“Grid-cell col2”>50%</div> <div class=“Grid-cell”>auto</div> <div class=“Grid-cell “>auto</div></div><div class=“Grid”> <div class=“Grid-cell”>auto</div> <div class=“Grid-cell col2”>50%</div> <div class=“Grid-cell clo3”>1/3</div></div>CSS代码如下。.col2 { flex: 0 0 50%;}.col3 { flex: 0 0 33.3%;}这里最关键的是通过flex的第三个属性,也就是flex-basis来定义元素占据的空间。2、圣杯布局圣杯布局(Holy Grail Layout:)指的是一种最常见的网站布局。页面从上到下,分成三个部分:头部(header),躯干(body),尾部(footer)。其中躯干又水平分成三栏,从左到右为:导航、主栏、副栏。HTML代码如下。<div class=“container”> <header class=“bg”>header</header> <div class=“body”> <main class=“content bg”>content</main> <nav class=“nav bg”>nav</nav> <aside class=“ads bg”>aside</aside> </div> <footer class=“bg”>footer</footer> </div>CSS代码如下。.container { display: flex; flex-direction: column; min-height: 100vh;}.body { display: flex; flex: 1;}header,footer { flex: 0 0 100px;}.content { flex: 1;}.ads,.nav { flex: 0 0 100px;}.nav { order: -1;}.bg { background: #eee; margin: 10px;}@media (max-width: 768px) { .body { flex-direction: column; flex: 1; } .nav, .ads, .content { flex: auto; }}这里面需要注意的点有container的flex-direction: column这样保证了header,body,footer是在垂直轴上排列header,footer的高度可以通过flex: 0 0 100px来控制nav可以通过order:-1来调整位置通过@media (max-width: 768px)来调整小屏幕的页面结构3、侧边固定宽度侧边固定宽度,右边自适应html代码如下。<div class=“container1”> <div class=“aside1 bg”>aside</div> <div class=“body1”> <div class=“header1 bg”>header</div> <div class=“content1 bg”>content</div> <div class=“footer1 bg”>footer</div> </div></div>CSS代码如下。.bg { background: #eee; margin: 10px;}.container1 { min-height: 100vh; display: flex;}.aside1 { /* flex: 0 0 200px; */ flex: 0 0 20%;}.body1 { display: flex; flex-direction: column; flex: 1;}.content1 { flex: 1;}.header1, .footer1 { flex: 0 0 10%;}这个和上面的基本差不多就不做解释了。4、流式布局每行的项目数固定,会自动分行。html代码如下<div class=“container2”> <div class=“item”></div> <div class=“item”></div> <div class=“item”></div> <div class=“item”></div> <div class=“item”></div> <div class=“item”></div> <div class=“item”></div> <div class=“item”></div> <div class=“item”></div></div>css代码如下.container2 { width: 200px; height: 150px; display: flex; flex-flow: row wrap; align-content: flex-start;}.item { box-sizing: border-box; background: #eee; flex: 0 0 20%; height: 40px; margin: 5px;}这里主要使用到了flex-flow: row wrap使得子元素水平排列,并且换行总结这是我简单总结的一些布局。如有错误,欢迎指正。更多系列文章 ...

March 26, 2019 · 2 min · jiezi

Cocos Creator 教程(1)——第一个游戏:一步两步

第一个游戏这节我们从头做一个比较有意思的小游戏——一步两步。下面是最终效果(跳一步得一分,跳两步得三分,电脑端左右键12步):一步两步写在前面这是本教程第一个游戏,所以我会讲的详细一点,但是也不能避免遗漏,所以有什么问题你可以先尝试查阅文档自己解决或者在下方留言,后面我会跟进完善。另外这个游戏并非原创,参照的是腾讯的微信小游戏《一步两步H5》,请不要直接搬过去,微信里重复的游戏太多了。创建工程选择空白项目创建工程你可以从这里下载游戏素材,然后将素材导入工程(直接拖进编辑器,或者放在工程目录)也可以从https://github.com/potato47/one-two-step下载完整代码进行参照。准备工作做完后,我会把这个游戏制作过程分为若干个小过程,让你体会一下实际的游戏制作体验。从一个场景跳转到另一个场景在res文件夹下新建一个scenes文件夹,然后在scenes里新建两个场景menu和game(右键->新建->Scene)。然后双击menu进入menu场景。在层级管理器中选中Canvas节点,在右侧属性检查器中将其设计分辨率调整为1280x720,然后将background图片拖入Canvas节点下,并为其添加Widget组件(添加组件->UI组件->Widget),使其充满画布。在Canvas下新建一个Label节点(右键->创建节点->创建渲染节点->Label),然后调整文字大小,并添加标题文字我们知道节点和组件通常是一起出现的,带有常见组件的节点在编辑器里可以直接创建,比如刚才的带有Label组件的节点和带有Sprite组件的节点,但是我们也可以新建一个空节点然后为其添加对应的组件来组装一个带有特殊功能的节点。新建节点->UI节点下有一个Button,如果你直接创建Button,你会发现它是一个带有Button组件的节点,并且有一个Label的子节点。现在我们用另一种方法创建Button:在Canvas右键新建一个空节点,然后为其添加Button组件,这时你会发现按钮并没有背景,所以我们再添加一个Sprite组件,拖入资源中的按钮背景图片,最后添加一个Label子节点给按钮添加上文字。下面我们给这个按钮添加点击事件。在资源管理器中新建src文件夹用来存放脚本,然后新建一个TypeScript脚本,名字为Menu(注意脚本组件名称区分大小写,这里建议首字母大写)。双击用VS Code打开脚本,更改如下:const { ccclass } = cc._decorator;@ccclass // 让编辑器能够识别这是一个组件export class Menu extends cc.Component { private onBtnStart() { cc.director.loadScene(‘game’); //加载game场景 }}一个类只有加上@ccclass才能被编辑器识别为脚本组件,如果你去掉@ccclass,你就不能把这个组件拖到节点上。另外可以看到代码中出现了几次cc这个东西,cc其实是Cocos的简称,在游戏中是引擎的主要命名空间,引擎代码中所有的类、函数、属性和常量都在这个命名空间中定义。很明显,我们想在点击开始按钮的时候调用onBtnStart函数,然后跳转到game场景。为了测试效果我们先打开game场景,然后放一个测试文字(将Canvas的设计分辨率也改为1280x720)。保存game场景后再回到Menu场景。Button组件点击后会发出一个事件,这个事件可以跟某个节点上的某个脚本内的某个函数绑定在一起。听着有点绕,动手做一遍就会明白这个机制。首先将Menu脚本添加为Canvas节点的组件,然后在开始按钮的Button组件里添加一个Click Event,将其指向Canvas节点下的Menu脚本里的onBtnStart函数。我们再调整一下Button的点击效果,将Button组件的Transition改为scale(伸缩效果),另外还有颜色变化和图片变化,可以自己尝试。最后点击上方的预览按钮,不出意外的话就可以在浏览器中看见预期效果。组织代码结构现在我们来编写游戏逻辑。首先我来讲一下我看到的一种现象:很多新手非常喜欢问,“看代码我都能看懂啊,但是要我自己写我就没思路啊”这时一位经验颇多的长者就会甩给他一句,“多写写就有思路了“不知道你们发现没有,这竟然是一个死循环。对于一个刚开始学习做游戏的人,首先要了解的是如何组织你的代码,这里我教给大家一个最容易入门的代码结构——单向分权结构(这是我想了足足两分钟的自认为很酷炫的一个名字)脚本分层:这个结构最重要的就是“权”这个字,我们把一个场景中使用的脚本按照“权力”大小给它们分层,权力最大的在最上层且只有一个,这个脚本里保存着它直接控制的若干个脚本的引用,被引用的脚本权力就小一级,被引用的脚本还会引用比它权力更小的脚本,依此类推。脚本互操作:上一层的脚本由于保存着下一层脚本的引用,所以可以直接操作下一层的脚本。下一层的脚本由上一层的脚本初始化,在初始化的时候会传入上一层的引用(可选),这样在需要的时候会反馈给上一层,由上一层执行更具体的操作。同层的脚本尽量不要互相操作,统一交给上层处理,同层解耦。不可避免的同层或跨层脚本操作可以使用全局事件来完成。具有通用功能的脚本抽离出来,任意层的脚本都可以直接使用。写了这么多,但你肯定没看懂,现在你可以翻到最上面再分析一下游戏的game场景,如何组织这个场景的脚本结构?首先,一个场景的根节点会挂载一个脚本,通常以场景名命名,这里就是Game。然后跳跃的人物也对应着一个脚本Player。跟Player同层的还应该有Block也就是人物踩着的地面方块。因为Player和Block之间互相影响并且我想让Game脚本更简洁,所以这里再加一个Stage(舞台)脚本来控制Player和Block。最终它们的层级关系如下:GameStagePlayerBlock上面这些都是我们的思考过程,下面我们落实到场景中。先新建几个脚本现在搭建场景,先添加一个跟menu场景一样的全屏背景然后添加一个空节点Stage,在Stage下添加一个Player节点和一个Block节点在Stage同层添加两个按钮来控制跳一步两步先添加第一个按钮,根据实际效果调整文字大小(font size)颜色(node color)和按钮的缩放倍数(scale)第二个按钮可以直接由第一个按钮复制这两个按钮显然是要放置在屏幕左下角和右下角的,但是不同屏幕大小可能导致这两个按钮的位置跑偏,所以最好的方案是给这两个按钮节点添加Widget组件,让它们跟左下角和右下角保持固定的距离,这里就不演示了,相信你可以自己完成(其实是我忘录了。。。)添加一个Label节点记录分数,系统字体有点丑,这里替换成我们自己的字体最后把脚本挂在对应的节点上。场景搭建到这里基本完成了,现在可以编写脚本了。Game作为一个统领全局的脚本,一定要控制关键的逻辑,,比如开始游戏和结束游戏,增加分数,还有一些全局的事件。Game.tsimport { Stage } from ‘./Stage’;const { ccclass, property } = cc._decorator;@ccclassexport class Game extends cc.Component { @property(Stage) private stage: Stage = null; @property(cc.Label) private scoreLabel: cc.Label = null; private score: number = 0; protected start() { this.startGame(); } public addScore(n: number) { this.score += n; this.scoreLabel.string = this.score + ‘’; } public startGame() { this.score = 0; this.scoreLabel.string = ‘0’; this.stage.init(this); } public overGame() { cc.log(‘game over’); } public restartGame() { cc.director.loadScene(‘game’); } public returnMenu() { cc.director.loadScene(‘menu’); } private onBtnOne() { this.stage.playerJump(1); } private onBtnTwo() { this.stage.playerJump(2); }}Stage作为Game直接控制的脚本,要给Game暴露出操作的接口并且保存Game的引用,当游戏状态发生改变时,通知Game处理。Stage.tsimport { Game } from ‘./Game’;import { Player } from ‘./Player’;const { ccclass, property } = cc._decorator;@ccclassexport class Stage extends cc.Component { @property(Player) private player: Player = null; private game: Game = null; public init(game: Game) { this.game = game; } public playerJump(step: number) { this.player.jump(step); }}而Player作为最底层的一个小员工,别人让你做啥你就做啥。Player.tsconst {ccclass, property} = cc._decorator;@ccclassexport class Player extends cc.Component { public jump(step: number) { if (step === 1) { cc.log(‘我跳了1步’); } else if (step === 2) { cc.log(‘我跳了2步’); } } public die() { cc.log(‘我死了’); }}之前讲了@ccclass是为了让编辑器识别这是一个组件类,可以挂在节点上,现在我们又看到了一个@property,这个是为了让一个组件的属性暴露在编辑器属性中,观察最上面的Game脚本,发现有三个成员变量,stage,scoreLabel和score,而只有前两个变量加上了@property,所以编辑器中只能看到stage和scoreLabel。@property括号里通常要填一个编辑器可以识别的类型,比如系统自带的cc.Label,cc.Node,cc.Sprite,cc.Integer,cc.Float等,也可以是用户脚本类名,比如上面的Stage和Player。回到编辑器,我们把几个脚本暴露在编辑器的变量通过拖拽的方式指向带有类型组件的节点。再把one,two两个按钮分别绑定在game里的onBtnOne,onBtnTwo两个函数上。这时我们已经有了一个简单的逻辑,点击1或2按钮,调用Game里的onBtnOne或onBtnTwo,传递给Stage调用playerJump,再传递给Player调用jump,player就会表现出跳一步还是跳两步的反应。点击预览按钮,进行测试:你可以按F12(windows)或cmd+opt+i(mac)打开chrome的开发者工具。人物跳跃动作现在我们来让Player跳起来,人物动作的实现大概可以借助以下几种方式实现:动画系统动作系统物理系统实时计算可以看到这个游戏人物动作比较简单,跳跃路径是固定的,所以我们选择用动作系统实现人物的跳跃动作。creator自带一套基于节点的动作系统,形式如node.runAction(action)。修改Player.ts,添加几个描述跳跃动作的参数,并且添加一个init函数由上层组件即Stage初始化时调用并传入所需参数。另外更改jump函数内容让Player执行jumpBy动作。Player.ts…private stepDistance: number; // 一步跳跃距离private jumpHeight: number; // 跳跃高度private jumpDuration: number; // 跳跃持续时间public canJump: boolean; // 此时是否能跳跃public init(stepDistance: number, jumpHeight: number, jumpDuration: number) { this.stepDistance = stepDistance; this.jumpHeight = jumpHeight; this.jumpDuration = jumpDuration; this.canJump = true;}public jump(step: number) { this.canJump = false; this.index += step; let jumpAction = cc.jumpBy(this.jumpDuration, cc.v2(step * this.stepDistance, 0), this.jumpHeight, 1); let finishAction = cc.callFunc(() => { this.canJump = true; }); this.node.runAction(cc.sequence(jumpAction, finishAction));}…Stage.ts…@property(cc.Integer)private stepDistance: number = 200;@property(cc.Integer)private jumpHeight: number = 100;@property(cc.Float)private jumpDuration: number = 0.3;@property(Player)private player: Player = null;private game: Game = null;public init(game: Game) { this.game = game; this.player.init(this.stepDistance, this.jumpHeight, this.jumpDuration);}public playerJump(step: number) { if (this.player.canJump) { this.player.jump(step); }}…这里要介绍一下 Cocos Creator 的动作系统,动作系统基于节点,你可以让一个节点执行一个瞬时动作或持续性的动作。比如让一个节点执行一个“3秒钟向右移动100”的动作,就可以这样写let moveAction = cc.moveBy(3, cc.v2(100, 0)); // cc.v2可以创建一个二位的点(向量),代表方向x=100,y=0this.node.runAction(moveAction);更多的动作使用可查询文档 http://docs.cocos.com/creator…回头看Player的jump方法,这里我们的意图是让Player执行一个跳跃动作,当跳跃动作完成时将this.canJump改为true,cc.CallFunc也是一个动作,这个动作可以执行你传入的一个函数。所以上面的finishAction执行的时候就可以将this.canJump改为true,cc.sequence用于将几个动作连接依次执行。可以看到jumpAction传入了很多参数,有些参数可以直接根据名字猜到,有一些可能不知道代表什么意思,这时你就要善于搜索api,另外要充分利用ts提示的功能,你可以直接按住ctrl/cmd+鼠标单击进入定义文件查看说明示例。再来看Stage,可以看到Player初始化的几个参数是由Stage传递的,并且暴露在了编辑器界面,我们可以直接在Stage的属性面板调整参数,来直观的编辑动作效果,这也是Creator编辑器的方便之处。上面的几个参数是我调整过后的,你也可以适当的修改,保存场景后预览效果。动态添加地面和移动场景显而易见,我们不可能提前设置好所有的地面(Block),而是要根据Player跳跃的时机和地点动态添加Block,这就涉及到一个新的知识点——如何用代码创建节点?每一个Block节点都是一样的,对于这样相同的节点可以抽象出一个模板,Creator里管这个模板叫做预制体(Prefab),想要一个新的节点时就可以通过复制Prefab得到。制作一个Prefab很简单,我们先在res目录下新建一个prefabs目录,然后将Block节点直接拖到目录里就可以形成一个Prefab了。你可以双击这个prefab进入其编辑模式,如果之前忘了将Block脚本挂在Block节点上,这里也可以挂在Block的Prefab上。有了Prefab后,我们就可以利用函数cc.instance来创建出一个节点。根据之前讲的组织代码原则,创建Block的职责应该交给他的上级,也就是Stage。之前编写Player代码时设置了一个index变量,用来记录Player跳到了“第几格”,根据游戏逻辑每当Player跳跃动作完成后就要有新的Block出现在前面。修改Stage如下:Stage.tsimport { Game } from ‘./Game’;import { Player } from ‘./Player’;import { Block } from ‘./Block’;const { ccclass, property } = cc._decorator;@ccclassexport class Stage extends cc.Component { @property(cc.Integer) private stepDistance: number = 200; @property(cc.Integer) private jumpHeight: number = 100; @property(cc.Float) private jumpDuration: number = 0.3; @property(Player) private player: Player = null; @property(cc.Prefab) private blockPrefab: cc.Prefab = null; // 编辑器属性引用 private lastBlock = true; // 记录上一次是否添加了Block private lastBlockX = 0; // 记录上一次添加Block的x坐标 private blockList: Array<Block>; // 记录添加的Block列表 private game: Game = null; public init(game: Game) { this.game = game; this.player.init(this.stepDistance, this.jumpHeight, this.jumpDuration); this.blockList = []; this.addBlock(cc.v2(0, 0)); for (let i = 0; i < 5; i++) { this.randomAddBlock(); } } public playerJump(step: number) { if (this.player.canJump) { this.player.jump(step); this.moveStage(step); let isDead = !this.hasBlock(this.player.index); if (isDead) { cc.log(‘die’); this.game.overGame(); } else { this.game.addScore(step === 1 ? 1 : 3); // 跳一步得一分,跳两步的三分 } } } private moveStage(step: number) { let moveAction = cc.moveBy(this.jumpDuration, cc.v2(-this.stepDistance * step, 0)); this.node.runAction(moveAction); for (let i = 0; i < step; i++) { this.randomAddBlock(); } } private randomAddBlock() { if (!this.lastBlock || Math.random() > 0.5) { this.addBlock(cc.v2(this.lastBlockX + this.stepDistance, 0)); } else { this.addBlank(); } this.lastBlockX = this.lastBlockX + this.stepDistance; } private addBlock(position: cc.Vec2) { let blockNode = cc.instantiate(this.blockPrefab); this.node.addChild(blockNode); blockNode.position = position; this.blockList.push(blockNode.getComponent(Block)); this.lastBlock = true; } private addBlank() { this.blockList.push(null); this.lastBlock = false; } private hasBlock(index: number): boolean { return this.blockList[index] !== null; }}首先我们在最上面添加了几个成员变量又来记录Block的相关信息。然后修改了playerJump方法,让player跳跃的同时执行moveStage,moveStage方法里调用了一个moveBy动作,这个动作就是把节点相对移动一段距离,这里要注意的是moveStage动作和player里的jump动作水平移动的距离绝对值和时间都是相等的,player向前跳,stage向后移动,这样两个相反的动作,就会让player始终处于屏幕中的固定位置而不会跳到屏幕外了。再看moveStage方法里会调用randomAddBlock,也就是随机添加block,随机算法要根据游戏规则推理一下:这个游戏的操作分支只有两个:1步或者是2步。所以每2个Block的间隔只能是0步或者1步。因此randomAddBlock里会判断最后一个Block是否为空,如果为空那新添加的一定不能为空。如果不为空则50%的概率随机添加或不添加Block。这样就能得到无限随机的地图了。为了激励玩家多按2步,所以设定跳1步的1分,跳2步得3分。另外Player跳几步randomAddBlock就要调用几次,这样才能保证地图与Player跳跃距离相匹配。再说一下addBlock方法,blockNode是由blockPrefab复制出来的,你必须通过addChild方法把它添加场景中的某个节点下才能让它显示出来,这里的this.node就是Stage节点。为了方便我们把lastBlockX初始值设为0,也就是水平第一个block的横坐标应该等于0,所以我们要回到编辑器调整一下stage,player,block三个节点的位置,让block和player的x都等于0,并且把Block的宽度设为180(一步的距离设为200,为了让两个相邻的Block有一点间距,要适当窄一些),最后不要忘记把BlockPrefab拖入对应的属性上。playerJump的的最后有一段判断游戏结束的逻辑,之前我们在player里设置了一个变量index,记录player当前跳到第几格,stage里也有一个数组变量blockList保存着所有格子的信息,当player跳完后判断一下落地点是否有格子就可以判断游戏是否结束。捋顺上面的逻辑后,你就可以预览一下这个看起来有点样子的游戏了地面下沉效果如果每一步都让玩家想很久,那这个游戏就没有尽头了。现在我们给它加点难度。设置的效果是:地面每隔一段时间就会下落,如果玩家没有及时跳到下一个格子就会跟着地面掉下去,为了实现这个人物和地面同时下坠的效果,我们要让Player和Block执行相同的动作,所以在Stage上新加两个变量fallDuration和fallHeight用来代表下落动作的时间和高度,然后传给Player和Block让它们执行。另外这种小游戏的难度一定是要随着时间增加而增大的,所以Block的下落时间要越来越快。下面我们来修改Block,Player,Stage三个脚本Block.tsconst { ccclass } = cc._decorator;@ccclassexport class Block extends cc.Component { public init(fallDuration: number, fallHeight: number, destroyTime: number, destroyCb: Function) { this.scheduleOnce(() => { let fallAction = cc.moveBy(fallDuration, cc.v2(0, -fallHeight)); // 下沉动作 this.node.runAction(fallAction); destroyCb(); }, destroyTime); }}这里补充了Block的init方法,传入了四个参数,分别是坠落动作的持续时间,坠落动作的高度,销毁时间,销毁的回调函数。scheduleOnce是一个一次性定时函数,存在于cc.Component里,所以你可以在脚本里直接通过this来调用这个函数,这里要实现的效果就是延迟destroyTime时间执行下落动作。Player.tsconst { ccclass } = cc._decorator;@ccclassexport class Player extends cc.Component { private stepDistance: number; // 一步跳跃距离 private jumpHeight: number; // 跳跃高度 private jumpDuration: number; // 跳跃持续时间 private fallDuration: number; // 坠落持续时间 private fallHeight: number; // 坠落高度 public canJump: boolean; // 此时是否能跳跃 public index: number; // 当前跳到第几格 public init(stepDistance: number, jumpHeight: number, jumpDuration: number, fallDuration: number, fallHeight: number) { this.stepDistance = stepDistance; this.jumpHeight = jumpHeight; this.jumpDuration = jumpDuration; this.fallDuration = fallDuration; this.fallHeight = fallHeight; this.canJump = true; this.index = 0; }… public die() { this.canJump = false; let dieAction = cc.moveBy(this.fallDuration, cc.v2(0, -this.fallHeight)); this.node.runAction(dieAction); }}首先将init里多传入两个变量fallDuration和fallHeight用来实现下落动作,然后补充die方法,这里的下落动作其实是个上面的Block里的下落动作是一样的。Stage.ts…@property(cc.Integer)private fallHeight: number = 500;@property(cc.Float)private fallDuration: number = 0.3;@property(cc.Float)private initStayDuration: number = 2; // 初始停留时间@property(cc.Float)private minStayDuration: number = 0.3; // 最小停留时间,不能再快了的那个点,不然玩家就反应不过来了@property(cc.Float)private speed: number = 0.1;private stayDuration: number; // 停留时间…public init(game: Game) { this.game = game; this.stayDuration = this.initStayDuration; this.player.init(this.stepDistance, this.jumpHeight, this.jumpDuration, this.fallDuration, this.fallHeight); this.blockList = []; this.addBlock(cc.v2(0, 0)); for (let i = 0; i < 5; i++) { this.randomAddBlock(); }}public addSpeed() { this.stayDuration -= this.speed; if (this.stayDuration <= this.minStayDuration) { this.stayDuration = this.minStayDuration; } cc.log(this.stayDuration);}public playerJump(step: number) { if (this.player.canJump) { this.player.jump(step); this.moveStage(step); let isDead = !this.hasBlock(this.player.index); if (isDead) { cc.log(‘die’); this.scheduleOnce(() => { // 这时还在空中,要等到落到地面在执行死亡动画 this.player.die(); this.game.overGame(); }, this.jumpDuration); } else { let blockIndex = this.player.index; this.blockList[blockIndex].init(this.fallDuration, this.fallHeight, this.stayDuration, () => { if (this.player.index === blockIndex) { // 如果Block下落时玩家还在上面游戏结束 this.player.die(); this.game.overGame(); } }); this.game.addScore(step === 1 ? 1 : 3); } if (this.player.index % 10 === 0) { this.addSpeed(); } }}…Player和Block下落动作都需要的fallDuration和fallHeight我们提取到Stage里,然后又添加了几个属性来计算Block存留时间。在playerJump方法里,补充了Player跳跃后的逻辑:如果Player跳空了,那么就执行死亡动画也就是下落动作,如果Player跳到Block上,那么这个Block就启动下落计时器,当Block下落时Player还没有跳走,那就和Player一起掉下去。最后增加下落速度的方式是每隔十个格子加速一次。回到编辑器,调整fallDuration,fallHeight,initStayDuration,minStayDuration,speed的值。预览游戏添加结算面板前面讲了这么多,相信你能自己拼出下面这个界面。上面挂载的OverPanel脚本如下:OverPanel.tsimport { Game } from “./Game”;const { ccclass, property } = cc._decorator;@ccclassexport class OverPanel extends cc.Component { @property(cc.Label) private scoreLabel: cc.Label = null; private game: Game; public init(game: Game) { this.game = game; } private onBtnRestart() { this.game.restartGame(); } private onBtnReturnMenu() { this.game.returnMenu(); } public show(score: number) { this.node.active = true; this.scoreLabel.string = score + ‘’; } public hide() { this.node.active = false; }}不要忘了将两个按钮绑定到对应的方法上。最后修改Game,让游戏结束时显示OverPanelGame.tsimport { Stage } from ‘./Stage’;import { OverPanel } from ‘./OverPanel’;const { ccclass, property } = cc._decorator;@ccclassexport class Game extends cc.Component { @property(Stage) private stage: Stage = null; @property(cc.Label) private scoreLabel: cc.Label = null; @property(OverPanel) private overPanel: OverPanel = null; private score: number = 0; protected start() { this.overPanel.init(this); this.overPanel.hide(); this.startGame(); } public addScore(n: number) { this.score += n; this.scoreLabel.string = this.score + ‘’; } public startGame() { this.score = 0; this.scoreLabel.string = ‘0’; this.stage.init(this); } public overGame() { this.overPanel.show(this.score); } public restartGame() { cc.director.loadScene(‘game’); } public returnMenu() { cc.director.loadScene(‘menu’); } private onBtnOne() { this.stage.playerJump(1); } private onBtnTwo() { this.stage.playerJump(2); }}将OverPanel的属性拖上去。为了不影响编辑器界面,你可以将OverPanel节点隐藏预览效果添加声音和键盘操作方式如果你玩过这个游戏,肯定知道声音才是其灵魂。既然是Player发出的声音,就挂在Player身上吧Player.tsconst { ccclass, property } = cc._decorator;@ccclassexport class Player extends cc.Component { @property({ type: cc.AudioClip }) private oneStepAudio: cc.AudioClip = null; @property({ type:cc.AudioClip }) private twoStepAudio: cc.AudioClip = null; @property({ type:cc.AudioClip }) private dieAudio: cc.AudioClip = null; … public jump(step: number) { … if (step === 1) { cc.audioEngine.play(this.oneStepAudio, false, 1); } else if (step === 2) { cc.audioEngine.play(this.twoStepAudio, false, 1); } } public die() { … cc.audioEngine.play(this.dieAudio, false, 1); }}这里你可能比较奇怪的为什么这样写@property({ type: cc.AudioClip})private oneStepAudio: cc.AudioClip = null;而不是这样写@property(cc.AudioClip)private oneStepAudio: cc.AudioClip = null;其实上面的写法才是完整写法,除了type还有displayName等参数可选,当只需要type这个参数时可以写成下面那种简写形式,但例外的是有些类型只能写完整形式,不然就会抱警告,cc.AudioClip就是其一。在电脑上点击两个按钮很难操作,所以我们添加键盘的操作方式。Game.tsimport { Stage } from ‘./Stage’;import { OverPanel } from ‘./OverPanel’;const { ccclass, property } = cc._decorator;@ccclassexport class Game extends cc.Component { … protected start() { … this.addListeners(); } … private addListeners() { cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, (event: cc.Event.EventKeyboard) => { if (event.keyCode === cc.macro.KEY.left) { this.onBtnOne(); } else if (event.keyCode === cc.macro.KEY.right) { this.onBtnTwo(); } }, this); }}在游戏初始化的时候通过cc.systemEvent注册键盘事件,按左方向键跳一步,按右方向键跳两步。至此我们的游戏就做完了。一步两步如果你有基础,这个游戏并不难,如果这是你的第一篇教程,你可能会很吃力,无论前者后者,遇到任何问题都可以在下方留言,我也会随时更新。另外不要忘了加QQ交流群哦 863758586 ...

January 7, 2019 · 6 min · jiezi