4 创建游戏界面
方便专注游戏界面,先注释以下代码
game/static/js/src/zbase.js
里注释菜单界面// this.menu = new AcGameMenu(this);
game/static/js/src/playground/zbase.js
里注释隐藏界面// this.hide();
2.0版本 games/static/js/src/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.start();
}
start(){
}
show() { // 打开playground界面
this.$playground.show();
}
hide() { // 关闭playground界面
this.$playground.hide();
}
}
games/static/css/game.js
// 添加样式
.ac-game-playground {
width: 100%;
height: 100%;
user-select: none; // 页面不能被选中
}
基类AcGameObject
作用:让每一个物体画好每一帧;简易的游戏引擎概念
cd game/static/js/src/playground
mkdir ac_game_object
cd ac_game_object
vim zbase.js
game/static/js/src/playground/ac_game_object/zbase.js
let AC_GAME_OBJECTS = []; // 存储全局数组(每秒钟调用数组对象60次)
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); // 删除函数splice
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]; // obj存储当前物体
if(!obj.has_called_start) { // 如果物体没有执行第一帧
obj.start(); // 调用start()
obj.has_called_start = true; // 标记已执行第一帧start()
}
else {
obj.timedelta = timestamp - last_timestamp; // 两帧时间间隔
obj.update(); // 不断调用
}
}
last_timestamp = timestamp; // 每一帧执行完,更新上一帧的时间戳
requestAnimationFrame(AC_GAME_ANIMATION);
}
requestAnimationFrame(AC_GAME_ANIMATION); // 一秒钟调用60次
游戏地图
cd game/static/js/src/playground
mkdir game_map
cd game_map
vim zbase.js
game/static/js/src/playground/game_map/zbase.js
class GameMap extends AcGameObject {
constructor(playground) {
super();
this.playground = playground;
this.$canvas = $(`<canvas></canvas>`); // canvas为画布
this.ctx = this.$canvas[0].getContext('2d'); // ctx操作画布
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)"; // 填充颜色,透明度20%
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); // 矩阵
}
}
games/static/js/src/playground/zbase.js
添加生成game_map
:this.game_map = new GameMap(this);
玩家类
cd game/static/js/src/playground
mkdir player
cd player
vim zbase.js
玩家生成、移动测试
game/static/js/src/playground/player/zbase.js
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
// playground场景 x,y 球的中心坐标;radius半径,spped速度(高度百分比), is_me是否是自己
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx; // 画笔ctx
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.1; // 精度,认为小于eps算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();
}
}
games/static/js/src/playground/zbase.js
添加生成player
:生成白球
this.players = [];
this.player.push(new Player(this, this,width / 2, this.height / 2, this.height * 0.05, "white", this.height * 0.15, true));
测试玩家代码 game/static/js/src/playground/player/zbase.js
constructor(...) {
...;
this.vx = 1;
this.vy = 1;
}
update() {
this.x += this.vx;
this.y += this.vy;
...
}
进行调试,发现这个球会移动,而且移动的时候尾部会出现模糊,这是因为GameMap画上的颜色是透明的黑色蒙版,会带来这种视觉效果。
加入监听
鼠标操作
game/static/js/src/playground/player/zbase.js
constructor(...) {
...;
this.vx = 0;
this.vy = 0;
}
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); // e.clientX,Y分别对应鼠标的X,Y坐标
}
});
}
move_to(tx, ty) {
console.log("move to", tx, ty); // 测试移动输出
}
update() {
this.x += this.vx;
this.y += this.vy;
this.render();
}
测试在画布上随便用右键点击,会发现console(网页控制台)里面会输出鼠标的位置。
上面代码可以得到物体中心(x, y)和目标位置(tx, ty);实现鼠标使物体移动。
$$
\theta = arctan \frac{y_2-y_1}{x_2-x_1} = arctan \frac{ty-y}{tx-x}\\
vx = cos\theta \\
vy = sin\theta
$$
constructor(...) {
...;
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) {
// console.log("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.cx = this.vy = 0;
}
else {
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); // moved真实移动距离,timedelta单位为ms,转为s
this.x += this.vx * moved;
this.y += this.vy * moved;
this.move_length -= moved;
}
this.render();
}
技能类
cd game/static/js/src/playground
mkdir skill
火球
cd skill
mkdir fireball
cd fireball
vim zbase.js
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) {
super();
this.playground = playground;
this.player = player;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.radius = radius;
this.color = color;
this.speed = speed;
this.move_length = move_length; // 火球射程
this.eps = 0.1;
}
start() {
}
update() {
if(this.move_length < this.eps) {
this.destroy();
return false;
}
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();
}
}
实现发射火球
game/static/js/src/playground/player/zbase.js
constructor(playground, x, y, radius, color, speed, is_me) {
...
this.cur_skill = null; // 当前选择的技能
}
add_listening_events() {
this.playground.game_map.$canvas.mousedown(function(e) { // 鼠标监听
...
else if (e.which === 1) {
if (outer.cur_skill === "fireball") {
outer.shoot_fireball(e.clientX, e.clientY);
}
outer.cur_skill = null; // 点击后清空
}
});
$(window).keydown(function(e) {
if(e.which === 81 ) { //q 可查询keycode
outer.cur_skill = "fireball";
return false;
}
});
}
shoot_fireball(tx, ty) {
// console.log("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_length = this.playground.height * 1;
new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length);
}
添加敌人
games/static/js/src/playground/zbase.js
constructor(root) {
...
for(let i = 0; i < 5; i++) {
this.player.push(new Player(this, this,width / 2, this.height / 2, this.height * 0.05, "blue", this.height * 0.15, false));
}
this.start();
}
敌人随机游走
game/static/js/src/playground/player/zbase.js
start() {
...
else { // 如果是敌人
let tx = Math.random() * this.playground.width; // 随机方向
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
}
update() {
if(this.move_length < this.eps){
...
if(!this.is_me) { // 敌人不停;生产队的驴
let tx = Math.random() * this.playground.width; // 随机方向
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
}
}
碰撞检测
圆的碰撞:两圆中心距离 < 两半径之和
game/static/js/src/playground/skill/fireball/zbase.js
constructor(..., damage){ // 加碰撞伤害
...
this.damage = damage;
this.eps = 0.1;
}
update() {
...
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.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(player) {
let angle = Math.atan2(player.y - this.y, player.x - this.x);
palyer.is_attacked(angle, this.damage;); // 被击中
this.destroy(); // 火球消失
}
game/static/js/src/playground/player/zbase.js
constructor(...) {
this.damage_x = 0;
this.damage_y = 0;
this.damage_speed = 0;
this.friction = 0.9 // 摩檫力
}
shoot_fireball(tx, ty) { // 在player里生成火球时也要添加伤害
...
let damage = this.playground.height * 0.01;
new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, damage);
}
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 * 100;
this.speed *= 0.8 // 被攻击后速度减少
}
update() {
if(this.damage_speed > 10){
this.move_length = 0;
this.vx = this.vy = 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.move_length < this.eps)
...
...
}
}
敌人赋予不同颜色
3.0 版本 games/static/js/src/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.player.push(new Player(this, this,width / 2, this.height / 2, this.height * 0.05, "white", this.height * 0.15, true));
for(let i = 0; i < 5; i++) {
this.player.push(new Player(this, this,width / 2, this.height / 2, this.height * 0.05, this.get_random_color(), this.height * 0.15, false));
}
this.start();
}
/*
get_random_color() {
let colors = ["blue", "red", "pink", "green", "grey"];
return colors[Math.floor(Math.random() * colors.length)];
}*/
get_random_color() { // 随机一个非白颜色
var color = "#";
while(color.length < 7) {
color += Math.floor(Math.random() * 16).tostring(16);
}
if(color == "#ffffff") return get_random_color();
return color;
}
start(){
}
show() { // 打开playground界面
this.$playground.show();
}
hide() { // 关闭playground界面
this.$playground.hide();
}
}
敌人实现发射火球
game/static/js/src/playground/player/zbase.js
constructor(...) {
...
this.spent_time = 0;
}
update() {
this.spent_time += this.timedelta / 1000;
if(!this.is_me && this.spent_time > 4 && Math.random() < 1 / 300.0) { // 4秒无敌时间,敌人每5秒攻击一次
let player = this.playerground.players[Math.floor(Math.random() * this.playground.players.length)]; // 随机攻击
let tx = player.x + player.speed * this.vx * this.timedelta / 1000 * 0.3;
let ty = player.y + player.speed * this.vy * this.timedelta / 1000 * 0.3;
this.shoot_fireball(tx, ty); // 攻击0.3秒后位置,可改进预测轨迹 敌人朝自己本身发要特判
}
if(this.damage_speed > 10){
...
}
}
on_destroy() {
for(let i = 0; i < this.playground.players.length; i++) {
if(this.playground.players[i] == this) {
this.playground.players.splice(i, 1);
}
}
}
Debug
白球死后还能发火球攻击敌人:如果玩家死了,使q失效
game/static/js/src/playground/player/zbase.js
$(window).keydown(function(e) {
if (outer.radius < 10) return false; // 这里监听下如果玩家死了,按q键就没有用了
if (e.which === 81) { // q
outer.cur_skill = "fireball";
return false;
}
});
粒子效果
cd game/static/js/src/playground
mkdir particle
cd particle
vim zbase.js
game/static/js/src/playground/particle/zbase.js
class Particle extends AcGameObject {
constrcutor(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.vx = vx;
this.vy = vy;
this.radius = radius;
this.color = color;
this.speed = speed;
this.move_length = move_length;
this.eps = 3;
this.friction = 0.9;
}
start() {
}
update() {
if(this.move_length < this.eps || this.speed < this.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.speed *= this.friction;
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();
}
}
game/static/js/src/playground/player/zbase.js
is_attacked(angle, damage) {
for(let i = 0; i < 20 + Math.random() * 5; 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);
}
...
}
// 把粒子效果放在前面,只要击中就要有效果
注意大小写
学麻了,溜了,学算法去了