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学习(五)中期项目-简易拳皇相关推荐

  1. Web学习笔记-中期项目(拳皇)

    CONTENTS 1. 项目原理 2. 基础文件 3. ac_game_object框架 4. 游戏地图与玩家模型的创建 5. 角色状态的实现 6. 角色基础状态动画实现 7. 角色攻击与被攻击状态实 ...

  2. swift学习五天 项目实战-知乎日报之网络交互NSURLConnection

    这里附上代码下载地址:     http://download.csdn.net/detail/guchengyunfeng/7989139 网络接口 格式: 协议1: http://news-at. ...

  3. 【Java全栈】Java全栈学习路线及项目全资料总结【JavaSE+Web基础+大前端进阶+SSM+微服务+Linux+JavaEE】

    目录 jdk api 1.8中文版 jdk api 1.8_google.CHM 零:Java 全栈知识体系 第一阶段:JavaSE 一,程序应用(★★) 二,面向对象程序设计基础(★★★) 面向对象 ...

  4. java web学习项目20套源码完整版

    java web学习项目20套源码完整版 自己收集的各行各业的都有,这一套源码吃遍所有作业项目! 1.BBS论坛系统(jsp+sql) 2.ERP管理系统(jsp+servlet) 3.OA办公自动化 ...

  5. maven mybatis mysql_Java Web学习系列——Maven Web项目中集成使用Spring、MyBatis实现对MySQL的数据访问...

    标签: 本篇内容还是建立在上一篇Java Web学习系列--Maven Web项目中集成使用Spring基础之上,对之前的Maven Web项目进行升级改造,实现对MySQL的数据访问. 添加依赖Ja ...

  6. web前端开发做项目,前端开发学习教程

    毕业工作一年之后,有了转行的想法,偶然接触到程序员这方面,产生了浓厚且强烈的兴趣,开始学习前端,成功收割了大厂offer,开始了我的程序员生涯. 在自学过程中有过一些小厂的面试经历,也在一些小型的互联 ...

  7. 韦东山 数码相框 项目学习(四)简易的TXT文档显示器(电纸书)

    韦东山 数码相框 项目学习(四)简易的TXT文档显示器(电纸书) 有了前面关于LCD.freetype的学习,已经可以开始TXT文档显示器的编写了.整个实现过程并不复杂,必须要弄清楚的是freetyp ...

  8. 学习机器学习的项目_辅助项目在机器学习中的重要性

    学习机器学习的项目 提示与建议 (Tips and Advice) There are a few questions that are asked frequently by machine lea ...

  9. Docker学习五:Docker 数据管理

    前言 本次学习来自于datawhale组队学习: 教程地址为: https://github.com/datawhalechina/team-learning-program/tree/master/ ...

最新文章

  1. Google排名第一的语言,引数十万人关注:搞定它,技术大牛都甘拜下风
  2. 设计模式--责任链(Responsibility_Chain)模式
  3. 电脑出现kernelbase.dll错误的两种解决方法
  4. 十分钟计算机说课稿,足球十分钟说课稿范文(精选3篇)
  5. 我为什么“放弃”从事八年的嵌入式领域
  6. 如何更有价值采集数据、高效分析数据?
  7. windows下boost库的基本使用方法
  8. AGC 030 B - Tree Burning
  9. 项目管理工具Redmine各功能测试
  10. python异常处理与导入模块与导入包
  11. C# .NET开发图形图像程序时提示“GDI+ 中发生一般性错误“
  12. JAVA开发一个合并单元格报表,纵向同值单元格的合并
  13. linux 中文交互最好,与linux相交互 - wsdsb的个人空间 - OSCHINA - 中文开源技术交流社区...
  14. 安卓智能手机运行iFIX组态软件
  15. 还在用老掉牙的后台模板?来试试这款人类高质量后台模板(Admin Plus)
  16. w7系统怎么ping服务器,怎么ping网速,教您ping网络的方法
  17. Flutter使用系统相机和相册获取图片
  18. AP计算机编程路上的照明灯----学校老师哈佛博士的伊利诺伊大学学生如何说
  19. 在word文档中从第3页开始编页码的方法
  20. Redis下载安装配置(linux版本)

热门文章

  1. 计算机网络中常用设备处于脱机状态,你的设备处于脱机状态请使用上次的密码登录...
  2. MySQL基础(四)运算符
  3. 哪些能提升睡眠质量的好物?改善睡眠必备神器
  4. Flink state使用
  5. 麓言信息UI设计和平面设计有什么区别?
  6. 查看七牛云生成qiniuUploadToken的过期时间
  7. Linux 10:生产者消费者问题
  8. MODBUS RTU通讯常见错误代码
  9. 黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第五章 WEB黑客(4)暴力破解HTML表单身份验证
  10. 《Java黑皮书基础篇第10版》 第5章【习题】【笔记】