Web学习(五)中期项目-简易拳皇
Web学习(五)中期项目-简易拳皇
项目参考地址:https://git.acwing.com/yxc/kof
游戏的基本原理:主要依靠requestAnimationFrame实现,该函数会在下次浏览器刷新页面之前执行一次,通常会用递归写法使其每秒执行60次func函数,每秒画60次物体,通过坐标改变物体位置,人眼看到的结果就是物体在移动。
1.创建表示游戏窗口的类
html:
<!DOCTYPE html>
<html lang="en"><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"><link rel="stylesheet" href="/static/css/base.css"><script src="https://cdn.acwing.com/static/jquery/js/jquery-3.3.1.min.js"></script><title>拳皇</title>
</head><body><div id="kof"></div><script type="module">import { KOF } from '/static/js/base.js'let kof = new KOF('kof');</script>
</body></html>
css:
#kof {width: 1280px;height: 720px;background-image: url('/static/images/background/0.gif');background-size: 200% 100%;background-position: top;position: absolute;
}
base.js:
KOF窗口大类包含两个小类:玩家与地图
// 创建一个类KOF表示当前的大窗口
class KOF {constructor(id) { //构造函数,需要传入idthis.$kof = $('#' + id);//jQuery选择器}
}export {KOF
}
2.创建ac_game_object类
game_map,player等对象都需要在每秒画60次以达到移动效果,所以我们创建一个共同的基类ac_game_object, game_map,player等对象继承ac_game_object基类即可。
let AC_GAME_OBJECTS = []class AcGameObject {constructor() {AC_GAME_OBJECTS.push(this); //存储对象this.timedelta = 0; //时间间隔,每个object对象都需要存储当前一帧与上一帧的时间间隔,每个物体的速度取决于时间间隔this.has_call_start = false; //表示当前对象是否调用过start函数}start() { //初始执行一次}updats() { //每一帧执行一次(第一帧除外)}destroy() { //删除对象for (let i in AC_GAME_OBJECTS) { //for in 是枚举下标,for of是枚举值if (AC_GAME_OBJECTS[i] === this) {AC_GAME_OBJECTS.splice(i, 1);break;}}}
}let last_timestamp; //上一帧执行时刻let AC_GAME_OBJECTS_FRAME = (timestamp) => { //timestamp它表示requestAnimationFrame() 开始去执行回调函数的时刻,时间戳for (let obj of AC_GAME_OBJECTS) {if (!obj.has_call_start) { //如果对象没有执行过start函数obj.start();obj.has_call_start = true;} else {obj.timedelta = timestamp - last_timestamp; //时间间隔,当前时刻-上一帧时刻obj.update();}}last_timestamp = timestamp; //更新上次调用函数的时刻requestAnimationFrame(AC_GAME_OBJECTS_FRAME); //递归调用
}requestAnimationFrame(AC_GAME_OBJECTS_FRAME);export {AcGameObject
}
3.定义game_map对象
import { AcGameObject } from '/static/js/ac_game_object/base.js';export class GameMap extends AcGameObject { //继承constructor(root) {super();this.root = root;this.$canvas = $('<canvas width="1280" height="720" tabindex=0></canvas>'); //jQuery用法,tabindex=0使得可以聚焦读取键盘输入this.ctx = this.$canvas[0].getContext('2d'); //取出canvas,这里参考canvas用法this.root.$kof.append(this.$canvas); //将canvas加入id为kof的div里this.$canvas.focus(); //使得convas可以聚焦}start() { //开始时执行}update() { //每一帧执行this.render();}render() { //渲染函数中需要将每一帧地图都清空,否则物体运动过程会一直停留在地图上this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);}
}
4.定义player对象
import { AcGameObject } from "/static/js/ac_game_object/base.js";export class Player extends AcGameObject {constructor(root, info) { //将初始参数放至构造函数中,这里root表示大类KOF,大类KOF包含两个小类玩家与地图super();this.root = root;this.id = info.id;this.x = info.x;this.y = info.y;this.width = info.width;this.height = info.height;this.color = info.color;this.direction = 1;this.vx = 0;this.vy = 0;this.speedx = 400; // 水平速度this.speedy = -1000; // 跳起的初始速度this.gravity = 50;this.ctx = this.root.game_map.ctx; //这里引入canvas便于下一步操作}start() {}update() {this.render();}render() {//测试代码this.ctx.fillstyle = this.color;this.ctx.fillRect(this.x, this.y, this.width, this.height);}}
在主js中创建地图与玩家对象
import { GameMap } from '/static/js/game_map/base.js';
import { Player } from '/static/js/player/base.js';// 创建一个类KOF表示当前的大窗口,此大类包含玩家对象,地图对象
class KOF {constructor(id) { //构造函数,需要传入idthis.$kof = $('#' + id);//jQuery选择器//创建地图this.game_map = new GameMap(this);//创建玩家this.players = [ //player对象需要传入root与infonew Player(this, {id: 0,x: 200,y: 0,width: 120,height: 200,color: 'blue',}),new Player(this, {id: 1,x: 900,y: 0,width: 120,height: 200,color: 'red',}),];}
}export {KOF
}
5.定义controller对象
controller对象用于读取键盘输入
export class Controller {constructor($canvas) {this.$canvas = $canvas;this.pressed_keys = new Set(); //set用于存储当前按住了哪个键this.start();}start() {let outer = this; //这里pressed_keys是Controller对象下的值,不是convas的,所以要使用outerthis.$canvas.keydown(function (e) { //按下outer.pressed_keys.add(e.key); //这里this指的是canvas,outer指的是controller});this.$canvas.keyup(function (e) { //放开outer.pressed_keys.delete(e.key);});}
}
6.在player对象中添加update_control函数
update_control() { //更新按下的键位let w, a, d, space;if (this.id === 0) { // 玩家1w = this.pressed_keys.has('w');a = this.pressed_keys.has('a');d = this.pressed_keys.has('d');space = this.pressed_keys.has(' ');} else { //玩家2w = this.pressed_keys.has('ArrowUp');a = this.pressed_keys.has('ArrowLeft');d = this.pressed_keys.has('ArrowRight');space = this.pressed_keys.has('Enter');}if (this.status === 0 || this.status === 1) { //静止状态或移动状态if (space) {this.status = 4;this.vx = 0;this.frame_current_cnt = 0;} else if (w) { //跳跃if (d) {this.vx = this.speedx;} else if (a) {this.vx = -this.speedx;} else {this.vx = 0;}this.vy = this.speedy;this.status = 3;this.frame_current_cnt = 0;} else if (d) { //向右this.vx = this.speedx;this.status = 1;} else if (a) { //向左this.vx = -this.speedx;this.status = 1;} else {this.vx = 0;this.status = 0;}}}
7.添加动画(人物图片)
参考:https://stackoverflow.com/questions/48234696/how-to-put-a-gif-with-canvas
新建utils文件夹中存放参考的gif.js
player中新建kyo.js:
import { Player } from '/static/js/player/base.js';
import { GIF } from '/static/js/utils/gif.js';export class Kyo extends Player {constructor(root, info) {super(root, info);this.init_animations();}init_animations() {let outer = this;let offsets = [0, -22, -22, -140, 0, 0, 0]; //偏移量,因为图片高度的原因,对应7种不同状态时回到同一水平线需要的偏移量for (let i = 0; i < 7; i++) { //对图片循环处理let gif = GIF();gif.load(`/static/images/player/kyo/${i}.gif`);this.animations.set(i, { //这里的animations是在player中定义过的Map用来存储键值对,set是插入键值对gif: gif,frame_cnt: 0, // 总图片数frame_rate: 5, // 每5帧过度一次offset_y: offsets[i], // y方向偏移量loaded: false, // 是否加载完整scale: 2, // 放大多少倍});gif.onload = function () {let obj = outer.animations.get(i); //Map用法get是查找关键字obj.frame_cnt = gif.frames.length;obj.loaded = true; //表示加载完整if (i === 3) { //如果是第3种图片即跳跃状态,更改帧率obj.frame_rate = 4;}}}}
}
在player中的base,js修改render渲染函数:
render() { //渲染函数// //测试代码,画出矩形// this.ctx.fillstyle = this.color;// this.ctx.fillRect(this.x, this.y, this.width, this.height);let status = this.status; //取出当前状态//如果和速度反方向,状态变为2(后退)if (this.status === 1 && this.direction * this.vx < 0) status = 2;let obj = this.animations.get(status); //将当前状态status传入animations中if (obj && obj.loaded) {if (this.direction > 0) { //如果是正向//frame_current_cnt表示当前记录了多少帧//frame_current_cnt % obj.frame_cnt 当前帧数 % 图片总帧数,因为图片需要循环播放//frame_rate控制图片播放速率let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;//取出第k张图片let image = obj.gif.frames[k].image;//画出图像, this.y + obj.offset_y:纵方向加上偏移量, image.width * obj.scale:图片大小乘以缩放倍数this.ctx.drawImage(image, this.x, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);} else {this.ctx.save();this.ctx.scale(-1, 1); // x坐标乘-1,y坐标不变,即水平反转this.ctx.translate(-this.root.game_map.$canvas.width(), 0);let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;let image = obj.gif.frames[k].image;//水平翻转之后,两图像初始位置将会一致,所以初始位置需要对称过去this.ctx.drawImage(image, this.root.game_map.$canvas.width() - this.x - this.width, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);this.ctx.restore(); //恢复坐标系}}//状态为攻击,if (status === 4 || status === 5 || status === 6) {if (this.frame_current_cnt == obj.frame_rate * (obj.frame_cnt - 1)) {if (status === 6) {this.frame_current_cnt--;} else {this.status = 0; // 回归静止状态}}}this.frame_current_cnt++; //下一帧}
8.让俩位玩家位置对称
update_direction() { //更新位置(面朝方向)if (this.status === 6) return;let players = this.root.players;if (players[0] && players[1]) {let me = this, you = players[1 - this.id];if (me.x < you.x) me.direction = 1; //如果我在左,则我面朝的方向为正方向else me.direction = -1;}}
9.攻击与被攻击实现
攻击函数:
update_attack() { //更新攻击函数if (this.status === 4 && this.frame_current_cnt === 18) { //处于攻击状态,且在18帧时(18帧时拳头挥出去)let me = this, you = this.root.players[1 - this.id];let r1;//r1出拳挥出的长方形,只要此长方形与人物模型长方形发生碰撞则认为击中了if (this.direction > 0) { //正方向r1 = {x1: me.x + 120,y1: me.y + 40,x2: me.x + 120 + 100,y2: me.y + 40 + 20,};} else { //反方向 r1 = {x1: me.x + me.width - 120 - 100,y1: me.y + 40,x2: me.x + me.width - 120 - 100 + 100,y2: me.y + 40 + 20,};}//r2为人物模型的长方形let r2 = {x1: you.x,y1: you.y,x2: you.x + you.width,y2: you.y + you.height};if (this.is_collision(r1, r2)) { //如果碰撞检测成立you.is_attack(); //调用被攻击函数}}}
碰撞检测函数:
is_collision(r1, r2) { //碰撞检测if (Math.max(r1.x1, r2.x1) > Math.min(r1.x2, r2.x2))return false;if (Math.max(r1.y1, r2.y1) > Math.min(r1.y2, r2.y2))return false;return true;}
被攻击函数:
is_attack() { //被攻击函数if (this.status === 6) return;this.status = 5;this.frame_current_cnt = 0;}
10.增加血条
更改css
#kof {width: 1280px;height: 720px;background-image: url('/static/images/background/0.gif');background-size: 200% 100%;background-position: top;position: absolute;
}#kof>.kof-head {width: 100%;height: 80px;position: absolute;top: 0;display: flex;align-items: center;
}/* 血条0框 */
#kof>.kof-head>.kof-head-hp-0 {height: 40px;width: calc(50% - 60px);margin-left: 20px;border: white 5px solid;border-right: none;box-sizing: border-box;
}/* 计时器 */
#kof>.kof-head>.kof-head-timer {height: 60px;width: 80px;background-color: orange;border: white 5px solid;box-sizing: border-box;color: white;font-size: 30px;font-weight: 800;text-align: center;line-height: 50px;user-select: none;
}/* 血条1框 */
#kof>.kof-head>.kof-head-hp-1 {height: 40px;width: calc(50% - 60px);border: white 5px solid;border-left: none;box-sizing: border-box;
}/* 血条0 -掉血*/
#kof>.kof-head>.kof-head-hp-0>div {background-color: red;height: 100%;width: 100%;float: right;
}/* 血条1 -掉血*/
#kof>.kof-head>.kof-head-hp-1>div {background-color: red;height: 100%;width: 100%;
}/* 血条0 */
#kof>.kof-head>.kof-head-hp-0>div>div {background-color: lightgreen;height: 100%;width: 100%;float: right;
}/* 血条1 */
#kof>.kof-head>.kof-head-hp-1>div>div {background-color: lightgreen;height: 100%;width: 100%;
}
gamemap中base.js地图中添加血条元素:
import { AcGameObject } from '/static/js/ac_game_object/base.js';
import { Controller } from '/static/js/controller/base.js';export class GameMap extends AcGameObject { //继承constructor(root) {super();this.root = root;this.$canvas = $('<canvas width="1280" height="720" tabindex=0></canvas>'); //jQuery用法,tabindex=0使得可以聚焦读取键盘输入this.ctx = this.$canvas[0].getContext('2d'); //取出canvas,这里参考canvas用法this.root.$kof.append(this.$canvas); //将canvas加入id为kof的div里this.$canvas.focus(); //使得convas可以聚焦this.controller = new Controller(this.$canvas); //将controller加入地图中,用于读取键盘输入//增加地图上血条div//地图上增加计时器div//增加地图上血条divthis.root.$kof.append($(`<div class="kof-head"><div class="kof-head-hp-0"><div><div></div></div></div> <div class="kof-head-timer">60</div> <div class="kof-head-hp-1"><div><div></div></div></div> </div>`));}start() { //开始时执行}update() { //每一帧执行this.render();}render() { //渲染函数中需要将每一帧地图都清空,否则物体运动过程会一直停留在地图上this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);}
}
player文件夹下base.js增加被攻击函数,实现扣血逻辑:
is_attack() { //被攻击函数if (this.status === 6) return;this.status = 5;this.frame_current_cnt = 0;this.hp = Math.max(this.hp - 20, 0);this.$hp_div.animate({ //jquery中.animate可以实现渐变效果width: this.$hp.parent().width() * this.hp / 100 //通过更改血条宽度实现扣血的显示}, 300); //里面的div变化慢一些,实现掉血效果this.$hp.animate({width: this.$hp.parent().width() * this.hp / 100}, 600); //外面的div变化快一些if (this.hp <= 0) { //死亡this.status = 6;this.frame_current_cnt = 0;this.vx = 0;}}
11.增加计时器
在gamemap文件夹base.js中实现计时器逻辑
import { AcGameObject } from '/static/js/ac_game_object/base.js';
import { Controller } from '/static/js/controller/base.js';export class GameMap extends AcGameObject { //继承constructor(root) {super();this.root = root;this.$canvas = $('<canvas width="1280" height="720" tabindex=0></canvas>'); //jQuery用法,tabindex=0使得可以聚焦读取键盘输入this.ctx = this.$canvas[0].getContext('2d'); //取出canvas,这里参考canvas用法this.root.$kof.append(this.$canvas); //将canvas加入id为kof的div里this.$canvas.focus(); //使得convas可以聚焦this.controller = new Controller(this.$canvas); //将controller加入地图中,用于读取键盘输入//增加地图上血条div//地图上增加计时器div//增加地图上血条divthis.root.$kof.append($(`<div class="kof-head"><div class="kof-head-hp-0"><div><div></div></div></div> <div class="kof-head-timer">60</div> <div class="kof-head-hp-1"><div><div></div></div></div> </div>`));this.time_left = 60000; // 单位:毫秒this.$timer = this.root.$kof.find(".kof-head-timer");}start() { //开始时执行}update() { //每一帧执行this.time_left -= this.timedelta; //时间逐渐减少if (this.time_left < 0) { //时间结束后两人都死亡this.time_left = 0;let [a, b] = this.root.players;if (a.status !== 6 && b.status !== 6) {a.status = b.status = 6;a.frame_current_cnt = b.frame_current_cnt = 0;a.vx = b.vx = 0;}}this.$timer.text(parseInt(this.time_left / 1000)); //修改time文本时间this.render();}render() { //渲染函数中需要将每一帧地图都清空,否则物体运动过程会一直停留在地图上this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);}
}
Web学习(五)中期项目-简易拳皇相关推荐
- Web学习笔记-中期项目(拳皇)
CONTENTS 1. 项目原理 2. 基础文件 3. ac_game_object框架 4. 游戏地图与玩家模型的创建 5. 角色状态的实现 6. 角色基础状态动画实现 7. 角色攻击与被攻击状态实 ...
- swift学习五天 项目实战-知乎日报之网络交互NSURLConnection
这里附上代码下载地址: http://download.csdn.net/detail/guchengyunfeng/7989139 网络接口 格式: 协议1: http://news-at. ...
- 【Java全栈】Java全栈学习路线及项目全资料总结【JavaSE+Web基础+大前端进阶+SSM+微服务+Linux+JavaEE】
目录 jdk api 1.8中文版 jdk api 1.8_google.CHM 零:Java 全栈知识体系 第一阶段:JavaSE 一,程序应用(★★) 二,面向对象程序设计基础(★★★) 面向对象 ...
- java web学习项目20套源码完整版
java web学习项目20套源码完整版 自己收集的各行各业的都有,这一套源码吃遍所有作业项目! 1.BBS论坛系统(jsp+sql) 2.ERP管理系统(jsp+servlet) 3.OA办公自动化 ...
- maven mybatis mysql_Java Web学习系列——Maven Web项目中集成使用Spring、MyBatis实现对MySQL的数据访问...
标签: 本篇内容还是建立在上一篇Java Web学习系列--Maven Web项目中集成使用Spring基础之上,对之前的Maven Web项目进行升级改造,实现对MySQL的数据访问. 添加依赖Ja ...
- web前端开发做项目,前端开发学习教程
毕业工作一年之后,有了转行的想法,偶然接触到程序员这方面,产生了浓厚且强烈的兴趣,开始学习前端,成功收割了大厂offer,开始了我的程序员生涯. 在自学过程中有过一些小厂的面试经历,也在一些小型的互联 ...
- 韦东山 数码相框 项目学习(四)简易的TXT文档显示器(电纸书)
韦东山 数码相框 项目学习(四)简易的TXT文档显示器(电纸书) 有了前面关于LCD.freetype的学习,已经可以开始TXT文档显示器的编写了.整个实现过程并不复杂,必须要弄清楚的是freetyp ...
- 学习机器学习的项目_辅助项目在机器学习中的重要性
学习机器学习的项目 提示与建议 (Tips and Advice) There are a few questions that are asked frequently by machine lea ...
- Docker学习五:Docker 数据管理
前言 本次学习来自于datawhale组队学习: 教程地址为: https://github.com/datawhalechina/team-learning-program/tree/master/ ...
最新文章
- Google排名第一的语言,引数十万人关注:搞定它,技术大牛都甘拜下风
- 设计模式--责任链(Responsibility_Chain)模式
- 电脑出现kernelbase.dll错误的两种解决方法
- 十分钟计算机说课稿,足球十分钟说课稿范文(精选3篇)
- 我为什么“放弃”从事八年的嵌入式领域
- 如何更有价值采集数据、高效分析数据?
- windows下boost库的基本使用方法
- AGC 030 B - Tree Burning
- 项目管理工具Redmine各功能测试
- python异常处理与导入模块与导入包
- C# .NET开发图形图像程序时提示“GDI+ 中发生一般性错误“
- JAVA开发一个合并单元格报表,纵向同值单元格的合并
- linux 中文交互最好,与linux相交互 - wsdsb的个人空间 - OSCHINA - 中文开源技术交流社区...
- 安卓智能手机运行iFIX组态软件
- 还在用老掉牙的后台模板?来试试这款人类高质量后台模板(Admin Plus)
- w7系统怎么ping服务器,怎么ping网速,教您ping网络的方法
- Flutter使用系统相机和相册获取图片
- AP计算机编程路上的照明灯----学校老师哈佛博士的伊利诺伊大学学生如何说
- 在word文档中从第3页开始编页码的方法
- Redis下载安装配置(linux版本)
热门文章
- 计算机网络中常用设备处于脱机状态,你的设备处于脱机状态请使用上次的密码登录...
- MySQL基础(四)运算符
- 哪些能提升睡眠质量的好物?改善睡眠必备神器
- Flink state使用
- 麓言信息UI设计和平面设计有什么区别?
- 查看七牛云生成qiniuUploadToken的过期时间
- Linux 10:生产者消费者问题
- MODBUS RTU通讯常见错误代码
- 黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第五章 WEB黑客(4)暴力破解HTML表单身份验证
- 《Java黑皮书基础篇第10版》 第5章【习题】【笔记】