这是实现之后的效果:
游戏界面展示
----------创建游戏界面
1.修改上节课的不足之处:
中的<script src="{% static 'js/dist/' %}"></script>z会作为全局的全局变量,会很不方便
我们将这一行删掉,在body中把script的tpye改成module,这样浏览器就会把引入的脚本识别为 JS module,且需要什么就引用什么,如下:
`<script type="module">
import {AcGame} from "{% static 'js/dist/' %}";
$(document).ready(function(){
let ac_game = new AcGame("ac_game_12345678");
});
</script>`
然后要将这个类名暴露(export)出来,也即在/game/static/js/src/的class AcGame前面加上export,即
export class AcGame
{
......
}
若出现SyntaxError: Unexpected token export报错,则将上述操作重复操作一遍即可。
----------开始写单人模式
1.为了调试方便,我们先将菜单页面关闭。
在/game/static/js/src/的class中,将创建菜单注释掉
然后在AcGamePlayground中,将游戏界面隐藏给注释掉,即可
2.打开game/static/js/src/playground/
`class AcGamePlayground {
constructor(root) {
= root;
this.$playground = $(`<div class="ac-game-playground"></div>`); #新标签,需要重新加上样式
// ();
.$ac_game.append(this.$playground);
= this.$(); #存下方面以后使用
= this.$();
this.game_map = new GameMap(this);
= [];
(new Player(this, / 2, / 2, * 0.05, "white", * 0.15, true));
for (let i = 0; i < 5; i ++ ) {
(new Player(this, / 2, / 2, * 0.05, this.get_random_color(), * 0.15, false));
}
();
}
get_random_color() {
let colors = ["blue", "red", "pink", "grey", "green"];
return colors[(() * 5)];
}
start() {
}
show() { // 打开playground界面
this.$();
}
hide() { // 关闭playground界面
this.$();
}
}
`
然后为新标签加上样式:
`.ac-game-playground
{
width: 100%; // 宽度
height: 100%; // 高度
user-select: none; //鼠标禁用菜单
}`
3.移动播放原理:每秒刷新60次,即每秒播放60张图,每一张图都会移动一点点,按照秒数去移动的时候,看起来就像是在移动一样。实现动的概念的时候,就是每秒钟重新画一张图,然后小球的坐标,每张图都会移动一点点,这样小球就可以动了。每一张图片被称为每一帧。
4.实现一个基类,即简易的游戏引擎:
在js/src/playground/下创建ac_game_object/,内容如下:
`let AC_GAME_OBJECTS = []; //将创建的物体放进一个全局数组,每一秒调用该数组里的对象60次即可
class AcGameObject {
constructor() {
AC_GAME_OBJECTS.push(this); //每一次创建都将对象直接加入全局数组
this.has_called_start = false; // 是否执行过start函数
= 0; // 当前帧距离上一帧的时间间隔,即两帧之间的时间间隔,单位ms
}
start() { // 只会在第一帧执行一次
}
update() { // 每一帧均会执行一次
}
on_destroy() { // 在被销毁前执行一次
}
destroy() { // 删掉该物体
this.on_destroy();
//在js中,当物体没有被变量存下来时,就会被自动释放掉
for (let i = 0; i < AC_GAME_OBJECTS.length; i ++ ) {
if (AC_GAME_OBJECTS[i] === this) {
AC_GAME_OBJECTS.splice(i, 1);
break;
}
}
}
}
let last_timestamp;
let AC_GAME_ANIMATION = function(timestamp) {
//每一帧我们需要遍历一次所有物体,执行一次update函数
for (let i = 0; i < AC_GAME_OBJECTS.length; i ++ ) {
let obj = AC_GAME_OBJECTS[i];
if (!obj.has_called_start) {
();
obj.has_called_start = true;
} else {
= timestamp - last_timestamp; //当前物体的时间间隔等于当前的时间戳-上一帧的时间戳
();
}
}
last_timestamp = timestamp; //更新上一秒的时间戳
requestAnimationFrame(AC_GAME_ANIMATION); //利用递归,实现每一帧都调用一次该函数
}
requestAnimationFrame(AC_GAME_ANIMATION); //js提供的api,会把一秒分成60帧,会在下一帧之前,调用一下该函数,同时把调用的时间timestamp传给该函数
`
该类,每一帧都会渲染一次物体
4.写游戏地图:
在js/src/playground/game_map/中写,用HTML里面的canvas画布渲染。(canvas可供参考)
内容如下:
`class GameMap extends AcGameObject {//GameMap为子类(派生类),AcGameObject为父类(基类),派生类可以调用基类的函数(首先判断自己是否有这个函数,若无,则调用基类中的函数)
constructor(playground) { //传人了一个类
super(); //调用一下基类的构造函数
= playground; //将画布存下来未来会用到,
this.$canvas = $(`<canvas></canvas>`); //将画布存下来
= this.$canvas[0].getContext('2d'); //未来操作是操作这个Context
= ; //长宽设置成与画布一样
= ;
.$(this.$canvas); //将这个画布加入到游戏界面中
}
start() {
}
update() {
();
}
render() { //渲染
= "rgba(0, 0, 0, 0.2)"; //黑色 20%透明度
(0, 0, , );//参数为x坐标 y坐标 宽度 高度
}
}
`
5.写玩家:
`class Player extends AcGameObject { //该类作为子类
constructor(playground, x, y, radius, color, speed, is_me) { //游戏界面,中心坐标 半径 颜色 速度 是否为自己
super(); //调用基类构造函数,存进全局数组,每秒调用60次
= playground;
= .game_map.ctx; //找到画布
= x; //玩家坐标
= y;
= 0; //x方向速度
= 0; //y方向速度
this.damage_x = 0; //伤害距离
this.damage_y = 0;
this.damage_speed = 0; //伤害速度
this.move_length = 0; //需要移动的距离
= radius;
= color;
= speed;
this.is_me = is_me;
= 0.1; //浮点运算,小于该值视为零
= 0.9; //摩擦力
this.spent_time = 0; //冷静期
this.cur_skill = null; //当前选择的是什么技能
}
start() {
if (this.is_me) { //是自己就加入监听函数
this.add_listening_events();
} else {
//随机一个坐标,让他们移动到那,
let tx = () * ;
let ty = () * ;
//移动到该坐标
this.move_to(tx, ty);
}
}
add_listening_events() { //绑定监听函数
let outer = this;
//截胡掉右键菜单事件
.game_map.$("contextmenu", function() {
return false;
});
//鼠标点击事件
.game_map.$(function(e) {
if ( === 3) { //右键
outer.move_to(, ); //鼠标坐标的api
} else if ( === 1) { //鼠标左键
if (outer.cur_skill === "fireball") {
outer.shoot_fireball(, );
}
outer.cur_skill = null; //释放之后将技能栏清空
}
});
$(window).keydown(function(e) { //判断选择的是什么技能
if ( === 81) { // q
outer.cur_skill = "fireball"; //判断出释放的是火球
return false;
}
});
}
shoot_fireball(tx, ty) {
let x = , y = ;
let radius = * 0.01;
let angle = Math.atan2(ty - , tx - );
let vx = (angle), vy = (angle);
let color = "orange";
let speed = * 0.5;
let move_length = * 1; //火球轨迹
new FireBall(, this, x, y, radius, vx, vy, color, speed, move_length, * 0.01); //每次打掉玩家20%的血量
}
get_dist(x1, y1, x2, y2) { 两点之间的距离
let dx = x1 - x2;
let dy = y1 - y2;
return (dx * dx + dy * dy);
}
move_to(tx, ty) { //移动
this.move_length = this.get_dist(, , tx, ty);
let angle = Math.atan2(ty - , tx - ); //atan2求角度 偏移量:所以应该是鼠标坐标减去玩家坐标
= (angle); //放在一个单位⚪中,利用角度求出x、y方向的速度倍数
= (angle);
}
is_attacked(angle, damage) {
//实现粒子效果,且必须放在开头,这样可以保证每次击中都能有粒子效果
for (let i = 0; i < 20 + () * 10; i ++ ) {
//从中心绽开
let x = , y = ;
//粒子大小
let radius = * () * 0.1;
let angle = * 2 * ();
let vx = (angle), vy = (angle);
let color = ;
let speed = * 10;
let move_length = * () * 5;
new Particle(, x, y, radius, vx, vy, color, speed, move_length);
}
//半径减少
-= damage;
if ( < 10) { //玩家半径小于10像素
(); //判断死亡
return false;
}
//给小球一个被击中的效果
this.damage_x = (angle);
this.damage_y = (angle);
this.damage_speed = damage * 100; //被击中的速度
*= 0.8; //被攻击后速度减慢
}
update() {
//人机发射炮弹
this.spent_time += / 1000; //计算过去了多久
//游戏开始5秒后再开始攻击 当生成的随机数小于300分之一时,就攻击一次 需要判断是否为人机
if (!this.is_me && this.spent_time > 4 && () < 1 / 300. 0) {
//人机每次随机 攻击一个小球
let player = [(() * )];
//速度 * 行动轨迹 智能人机
let tx = + * * / 1000 * 0.3;
let ty = + * * / 1000 * 0.3;
this.shoot_fireball(tx, ty);
}
if (this.damage_speed > 10) { //如果有伤害速度
则不能操控小球
= = 0;
this.move_length = 0;
+= this.damage_x * this.damage_speed * / 1000;
+= this.damage_y * this.damage_speed * / 1000;
this.damage_speed *= ;
} else {
if (this.move_length < ) { //如果需要移动的距离小于设定的初始值
this.move_length = 0; //则判定不需要移动了
= = 0; //速度归0
if (!this.is_me) { //如果停下来的是人机,则重新给他们规划路线
let tx = () * ;
let ty = () * ;
this.move_to(tx, ty);
}
} else {
//当前每秒的速度 / 1000 * 时间差 = 这段时间差需要移动的距离 (这是按照当前速度需要移动的距离)但不能出界,需要与实际需要移动的距离取最小值
let moved = (this.move_length, * / 1000); //实际移动距离
+= * moved; //玩家坐标等于当前坐标加上当前方向乘上距离
+= * moved;
this.move_length -= moved; //更新需要移动的距离
}
}
();
}
render() {
//画⚪
();
(, , , 0, * 2, false);坐标 角度 是否顺逆时针
= ; //颜色
();
}
//玩家死之后 注销玩家
on_destroy() {
for (let i = 0; i < ; i ++ ) {
if ([i] === this) { //这句话表示玩家死亡
(i, 1);//注销玩家
}
}
}
}
`
6.写火球技能:
在game/static/js/src/playground/skill/fireball/中写,内容如下:
`class FireBall extends AcGameObject {
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) { //伤害
super();
= playground;
= player;
= .game_map.ctx;
= x;
= y;
= vx;
= vy;
= radius;
= color;
= speed;
this.move_length = move_length;
= damage;
= 0.1;
}
start() {
}
update() {
if (this.move_length < ) { //移动完了
(); //直接清空
return false; //火球消失
}
let moved = (this.move_length, * / 1000);
+= * moved;
+= * moved;
this.move_length -= moved;
//玩家火球与人机碰撞检测
for (let i = 0; i < ; i ++ ) {
let player = [i];
if ( !== player && this.is_collision(player)) { //非自己 且与玩家碰撞了
(player); //击打他一下
}
}
();
}
get_dist(x1, y1, x2, y2) { //获取两点之间的距离
let dx = x1 - x2;
let dy = y1 - y2;
return (dx * dx + dy * dy);
}
is_collision(player) { //火球与玩家是否碰撞
let distance = this.get_dist(, , , ); //求出火球与玩家的距离
if (distance < + ) //距离小于玩家与火球球心距离,判断为击中
return true;
return false;
}
attack(player) { //击中玩家
//方向
let angle = Math.atan2( - , - );
player.is_attacked(angle, );
();
}
render() {
();
(, , , 0, * 2, false);
= ;
();
}
}
`
7.写被攻击后的粒子效果:
在game/static/js/src/playground/animation/particle/下写,内容如下:
`class Particle extends AcGameObject {
constructor(playground, x, y, radius, vx, vy, color, speed, move_length) {
super();
= playground;
= .game_map.ctx;
= x;
= y;
= radius;
= vx;
= vy;
= color;
= speed;
this.move_length = move_length;
= 0.9;
= 1;
}
start() {
}
update() {
//判断速度有没有变成很小的值,或者需要移动距离为 0时
if (this.move_length < || < ) {
(); //消失
return false;
}
let moved = (this.move_length, * / 1000);
+= * moved;
+= * moved;
*= ; //无线趋近于0
this.move_length -= moved;
();
}
render() {
();
(, , , 0, * 2, false);
= ;
();
}
}
`