3.1 前期准备
3.1.1 模块化引用 js 文件
进入 /game/templates/multiends
打开 web.html
:
<script src="{% static 'js/dist/game.js' %}"></script>
使用这种引用方式会将所有的 js
对象作为网页内部的全局变量引入,为防止后续引用的 js
文件发生命名冲突,我们改为模块化引用。
首先将该文件修改为:
{% load static %} <!--查找并载入静态文件static的文件夹--> <head> <link rel="stylesheet" href="https://cdn.acwing.com/static/jquery-ui-dist/jquery-ui.min.css"> <script src="https://cdn.acwing.com/static/jquery/js/jquery-3.3.1.min.js"></script> <link rel="stylesheet" href="{% static 'css/game.css' %}"> <!--删掉该行<script src="{% static 'js/dist/game.js' %}"></script>--> </head> <body style="margin: 0"> <div id="ac_game_12345678"></div> <!--修改此处为:--> <script type="module"> import {AcGame} from "{% static 'js/dist/game.js' %}" $(document).ready(function(){ let ac_game = new AcGame("ac_game_12345678"); }); </script> </body>
然后修改 AcGame
类对象的引入方式,进入 /game/static/js/src
,打开 zbase.js
:
export class AcGame { //此处添加 export constructor(id) { this.id = id; this.$ac_game = $(`#` + id); this.menu = new AcGameMenu(this); this.playground = new AcGamePlayground(this); } }
修改 js
文件后记得在 /script
下运行打包脚本重新打包。
3.1.2 修改页面显示
为了便于游戏界面的调试,我们先不显示菜单界面,默认直接打开游戏界面。
还是进入 /game/static/js/src
,打开 zbase.js
:
export class AcGame { //此处添加 export constructor(id) { this.id = id; this.$ac_game = $(`#` + id); //this.menu = new AcGameMenu(this); 将该行注释掉,不生成菜单界面对象 this.playground = new AcGamePlayground(this); } }
然后进入 /game/static/js/src/playground
,打开 zbase.js
:
class AcGamePlayground { constructor(root) { this.root = root; this.$playground = $(`<div>lys is a dog</div>`); //this.hide(); 注释掉改行,不默认关闭 this.root.$ac_game.append(this.$playground); this.start(); } start() { } show() { // 打开playground界面 this.$playground.show(); } hide() { // 关闭playground界面 this.$playground.hide(); } }
修改 js
文件后记得在 /script
下运行打包脚本重新打包。
3.1.3 创建游戏界面对象
首先进入 game/static/js/src/playground/zbase.js
,创建新的 html
类:
class AcGamePlayground { constructor(root) { this.root = root; this.$playground = $(`<div class="ac_game_playground">lys is a dog</div>`); //创建新的html对象 //this.hide(); 注释掉改行,不默认关闭 this.root.$ac_game.append(this.$playground); this.start(); } start() { } show() { // 打开playground界面 this.$playground.show(); } hide() { // 关闭playground界面 this.$playground.hide(); } }
同时要在 game/static/css
里面添加该 html
类的 css
样式:
.ac_game_playground { width: 100%; height: 100%; user-select: none; }
3.2 游戏界面文件结构
game/static |-- css | `-- game.css |-- image | |-- menu | | `-- background.png | |-- playground | `-- settings `-- js |-- dist | `-- game.js `-- src |-- menu | `-- zbase.js |-- playground #游戏界面 | |-- ac_game_object #可动对象的基类 | | `-- zbase.js | |-- game_map #地图 | | `-- zbase.js | |-- particle #动效 | | `-- zbase.js | |-- player #人物 | | `-- zbase.js | |-- skill #技能 | | `-- fireball | | `-- zbase.js | `-- zbase.js |-- settings `-- zbase.js
3.3 游戏界面文件创建
3.3.1 创建可动对象的基类文件
进入 game/static/js/src/playground/ac_game_object
,创建 zbase.js
:
//将创建的对象存入全局数组里,之后每秒调用数组里的对象调用60次 let AC_GAME_OBJECTS = []; class AcGameObject { constructor() { AC_GAME_OBJECTS.push(this); //创建对象加入数组 this.has_called_start = false; //标记是否执行过start函数 this.timedelta = 0; //当前帧距离上一帧的时间间隔 } start() { //只会在第一帧执行一次 } update() { //每一帧都会执行一次 } on_destory() { //在物体被销毁前执行一次 } destory () { //删除当前物体 this.on_destory(); for(let i = 0; i < AC_GAME_OBJECTS.length; i ++){ if(AC_GAME_OBJECTS[i] === this) { //找到需要删除的对象 AC_GAME_OBJECTS.splice(i, 1); i --; } } } } let last_timestamp; //上一帧的时间戳 let AC_GAME_ANIMATION = function(timestamp) { //timestamp是传入的当前时间 for(let i = 0; i < AC_GAME_OBJECTS.length; i ++){ //更新所有的可以动的对象 let obj = AC_GAME_OBJECTS[i]; if(!obj.has_called_start){ obj.start(); obj.has_called_start = true; } else{ obj.timedelta = timestamp - last_timestamp; //更新对象的时间间隔 obj.update(); //更新这一帧对象的位置 } } last_timestamp = timestamp; requestAnimationFrame(AC_GAME_ANIMATION); //递归调用 } requestAnimationFrame(AC_GAME_ANIMATION); //js API调用一帧里面的函数
3.3.2 创建地图文件
进入 game/static/js/src/playground/game_map
,创建 zbase.js
:
class GameMap extends AcGameObject { constructor(playground) { //将playground的参数传进来 super(); this.playground = playground; //存下来 this.$canvas = $(`<canvas></canvas>`); //API 创建画布 this.ctx = this.$canvas[0].getContext('2d'); this.ctx.canvas.width = this.playground.width; //画布参数 this.ctx.canvas.height = this.playground.height; this.playground.$playground.append(this.$canvas); //传回创建的对象 } start() { } //每一帧都会调用的更新函数 update() { this.render(); } render() { //不断创建画布 this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)"; //背景颜色和透明度 this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); //js的API } }
3.3.3 创建玩家文件
进入 game/static/js/src/playground/player
,创建 zbase.js
:
class Player extends AcGameObject { constructor(playground, x, y, radius, color, speed, is_me, life) { //传入需要处理的参数 super(); this.playground = playground; this.ctx = this.playground.game_map.ctx; //坐标 this.x = x; this.y = y; //速度方向 this.vx = 0; this.vy = 0; //受到伤害的速度方向和速度 this.damage_x = 0; this.damage_y = 0; this.damage_speed = 0; this.friction = 0.9; //摩擦力 this.move_length = 0; //移动距离 this.radius = radius; //该对象的半径 this.color = color; //颜色 this.speed = speed; //速度 this.is_me = is_me; //是否是玩家 this.life = life; //生命值 this.eps = 0.1; //精度 this.cur_skill = null; //当前选择的技能 this.spent_time = 0; //开局静默期 } start() { //是自己本身 if(this.is_me) { this.add_listening_events(); //通过监听函数控制 } else { //敌人 //通过随机生成的坐标控制移动 let tx = Math.random()*this.playground.width; let ty = Math.random()*this.playground.height; this.move_to(tx, ty); } } //监听函数,判断鼠标点击行为 add_listening_events() { let outer = this; if(this.life <= 0) return false; //死亡不再接收指令 this.playground.game_map.$canvas.on("contextmenu", function() { //截断鼠标右键显示菜单选项 return false; }); //监听鼠标移动 this.playground.game_map.$canvas.mousedown(function(e) { if(e.which === 3) { //判断鼠标的键位 1是左键, 2是滚轮 outer.move_to(e.clientX, e.clientY); //鼠标点击移动API } else if(e.which === 1) { if(outer.cur_skill === "fireball") { //发射火球 outer.shoot_fireball(e.clientX, e.clientY, this.color); } else if(outer.cur_skill === "go_to") { //闪现方向 outer.go_to(e.clientX, e.clientY); } outer.cur_skill = null; //清空当前的技能选择 } }); //监听键盘按键 $(window).keydown(function(e) { //keycode if(e.which === 81) { //按 'Q' 发射火球 outer.cur_skill = "fireball"; return false; } else if(e.which === 69) { //按 'E' 闪现 outer.cur_skill = "go_to"; return false; } }); } //发射火球 shoot_fireball(tx, ty, color) { let x = this.x, y = this.y; //发射位置为当前位置 let radius = this.playground.height*0.01; //火球半径 let angle = Math.atan2(ty - this.y, tx - this.x); //计算当前位置相对鼠标点击坐标的方向角度 let vx = Math.cos(angle), vy = Math.sin(angle); //计算速度的方向 let speed = this.speed*2; //火球速度为自身移动速度的2倍数 let move_length = this.playground.height*1; //火球移动的最大距离 if(this.life > 0) new FireBall(this.playground, this, x, y, radius, vx, vy, this.color, speed, move_length, this.playground.height*0.01); //当前对象存活才可发射火球 //console.log("fireball", tx, ty); //if(this.is_me) console.log("life:", this.life); } //瞬移操作 go_to(tx, ty) { this.x = tx; //直接更新位置 this.y = ty; this.move_length = 0; //重置移动方向和距离 } //计算移动的相对距离 get_dist(x1, y1, x2, y2) { let dx = x1 - x2; let dy = y1 - y2; return Math.sqrt(dx*dx + dy*dy); } //移动的方向 move_to(tx, ty) { this.move_length = this.get_dist(this.x, this.y, tx, ty); let angle = Math.atan2(ty - this.y, tx - this.x); //计算相对位置的角度 this.vx = Math.cos(angle), this.vy = Math.sin(angle); } //受到攻击后执行的逻辑 is_attacked(angle, damage) { if(this.life <= 0) return false; //生命值归零的对象直接忽视 //释放粒子效果 for(let i = 0; i < 10 + Math.random()*5; i ++){ let x = this.x, y = this.y; let radius = this.radius*Math.random()*0.11; //粒子大小半径 let angle = Math.PI*2*Math.random(); //随机的角度 let vx = Math.cos(angle), vy = Math.sin(angle); let color = this.color; //粒子颜色 let speed = this.speed*4; //释放速度 let move_length = this.radius*Math.random()*10; //粒子释放半径 new Particle(this.playground, x, y, radius, vx, vy, color, speed, move_length); //基于上述参数生成粒子对象 } this.radius -= damage*0.65; //受到攻击变小 this.speed *= 0.88; //速度减慢 this.life -= 1; //生命值降低 if(this.life <= 0){ //生命值归零即为死亡 this.destory(); //销毁该对象 return false; } else { //受击的击退效果 this.damage_x = Math.cos(angle); //击退的方向 this.damage_y = Math.sin(angle); this.damage_speed = damage*50; //击退的速度 } } //每一帧刷新 update() { //生命值归零直接销毁对象 if(this.life <= 0) { this.destory(); return false; } //更新静默的时间 this.spent_time += this.timedelta/1000; if(this.damage_speed > this.eps) { //当前存在受击的方向和速度则先被击退 //打断当前的移动 this.vx = this.vy = 0; this.move_length = 0; //更改击退的位置和方向 this.x += this.damage_x*this.damage_speed*this.timedelta/1000; this.y += this.damage_y*this.damage_speed*this.timedelta/1000; this.damage_speed *= this.friction; //摩擦效果 } else { if(!this.is_me) { //人机模式下敌人的攻击规则 if(Math.random() < 1/250.0 && this.spent_time > 3) { //攻击频率和静默时间 //随机攻击当前场上存在的人 let player = this.playground.players[Math.floor(Math.random()*this.playground.players.length)]; //只朝玩家攻击(地狱模式QAQ) //let player = this.playground.players[0]; //发射火球 this.shoot_fireball(player.x, player.y, this.color); } } //当前移动距离为0,即到达了上一次移动的终点位置 if(this.move_length < this.eps) { //重置速度和移动距离 this.vx = this.vy = 0; this.move_length = 0; if(!this.is_me) { //人机再随机一个坐标方向移动 let tx = Math.random()*this.playground.width; let ty = Math.random()*this.playground.height; this.move_to(tx, ty); } } else { //移动 let moved = Math.min(this.move_length, this.speed*this.timedelta/1000); //这一帧的移动距离 this.x += this.vx*moved; //移动后的位置 this.y += this.vy*moved; this.move_length -= moved; //更新还需要移动的距离 } } this.render(); //调用渲染函数,每一帧都要重新渲染该对象的位置,否则会消失 } render() { this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2, false); //画圆 this.ctx.fillStyle = this.color; this.ctx.fill(); } }
3.3.4 创建火球技能文件
进入 game/static/js/src/playground/skill/fireball
,创建 zbase.js
:
class FireBall extends AcGameObject { constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) { super(); this.playground = playground; this.ctx = this.playground.game_map.ctx; this.player = player; //火球位置 this.x = x; this.y = y; //火球半径 this.radius = radius; //火球速度方向 this.vx = vx; this.vy = vy; this.color = color; //颜色 this.speed = speed; //速度 this.move_length = move_length; //运动距离 this.damage = damage; //伤害 this.eps = 0.1; //精度 } start() { } update() { //到达最大距离消失 if(this.move_length < this.eps){ this.destory(); return false; } //更新距离,逻辑同player let moved = Math.min(this.move_length, this.speed*this.timedelta/1000); this.x += this.vx*moved; this.y += this.vy*moved; this.move_length -= moved; //判断火球是否击中某个球 for(let i = 0; i < this.playground.players.length; i ++) { let player = this.playground.players[i]; if(this.player !== player && this.is_collision(player) && this.player.life > 0) { this.attack(player); //调用击中函数 } } //调用渲染函数 this.render(); } //获取火球和该player的中心距离 get_dist(x1, y1, x2, y2) { let dx = x1 - x2; let dy = y1 - y2; return Math.sqrt(dx*dx + dy*dy); } //判断是否可以击中 is_collision(player) { let distance = this.get_dist(this.x, this.y, player.x, player.y); if(distance < this.radius + player.radius) return true; else return false; } //击中之后的逻辑 attack(player) { if(player.life > 0) this.destory(); //击中后销毁火球 let angle = Math.atan2(player.y - this.y, player.x - this.x); //计算角度,用于求击退速度方向 player.is_attacked(angle, this.damage); //调用player里的逻辑函数 return false; } render() { this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2, false); this.ctx.fillStyle = this.color; this.ctx.fill(); } }
3.3.5 创建动效文件
进入 game/static/js/src/playground/particle
,创建 zbase.js
:
class Particle extends AcGameObject { constructor(playground, x, y, radius, vx, vy, color, speed, move_length) { super(); this.playground = playground; this.ctx = this.playground.game_map.ctx; this.x = x; this.y = y; this.radius = radius; this.vx = vx; this.vy = vy; this.color = color; this.speed = speed; this.move_length = move_length; this.friction = 0.9; this.eps = 3; } start() { } update() { if(this.move_length < this.eps || this.speed < this.eps) { this.destory(); return false; } let moved = Math.min(this.move_length, this.speed*this.timedelta/1000); this.x += this.vx*moved; this.y += this.vy*moved; this.move_length -= moved; this.speed *= this.friction; this.render(); } render() { this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2, false); this.ctx.fillStyle = this.color; this.ctx.fill(); } }
3.3.6 更新总文件
进入 game/static/js/src/playground
,打开 zbase.js
:
class AcGamePlayground { constructor(root) { this.root = root; this.$playground = $(`<div class="ac_game_playground">lys is a dog</div>`); // this.hide(); this.root.$ac_game.append(this.$playground); this.width = this.$playground.width(); this.height = this.$playground.height(); this.game_map = new GameMap(this); //创建地图对象 this.players = []; //存储所有的玩家对象 //创建玩家本身 this.players.push(new Player(this, this.width/2, this.height/2, this.height*0.05, "white", this.height*0.35, true, 5)); //此处大小和界面大小绑定,便于适应不同大小的窗口 //添加敌人 for(let i = 0; i < 4; i ++) { this.players.push(new Player(this, this.width/2, this.height/2, this.height*0.05, this.get_random_color(), this.height*0.35, false, 5)); } this.start(); } //随机的敌人颜色 get_random_color() { let colors = ["blue", "red", "pink", "green", "grey"]; return colors[Math.floor(Math.random()*5)]; } start() { } show() { // 打开playground界面 this.$playground.show(); } hide() { // 关闭playground界面 this.$playground.hide(); } }
最后进入 game/scripts
,运行之前写好的打包文件脚本,启动服务查看效果即可。