让 AI 在浏览器端打游戏,其中 AI 打游戏,分解一下就是 AI 和游戏,所以分享主要两个比较完整部分组成game和神经网络。我们先用 js 在浏览器 atari 游戏,估计就是连 80后可能也是只听说这个游戏机,也没有看过真机,我的确见过真机,那是游戏厅还是以租个房子,atari 连接一个彩电,然后老板在傍边一局一局地收钱。
对您的一点要求
- 熟悉 web 开发
- 了解 html5 新特性,特别是需要熟悉 canvas 相关 API,因为这里会频繁使用 canvas 的 API
- 熟悉神经网络 以上是对您一点点要求,也是能够完全理解这篇文章的前提
js 实现 game
起步搭建环境
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas id=gameCanvas" width="700" height="500"></canvas>
<script>
</script>
</body>
</html>
- 在项目下,所谓项目就是创建文件夹而已,然后创建 index.html, 项目就算搭建完成,所以 js 开发这么受欢迎,就是因为入门时简单、直观。不像其他语言,仅搭建环境就让您筋疲力竭。
- 创建一个 canvas 元素,一手终结了 flash 的 canvas,现在 html 上所有绘制工作都是在这个画布上完成,所以今天绘制 game 的画面任务也交给他,创建好 canvas 为其设置其 width 和 height
在 script 标签内写 javascript 代码
/** @type {HTMLCanvasElement} */
var canv = document.getElementById("gameCanvas");
var ctx = canv.getContext("2d");
- 通过 id 获取 canvas ,然后获取 canvas 的 2d 上下文,也就是 canvas 提供用于在 canvas 绘制的接口。
- javascript 一些新特性也解决 jQuery 的设置,想一想最近开始做 web 前端开发都有可能不知道当年那个精彩绝伦的 jQuery
const FPS = 30; //设置每秒的帧数
/** @type {HTMLCanvasElement} */
var canv = document.getElementById("gameCanvas");
var ctx = canv.getContext("2d");
// 设置
setInterval(update, 1000 / FPS);
function update(){
//绘制背景
//绘制 ship
//旋转 ship
//移动 ship
}
更新画面
- 无论动画还是游戏也好都是由一序列图片在一定时间间隔持续呈现在我们面前,也是利用我们人眼的功能来实现,让我们感觉画面时连续
- 这里 setInterval 方法是可以在指定延迟时间重复执行代码片段的方法,恰恰满足了我们要求,所以更新画面工作都会放到
update函数内,然后由 setInterval 在每间隔一段时间来执行这个函数
绘制背景
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canv.width, canv.height);
绘制 ship
ship 形状非常简单,就是用一个三角形来来表示 ship, ship 状态包括其中心点位置 x 和 y,以及 ship 大小和 ship 的角度,所以 ship 的状态就是在 t 时刻位置和旋转角度所决定的。
var ship = {
x: canv.width / 2,
y: canv.height / 2,
r: SHIP_SIZE / 2,
a: 90 / 180 * Math.PI //
}
下面代码是将 ship 绘制在 canvas 上,其背后的原理,也就是几何方面 ,所以添加 4/3 和 2/3 系数就是让 ship 中 x 和 y 位于 ship 中调整补偿
//绘制 ship 角色
ctx.strokeStyle = "white";
ctx.lineWidth = SHIP_SIZE / 20;
ctx.beginPath();
// canvas 的原点位于左上角
// 坐标角度应该是以 x 轴右半轴作为起始轴逆时针旋转得到角度 a
ctx.moveTo(
ship.x + 4/3 * ship.r * Math.cos(ship.a),
ship.y - 4/3 * ship.r * Math.sin(ship.a)
);
ctx.lineTo(
ship.x - ship.r * (2/3 * Math.cos(ship.a) + Math.sin(ship.a)),
ship.y + ship.r * (2/3 * Math.sin(ship.a) - Math.cos(ship.a))
);
ctx.lineTo(
ship.x - ship.r * (2/3 * Math.cos(ship.a) - Math.sin(ship.a)),
ship.y + ship.r * (2/3 * Math.sin(ship.a) + Math.cos(ship.a))
);
ctx.closePath();
ctx.stroke();
ship 绘制就是由 3 条首尾相连的线所组成,这里使用 canvas 绘制线的 API 简单说一下,想象有一只笔 beginPath 就是拿笔告诉 canvas 我要在上面绘制线条了 moveTo 我们将笔触放置 moveTo 指定位置,然后就可以一步一步 lineTo 移动笔触来绘制直线了最后 closePah 将首尾点闭合以下,最后还的执行以下 stroke 来按照指定路线绘制线条。
事件处理
// 设置事件处理器
document.addEventListener("keydown",keyDown);
document.addEventListener("keyup",keyUp);
function keyDown(/** @type {keyboardEvent} */ ev){
console.log("keydown");
}
function keyUp(/** @type {keyboardEvent} */ ev){
console.log("keyup");
}
让 ship 转起来
var ship = {
x: canv.width / 2,
y: canv.height / 2,
r: SHIP_SIZE / 2,
a: 90 / 180 * Math.PI, //
rot: 0
}
首先给 ship 增加一个属性,也就是 ship 旋转角度,默认为 0,当通过键盘操控 ship 按下 left arrow 按键时在当前 ship 朝向旋转一定角度
const TRUN_SPEED = 360; 这是每次按下 left 或者 right ship 旋转的角度的大小。
有关动作这里策略模式替换 switch 功能,其实可能把问题搞复杂了,不如直接用 switch 了,有关 code 如何实现的策略模式这里不做过多解释,估计大家一看就懂。
class ShipAction {
constructor(actionId, handler){
this._actionId = actionId;
this._handler = handler
}
doAction(){
this._handler();
}
}
class ShipActionManager{
constructor(){
this._actions =[];
}
addAction(action){
this._actions = [...this._actions,action]
}
getAction(actionId){
return this._actions.find(action => action._actionId == actionId);
}
}
const shipKeyDownActionManager = new ShipActionManager();
const shipKeyUpActionManager = new ShipActionManager();
const leftAction = new ShipAction(KEYCODE.LEFT,()=>{
ship.rot = TRUN_SPEED / 180 * Math.PI /180;
});
const rightAction = new ShipAction(KEYCODE.RIGHT,()=>ship.rot = -TRUN_SPEED / 180 * Math.PI /180);
const stopLeftAction = new ShipAction(KEYCODE.LEFT,()=>ship.rot = 0);
const stopRightAction = new ShipAction(KEYCODE.RIGHT,()=>ship.rot = 0);
shipKeyDownActionManager.addAction(leftAction);
shipKeyDownActionManager.addAction(rightAction);
shipKeyUpActionManager.addAction(stopLeftAction);
shipKeyUpActionManager.addAction(stopRightAction);
function keyDown(/** @type {keyboardEvent} */ ev){
shipAction = shipKeyDownActionManager.getAction(ev.keyCode);
if (shipAction != undefined){
shipAction.doAction()
}
}
function keyUp(/** @type {keyboardEvent} */ ev){
shipAction = shipKeyUpActionManager.getAction(ev.keyCode);
if (shipAction != undefined){
shipAction.doAction()
}
}
到此为止,我们就把控制 ship 旋转这件事搞定了。
为 ship 添加推进器
const SHIP_THRUST = 5;
const FRICTION = 0.7;
添加了两个常量,分别是推进器(thrust),推进器的动力大小,FRICTION 摩擦系数,好处就是当松开按键时,让 ship 在阻力下慢慢停下。
在 update 函数添加
//thrust the ship
if (ship.thrusting){
ship.thrust.x += SHIP_THRUST * Math.cos(ship.a) / FPS;
ship.thrust.y -= SHIP_THRUST * Math.sin(ship.a) / FPS;
}else{
ship.thrust.x -= FRICTION * ship.thrust.x / FPS;
ship.thrust.y -= FRICTION * ship.thrust.y / FPS;
}
当按下 up 按启动 ship 推进器开始加速,当释放 up 按键可以因为阻力 FRICTION 存在,
ship.x += ship.thrust.x;
ship.y += ship.thrust.y;
接下来整理一下代码,将绘制 ship 代码整理到一个方法 drawShip 中,然后我们来回推进器加速的效果,就是用小三角形来表示 ship 推进时后面喷出的火焰效果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas id="gameCanvas" width="700" height="500"></canvas>
<script>
const FPS = 30; //设置每秒的帧数
const FRICTION = 0.7;
const SHIP_SIZE = 30;
const TRUN_SPEED = 360;
const SHIP_THRUST = 5;
const KEYCODE = {
LEFT: 37,
UP:38,
RIGHT: 39,
DOWN:40
}
/** @type {HTMLCanvasElement} */
var canv = document.getElementById("gameCanvas");
var ctx = canv.getContext("2d");
// 设置刷新游戏画面的函数,也就是以每秒在画布上绘制游戏画面
setInterval(update, 1000 / FPS);
// 定义 ship 角色
var ship = {
x: canv.width / 2,
y: canv.height / 2,
r: SHIP_SIZE / 2,
a: 90 / 180 * Math.PI, //
rot: 0,
thrusting: false,
thrust:{
x: 0,
y: 0
}
}
function drawThrustingEffect(ctx,color,size,scale){
ctx.fillStyle = color
ctx.strokeStyle = "yellow";
ctx.lineWidth = size;
ctx.beginPath();
ctx.moveTo(
ship.x - ship.r * (2/3 * Math.cos(ship.a) + scale * Math.sin(ship.a)),
ship.y + ship.r * (2/3 * Math.sin(ship.a) - scale * Math.cos(ship.a))
);
ctx.lineTo(
ship.x - 4/3 * ship.r * Math.cos(ship.a),
ship.y + 4/3 * ship.r * Math.sin(ship.a)
);
ctx.lineTo(
ship.x - ship.r * (2/3 * Math.cos(ship.a) - scale *Math.sin(ship.a)),
ship.y + ship.r * (2/3 * Math.sin(ship.a) + scale * Math.cos(ship.a))
);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// 绘制 ship
function drawShip(ctx){
ctx.strokeStyle = "white";
ctx.lineWidth = SHIP_SIZE / 20;
ctx.beginPath();
// canvas 的原点位于左上角
// 坐标角度应该是以 x 轴右半轴作为起始轴逆时针旋转得到角度 a
ctx.moveTo(
ship.x + 4/3 * ship.r * Math.cos(ship.a),
ship.y - 4/3 * ship.r * Math.sin(ship.a)
);
// ship.x - ship.r * Math.cos(ship.a) - ship.r * Math.sin(ship.a)
//原的内接三角形,120 度 (a + 120)/180 * Math.PI
// ship.x + ship.r * Math.cos(ship.a)
ctx.lineTo(
ship.x - ship.r * (2/3 * Math.cos(ship.a) + Math.sin(ship.a)),
ship.y + ship.r * (2/3 * Math.sin(ship.a) - Math.cos(ship.a))
);
ctx.lineTo(
ship.x - ship.r * (2/3 * Math.cos(ship.a) - Math.sin(ship.a)),
ship.y + ship.r * (2/3 * Math.sin(ship.a) + Math.cos(ship.a))
);
ctx.closePath();
ctx.stroke();
}
function update(){
//绘制背景
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canv.width, canv.height);
//thrust the ship
if (ship.thrusting){
ship.thrust.x += SHIP_THRUST * Math.cos(ship.a) / FPS;
ship.thrust.y -= SHIP_THRUST * Math.sin(ship.a) / FPS;
drawThrustingEffect(ctx,"red",SHIP_SIZE/10,0.5);
}else{
ship.thrust.x -= FRICTION * ship.thrust.x / FPS;
ship.thrust.y -= FRICTION * ship.thrust.y / FPS;
}
//出屏控制
if (ship.x < 0 - ship.r){
ship.x = canv.width + ship.r;
}else if(ship.x > canv.width + ship.r){
ship.x = 0 - ship.r;
}
if (ship.y < 0 - ship.r){
ship.y = canv.height + ship.r;
}else if(self.y > canv.height + ship.r){
ship.y = 0 - ship.r;
}
//绘制 ship 角色
drawShip(ctx);
//旋转 ship 角色
ship.a += ship.rot
//移动 ship 角色
ship.x += ship.thrust.x;
ship.y += ship.thrust.y;
}
// 设置事件处理器
document.addEventListener("keydown",keyDown);
document.addEventListener("keyup",keyUp);
class ShipAction {
constructor(actionId, handler){
this._actionId = actionId;
this._handler = handler
}
doAction(){
this._handler();
}
}
class ShipActionManager{
constructor(){
this._actions =[];
}
addAction(action){
this._actions = [...this._actions,action]
}
getAction(actionId){
return this._actions.find(action => action._actionId == actionId);
}
}
const shipKeyDownActionManager = new ShipActionManager();
const shipKeyUpActionManager = new ShipActionManager();
const leftAction = new ShipAction(KEYCODE.LEFT,()=>{
ship.rot = TRUN_SPEED / 180 * Math.PI /180;
});
const rightAction = new ShipAction(KEYCODE.RIGHT,()=>ship.rot = -TRUN_SPEED / 180 * Math.PI /180);
const startThrustingAction = new ShipAction(KEYCODE.UP, ()=> ship.thrusting = true);
const stopLeftAction = new ShipAction(KEYCODE.LEFT,()=>ship.rot = 0);
const stopRightAction = new ShipAction(KEYCODE.RIGHT,()=>ship.rot = 0);
const stopThrustingAction = new ShipAction(KEYCODE.UP, ()=> ship.thrusting = false);
shipKeyDownActionManager.addAction(leftAction);
shipKeyDownActionManager.addAction(rightAction);
shipKeyDownActionManager.addAction(startThrustingAction);
shipKeyUpActionManager.addAction(stopLeftAction);
shipKeyUpActionManager.addAction(stopRightAction);
shipKeyUpActionManager.addAction(stopThrustingAction);
function keyDown(/** @type {keyboardEvent} */ ev){
shipAction = shipKeyDownActionManager.getAction(ev.keyCode);
if (shipAction != undefined){
shipAction.doAction()
}
}
function keyUp(/** @type {keyboardEvent} */ ev){
shipAction = shipKeyUpActionManager.getAction(ev.keyCode);
if (shipAction != undefined){
shipAction.doAction()
}
}
</script>
</body>
</html>