Django框架课-创建游戏界面 (1)
创建游戏界面 (1)
最后的结构:
playground/
|-- ac_game_object
| `-- zbase.js
|-- game_map
| `-- zbase.js
|-- particle
| `-- zbase.js
|-- player
| `-- zbase.js
|-- skill
| `-- fireball
| `-- zbse.js
`-- zbase.js
修改成以模块单位引入js代码
前面的工作都是直接把js代码简单拼接,但是如果有重名的变量是有可能出错的。
所以修改成以模块形式导入js代码
修改:acapp/game/templates/multiends/web.html
{% load 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' %}">
</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>
然后修改static/js/src/zbase.js
最前面加上一个export
export class AcGame {
constructor(id){
this.id = id;
this.$ac_game = $('#' + id);
this.menu = new AcGameMenu(this);
this.playground = new AcGamePlayground(this);
this.start();
}
start(){
}
}
写游戏界面
写游戏界面的时候可以先把菜单界面关闭掉,让他只显示游戏界面,这样不用每次刷新都要点击一下单人模式才能查看自己写的效果。
class AcGamePlayground{
constructor(root){
this.root = root;
this.$playground = $(`<div class="ac-game-playground"></div>`);
// this.hide();
this.root.$ac_game.append(this.$playground);
this.start();
}
start(){
}
show(){ // 打开playground界面
this.$playground.show();
}
hide(){ // 关闭playground界面
this.$playground.hide();
}
}
添加基本的playground的css (添加到game.css文件的末尾即可)
.ac-game-playground{
width: 100%;
height: 100%;
user-select: none;
}
playground/zbase.js
游戏在一个画布之中,我们要先把其宽高记下来以备使用(作为基本属性写在AcGamePlayground
类的构造函数中)
class AcGamePlayground{
constructor(root){
this.root = root;
this.$playground = $(`<div class="ac-game-playground"></div>`);
// this.hide();
this.root.$ac_game.append(this.$playground);
// 宽高
this.width = this.$playground.width();
this.height = this.$playground.height();
this.start();
}
start(){
}
show(){ // 打开playground界面
this.$playground.show();
}
hide(){ // 关闭playground界面
this.$playground.hide();
}
}
游戏人物、物体的“动”是如何实现?
电影是一张张图片每秒钟快速切换n张图片,连续起来就成为了电影。
游戏也是如此。
每秒钟刷新60次(播放60张图片),每一张图片让人物/物体稍微一动一点点,这样就看着是“动”起来了。
要写的游戏是术士之战。每一个角色小球,每一个技能小球等等都要在每秒钟刷新60次,我们可以写一个js基类AcGameObject
来实现每一帧去渲染出一个图片
AcGameObject基类
进入js/src/playground/ac_game_object
新建zbase.js
每一个是这个AcGameObject
基类的物体,每一帧都要画出来。我们搞个全局变量数组存起来这些物体对象,每秒中调用这个数组里的对象60次(60帧)。
这样的对象应该有三个函数:
一开始就执行一次的函数。比如一个小球刚出现的时候其基本信息,颜色、大小、昵称等(从服务器端接受过来初始化),一般在第一帧执行。
总是更新的函数,比如实现移动。每一帧都执行的函数。
删除函数,删除这个对象的函数。删除对象就直接从全局数组中删除这个对象就行。从全局数组中删除之后就不会再去渲染它,而且删除后也会释放内存。
一个物体被删掉之前可能需要恢复现场,比如一个玩家被打死了,血量变成0了,但是在删除之前可能需要给对手加分啊什么的,这个时候就还需要再添加一个函数,某个物体被删除前需要执行的函数。
一个简易的游戏引擎就是这样了。
let AC_GAME_OBJECTS = [];
class AcGameObject{
constructor() {
AC_GAME_OBJECTS.push(this);
}
start() { // 只在第一帧执行一次
}
update() { // 每一帧都会执行一次
}
on_destroy(){ // 物体在被销毁前执行一次
}
destroy(){ // 删除该物体
this.on_destroy();
for (let i = 0; i < AC_GAME_OBJECTS.length; i ++) {
if (AC_GAME_OBJECTS[i] === this) {
AC_GAME_OBJECTS.splice(i,1);
break;
}
}
}
}
实现每一帧去循环一次数组里的所有对象,使用js中的一个api: requestAnimationFrame()
使用这个api调用我们写的动画函数(回调函数),可以保证每一秒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_destroy(){ // 物体在被销毁前执行一次
}
destroy(){ // 删除该物体
this.on_destroy();
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) {
// 遍历数组中每一个对象,刷新动画
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);
创建游戏地图
之前已经创建了一个AcGamePlayground
类(playground/zbase.js),这个里面存了游戏界面的宽和高,创建游戏地图要用canvas画布,先新建一个game_map/zbase.js
中写一个GameMap
类来生成画canvas画布
GameMap
是我们画游戏地图的一个类,这个游戏地图也要一帧一帧的刷新所以也要继承AcGameObject
游戏对象基类
GameMap
: (playground/game_map/zbase.js
)
class GameMap extends AcGameObject {
constructor(playground) { // 传入AcGamePlayground的对象实例
super(); // 继承基类的构造函数
this.playground = playground;
this.$canvas = $(`<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() {
}
}
然后修改AcGamePlayground
: (playground/zbase.js
)
先创建一个地图
class AcGamePlayground{
constructor(root){
this.root = root;
this.$playground = $(`<div class="ac-game-playground"></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.start();
}
start(){
}
show(){ // 打开playground界面
this.$playground.show();
}
hide(){ // 关闭playground界面
this.$playground.hide();
}
}
现在访问http://47.94.107.232:8000/ F12就可以看到ac-game-playground下生成的canvas标签了
画地图背景:
每一帧渲染出黑色背景,写一个render函数,放在update中。修改playground/game_map/zbase.js :
class GameMap extends AcGameObject {
constructor(playground) {
super(); // 继承基类的构造函数
this.playground = playground;
this.$canvas = $(`<canvas></canvas>`);
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() {
// 每一帧都要花一次所以rander在这里执行
this.render();
}
// 渲染游戏背景
render() {
this.ctx.fillStyle = "rgba(0,0,0)";
this.ctx.fillRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);
}
}
现在访问就可以看到黑色的背景。
地图创建完毕,下面创建玩家
创建游戏玩家
每一个玩家是一个小球
玩家也是需要每一帧画出来的东西,所以也继承AcGameObject
游戏对象基类
构造函数需要传入,地图、横纵坐标(如果是三维的游戏就要再传一个z轴坐标)、小球半径、小球颜色、移动速度、是否是玩家自己。
关于玩家的移动速度
参数:其表示应该是每秒移动百分之多少,屏幕高度的百分比去表示,每秒移动其百分之多少。因为是一个联机游戏,如果用每秒移动了多少个像素来表示速度,不同玩家的屏幕分辨率不同,分辨率高的屏幕像素就多,就会显得移动的慢,这样对于联机的其他玩家是不公平的。
关于是否是玩家自己
参数:玩家们的小球的移动方式有两种,一种是通过键盘鼠标(玩家自己),另一种是通过网络传输(联网对局中的其他玩家的移动信息传输过来),所以要加一个is_me参数来判断是否是玩家自己
playground/zbase.js :
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.01; // 移动的时候会涉及到浮点运算,小于0.01判断为0
}
start() { // 第一帧执行的函数
}
update() { // 每一帧执行的函数
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();
}
}
然后修改AcGamePlayground
: (playground/zbase.js
)
先创建一个玩家自己,让玩家自己是白色,生成在屏幕中间
class AcGamePlayground{
constructor(root){
this.root = root;
this.$playground = $(`<div class="ac-game-playground"></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.15, true)); // 创建玩家自己,is_me为true表示是玩家自己
this.start();
}
start(){
}
show(){ // 打开playground界面
this.$playground.show();
}
hide(){ // 关闭playground界面
this.$playground.hide();
}
}
效果如下:
为了更加美观,可以将背景每次刷新的透明度改成半透明的背景,这样就可以是一种渐变式的渲染。
GameMap
类中:render()时
this.ctx.fillStyle = "rgba(0,0,0,0.2)";
// rgba再加一个透明度参数(值为0.2)就行
实现玩家移动
给这个类加一个速度属性,并且update的时候每次让坐标加上速度就行
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.vx = 1; // 加一个速度属性
this.vy = 1;
this.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.1;
}
start() {
}
update() {
// 每次update让其坐标加上每一帧移动的距离就行
this.x += this.vx;
this.y += this.vy;
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();
}
}
下面的图就是移动过程中的截图,可以看到移动方向尾部会有一个模糊化的光圈,这个就是因为尾端只渲染了透明度为0.2的背景,而头端(方向朝向端)渲染的多就显示的更加清楚。
鼠标实现控制移动方向
现在只是向一个方向移动。MOBA类游戏是鼠标控制移动方向,那我们就让鼠标控制移动速度就行。
前面说了,球的移动方式有两种,一种是键盘鼠标,另一种是网络传参。
玩家想要控制自己的球,要用键盘鼠标,第一步就是先判断此球是否是玩家自己。
在start(){}
中判断是否是玩家自己。
if(this.is_me){
this.add_listening_events(); // 如果是自己就添加事件
}
先将鼠标的右键菜单隐藏,然后将鼠标右键的位置传进来
add_listening_events() {
let outer = this;
// 如果右键,不处理菜单事件(鼠标右键之后,不会显示右键菜单)
this.playground.game_map.$canvas.on("contextmenu",function(){
return false;
});
this.playground.game_map.$canvas.mousedown(function(e){
if(e.which === 3){ // 左键1 中键2 右键3
outer.move_to(e.clientX,e.clientY); // 这里要用outer才能调用到move_to方法,直接用this代表的是这个函数本身
}
});
}
move_to(tx,ty) {
console.log("move to",tx,ty);
}
随便鼠标右击几个位置:
实现鼠标控制移动
起始坐标为(x,y),终点坐标为(tx,ty)
Player
的构造函数加一个
this.move_length = 0;
,这个是保存距离目标地点的距离。
求距离,需要求出两点间的距离(x1,y1)到(x2,y2):
get_dist(x1, y1, x2, y2) {
let dx = x1 - x2;
let dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
求出横纵方向上的速度,先求角度,然后根据角度的cos和sin值乘1得到x、y轴上的速度。
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);
}
改写update()
让其移动
如果需要移动的距离(move_length)小于了eps
,就不需要移动了,认为到达了目标地点。
让move_length
、vx
、vy
置0
否则就计算出真实移动的距离,进行移动。
真实移动的距离是move_length
与每一帧移动距离(speed*timedela/1000
单位为毫秒,要除以1000)的最小值
让x、y加上该方向上的速度乘以真实移动的距离
最后让move_length
减去已经移动的距离(更新成还需要移动的距离)
update() {
if (this.move_length < this.eps) {
this.move_length = 0;
this.vx = this.vy = 0;
} 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();
}
最后代码:
js/src/playground/player/zbase.js
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.move_length = 0;
this.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.1;
}
start() {
if(this.is_me) {
// 如果这个球是玩家自己,玩家就可以控制鼠标移动此球
this.add_listening_events();
}
}
add_listening_events() {
let outer = this;
// 如果右键,不处理菜单事件(鼠标右键之后,不会显示右键菜单)
this.playground.game_map.$canvas.on("contextmenu",function(){
return false;
});
this.playground.game_map.$canvas.mousedown(function(e) {
if(e.which === 3) {
outer.move_to(e.clientX,e.clientY); // 这里要用outer才能调用到move_to方法,直接用this代表的是这个函数本身
}
});
}
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);
}
update() {
if (this.move_length < this.eps) {
this.move_length = 0;
this.vx = this.vy = 0;
} 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();
}
}
最后效果:
鼠标右键某个位置,小球会自动跑到那个位置
(下图是右键了地图左上角区域,小球移动过程中的图片)
实现火球技能
火球类
playground中创建skill文件夹
进入到skill文件夹中,创建fireball文件夹,进入fireball文件夹
写火球对象zbase.js
火球和刚才的玩家类似,传入的参数有地图,玩家(用来判断火球是谁发出的,要不要对这个玩家造成伤害如果是自己发出的,就不可以对自己造成伤害),x,y坐标,速度vx,vy,颜色,speed,移动距离(至目的地的距离,会不断减小,减小到0就不再移动,说明到达了目的地)
class FireBall extends AcGameObject {
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length) {
super();
this.playground = playground;
this.player = player;
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.eps = 0.1; // 距离小于0.1表示为0
}
start() {
}
update() {
if(this.move_length < this.eps) { // 如果移动距离为0,就不再移动,调用基类的destory函数销毁火球
this.destroy();
return false;
}
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); // thimedelta是一帧的时间单位毫秒,要模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();
}
}
火球类实现完了
实现发火球的功能
去Player类里写
lol实现技能:键盘上按一个选择技能,鼠标左键点击方向确定施法
首先在构造函数里加一个this.cur_skill = null
来表示目前选择的技能是什么
然后写一个事件监听,接受键盘键入和鼠标左键键入
add_listening_events() {
let outer = this;
// 如果右键,不处理菜单事件(鼠标右键之后,不会显示右键菜单)
this.playground.game_map.$canvas.on("contextmenu",function(){
return false;
});
this.playground.game_map.$canvas.mousedown(function(e) {
if(e.which === 3) {
outer.move_to(e.clientX,e.clientY); // 这里要用outer才能调用到move_to方法,直接用this代表的是这个函数本身
} else if(e.which === 1) { // 如果点击了鼠标左键
if(outer.cur_skill === "fireball") {
outer.shoot_fireball(e.clientX,e.clientY);
}
outer.cur_skill = null; // 释放完该技能,取消选中该技能
}
});
// 使用window来获取键盘事件,(上面的鼠标点击事件是用canvas来获取,这里不可以使用canvas来获取,canvas不能聚焦
$(window).keydown(function (e) {
if(e.which === 81) { // 如果键盘获取的keycode是81(代表的是q),就使用火球技能
outer.cur_skill = "fireball";
return false;
}
});
}
shoot_fireball(tx,ty) {
console.log("shoot fireball", tx, ty);
}
现在先按q,鼠标左键点击任意一个位置,就会输出这个位置的坐标
火球移动的具体逻辑
修改Player类里的shoot_fireball函数
shoot_fireball(tx,ty) {
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 color = "orange";
let speed = this.playground.height * 0.5;
let move_dist = this.playground.height * 1;
new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_dist);
}
实现单机版(添加人机)
单机要加入其他敌人,进入 AcGamePlayground 类
constructor(root){
this.root = root;
this.$playground = $(`<div class="ac-game-playground"></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.15, true));
// 把上面的抄下来,添加几个敌人,后面的is_me参数要传false
for(let i = 0; i < 5; i ++ ) {
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, "blue", this.height * 0.15, false));
}
this.start();
}
现在的敌人都是蓝色的,且不能动。我们需要写简单的ai,让他们动起来。
最简单的方法是让他们随机游走,每次生成一个随机的目的地。
进入 Player 类,修改start和update函数
在start()和update()让人机每次获得一个随机目的地就可以。
start() {
if(this.is_me) {
// 如果这个球是玩家自己,玩家就可以控制鼠标移动此球
this.add_listening_events();
} else { // 如果这个玩家是人机,就让他走到一个随机目的地
let tx = Math.random() * this.playground.width; // js的Math.random()返回的是一个0~1的数
let ty = Math.random() * this.playground.height;
this.move_to(tx,ty);
}
}
update() {
if (this.move_length < this.eps) {
this.move_length = 0;
this.vx = this.vy = 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();
}
之后可以加上攻击的伤害,人机攻击等功能。
增加火球伤害与击退效果
碰撞:两小球的中心点的距离小于两小球的半径之和就视为发生了碰撞
判断火球是否打击到敌人,需要在每一帧都要进行碰撞检测。枚举每个player,如果不是自己,就判断是否发生碰撞,若发生碰撞,就attack一下这个player(减小被攻击玩家生命值,造成击退效果等)
判断火球是否击中,就写在FireBall类里,作用在player的行为就写在Player类里,把is_attacked写在Player类里。
需要给is_attacked传入击退方向、damage值。对于击退方向,就可以传一个角度。
FireBall类:
class FireBall extends AcGameObject {
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
super();
this.playground = playground;
this.player = player;
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.eps = 0.1; // 距离小于0.1表示为0
}
start() {
}
update() {
if(this.move_length < this.eps) { // 如果移动距离为0,就不再移动,调用基类的destory函数销毁火球
this.destroy();
return false;
}
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); // thimedelta是一帧的时间单位毫秒,要模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 && is_collision(player)) {
this.attack(player);
}
}
this.render();
}
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;
return false;
}
attack(palyer) { // 击中玩家
let angle = Math.atan2(player.y - this.y, player.x - this.x);
player.is_attacked(angle,this.damage); // 调用被击中的函数实现击中效果
this.destroy(); // 火球消失
}
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();
}
}
让玩家的血量就是半径。
火球加一个伤害参数damage。创建火球的时候,伤害是一个具体的值,可以定为this.playground.height * 0.01
,玩家半径是this.playground.height * 0.05
,每击中一次,让玩家半径减小这个值,即火球的伤害为血量的20%,高度变,玩家大小、火球伤害都会发生改变,比较合理。
火球击中了玩家,玩家会被击退,击退期间玩家应该是失去控制的。朝着一个方向推一段距离。这个时候玩家自身的vx和vy都应该置为0。
玩家被击退刚开始击退速度应该比较快后来应该是越来越慢变成0,这样子更加丝滑,需要添加一个类似于摩擦力的参数去实现。
构造函数中加一些新的基础属性damage_x
、damage_y
、damage_speed
、friction
(击退摩擦力)
Player类:
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.move_length = 0;
this.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.1;
this.cur_skill = null;
// 新添的: 伤害方向、击退速度、击退摩擦力
this.damage_x = 0;
this.damage_y = 0;
this.damage_speed = 0;
this.friction = 0.9;
}
start() {
...
}
add_listening_events() {
...
}
shoot_fireball(tx,ty) {
...
}
get_dist(x1, y1, x2, y2) {
...
}
move_to(tx,ty) {
...
}
is_attacked(angle,damage) { // 玩家被击中,作用于玩家身上的效果
this.radius -= damage;
if(this.radius < 10){
this.destroy();
return false;
}
this.damage_x = Math.cos(angle);
this.damage_y = Math.sin(angle);
this.damage_speed = damage * 80; // 可以调一下
this.speed *= 0.8; // 被击中之后速度变慢,小球速度变慢也合理
}
update() {
if (this.damage_speed > 10){ // 如果受到伤害
this.vx = this.vy = 0;
this.move_length = 0;
// 朝着damage的方向移动
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.move_length < this.eps) {
this.move_length = 0;
this.vx = this.vy = 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() {
...
}
}
也可以加击中之后,冰球,绳索等等其他各种各样的效果
增加火球粒子效果
火球打中之后会有粒子爆炸效果
在playground下新建paritcle
文件夹
写Particle粒子类
需要传入地图(因为要画出来),粒子生成位置x、y,粒子半径,粒子颜色,粒子速度,粒子最大运动长度。
Particle粒子类:
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 = 1;
}
start() {
}
update() {
if(this.move_length < this.eps || this.speed < this.eps) { // 速度小于eps,粒子消失
this.destroy();
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();
}
}
在Player类中对于is_attacked函数补充:new粒子,并传入一些随机的参数
// 粒子爆炸烟花特效
for(let i = 0; i < 20 + Math.random() * 10; i ++) {
let x = this.x, y = this.y;
let radius = this.radius * Math.random() * 0.1;
let angle = Math.PI * 2 * Math.random();
let vx = Math.cos(angle), vy = Math.sin(angle);
let color = this.color;
let speed = this.speed * 10;
let move_length = this.radius * Math.random() * 5;
new Particle(this.playground, x, y, radius, vx, vy, color, speed, move_length);
}
人机随机颜色
最好是自己选定几个颜色,然后随机。纯随机数选出颜色可能并不好看。
打开AcGamePlayground基类
添加函数,然后在生成人机传入颜色参数时直接this.get_random_color()即可
get_random_color() {
let colors = ["blue", "red", "pink", "grey", "green"];
return colors[Math.floor(Math.random() * 5)];
}
调整火球粒子出现顺序
击中后先出现粒子效果,然后再去扣血(减小被击中玩家半径),否则当给玩家最后一击的时候,radius < 10
,玩家直接被销毁,return false
这样就不会再出现粒子效果,所以粒子效果代码放在扣血前面。
is_attacked(angle,damage) { // 玩家被击中,作用于玩家身上的效果
// 粒子爆炸烟花特效 , 把粒子特效放在最前面要!
for(let i = 0; i < 20 + Math.random() * 10; i ++) {
let x = this.x, y = this.y;
let radius = this.radius * Math.random() * 0.1;
let angle = Math.PI * 2 * Math.random();
let vx = Math.cos(angle), vy = Math.sin(angle);
let color = this.color;
let speed = this.speed * 10;
let move_length = this.radius * Math.random() * 5;
new Particle(this.playground, x, y, radius, vx, vy, color, speed, move_length);
}
this.radius -= damage;
if(this.radius < 10){
this.destroy();
return false;
}
this.damage_x = Math.cos(angle);
this.damage_y = Math.sin(angle);
this.damage_speed = damage * 80; // 可以调一下
this.speed *= 0.8; // 被击中以后,速度变慢,小球速度慢也比较合理
}
人机向随机一个其他敌人发射火球
2:11:30
让人机随机向一个人发射火球
if (!this.is_me){
let player = this.playground.players[Math.floor(Math.random() *this.playground.players.length)];
this.shoot_fireball(player.x,player.y);
}
目前是有可能向自己发射的(虽然没有伤害),可以加一个特判,如果是自己就重新发射。
但是这样游戏刚开始,玩家还没有反应过来人机就已经开火了,并且会一直开火。所以加一个游戏开始冷静期,并给人机开火加一个时间限制。
让人机游戏开始4s后,大约每4s向随机一个人发射一个火球。
构造函数中加一个this.spent_time = 0;
update() {
this.spent_time += this.timedelta / 1000;
if( !this.is_me && this.spent_time > 4 && Math.random() < 1 / 240.0){
let player = this.playground.players[Math.floor(Math.random() *this.playground.players.length)];
this.shoot_fireball(player.x,player.y);
}
}
此外可以加一个0.3s的预判,让人机往敌人移动方向的未来0.3s位置发射火球
let player = this.playground.players[Math.floor(Math.random() * this.playground.players.length)];
let tx = player.x + player.speed * player.vx * this.timedelta / 1000 * 0.3; // 预判0.3s后的位置
let ty = player.y + player.speed * player.vy * this.timedelta / 1000 * 0.3;
this.shoot_fireball(tx,ty);
Player中添加on_destroy
如果某个玩家死亡就将其从players中删除掉
on_destroy() { // 玩家死后调用这个函数,将玩家删除掉
for (let i = 0; i < this.playground.players.length; i ++) {
if (this.playground.players[i] === this) {
this.playground.players.splice(i,1);
}
}
}
还有一些小bug
比如玩家死后还能发火球,人机发射火球也会朝着自己移动方向发射火球等等