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
,运行之前写好的打包文件脚本,启动服务查看效果即可。