学习平台 AcWing SpringBoot框架课
day01:Java语法上 基本概念 JDK、JRE、JVM的关系
JDK:Java Development Kit,java开发工具包
JRE:Java Runtime Enviroment,Java运行环境
JVM:Java Virtual Machine,Java虚拟机
JDK包含JRE,JRE包含JVM
JDK版本选择
目前JDK1.8(也叫JDK8,不是JDK18)用的最多
Java代码的编译运行流程
将Java源码编译成Java字节码。
使用JVM将Java字节码转化成机器码。
JVM作用:跨平台、内存管理、安全。
JSE、JEE、JME的区别
JSE: Java Standard Edition,标准版
JEE:Java Enterprise Edition,企业版
JME: Java Mirco Edition,移动版
Spring是JEE的轻量级替代品
SpringBoot是Spring + 自动化配置
Java 语法 变量、运算符、输入与输出 类似于c#
,Java
的所有变量和函数都需要定义在class
中。
内置数据类型
类型
字节数
举例
byte
1
123
short
2
12345
int
4
123456789
long
8
123456789L
float
4
1.2F
double
8
1.2, 1.2D
boolean
1
true, false
char
2
‘A’
常量 使用 final
修饰
类型转换
显示转化:int x = (int)'A'
隐式转化:double x = 12, y = 4 * 3.3;
,只适用于低精度类型向高精度类型转换。
表达式 与 C++、Python3类似
1 2 3 int a = 1 , b = 2 , c = 3 ;int x = (a + b) * c;x ++;
输入
1 2 3 4 5 6 Scanner cin = new Scanner (System.in);String str = cin.next();int x = cin.nextInt();float y = cin.nextFloat();double z = cin.nextDouble();String line = cin.nextLine();
方式二:效率较高,输入规模较大时使用。注意需要抛异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.xzt;import java.io.BufferedReader;import java.io.InputStreamReader;public class Main { public static void main (String[] args) throws Exception { BufferedReader br = new BufferedReader (new InputStreamReader (System.in)); String str = br.readLine(); System.out.println(str); } }
输出
1 2 3 4 5 System.out.println(123 ); System.out.println("Hello World" ); System.out.print(123456 ); System.out.print("xzt\n" ); System.out.printf("%04d %.2f\n" , 4 , 123.456D );
方式二:输出效率较高,输出规模较大时使用,需要抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.xzt;import java.io.BufferedWriter;import java.io.OutputStreamWriter;public class Main { public static void main (String[] args) throws Exception { BufferedWriter bw = new BufferedWriter (new OutputStreamWriter (System.out)); bw.write("hello World\n" ); bw.flush(); } }
判断语句 if-else
语句与 C++
、Python
中类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.xzt;import java.io.*;import java.util.Scanner;public class Main { public static void main (String[] args) throws Exception { Scanner sc = new Scanner (System.in); int year = sc.nextInt(); if (year % 100 == 0 ) { if (year % 400 == 0 ) System.out.printf("%d是闰年\n" , year); else System.out.printf("%d不是闰年\n" , year); } else { if (year % 4 == 0 ) System.out.printf("%d是闰年\n" , year); else System.out.printf("%d不是闰年\n" , year); } } }
switch
语句与 C++
类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.xzt;import java.io.*;import java.util.Scanner;public class Main { public static void main (String[] args) { Scanner sc = new Scanner (System.in); int day = sc.nextInt(); String name; switch (day) { case 1 : name = "Monday" ; break ; case 2 : name = "Tuesday" ; break ; case 3 : name = "Wednesday" ; break ; case 4 : name = "Thursday" ; break ; case 5 : name = "Friday" ; break ; case 6 : name = "Saturday" ; break ; case 7 : name = "Sunday" ; break ; default : name = "not valid" ; } System.out.println(name); } }
逻辑运算符与条件表达式 与 C++
、Python
类似
注意 :在Java
中,判断语句和循环语句的判断条件只能是boolean
类型,不能是整型,和C++
不一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.xzt;import java.io.*;import java.util.Scanner;public class Main { public static void main (String[] args) { Scanner sc = new Scanner (System.in); int year = sc.nextInt(); if (year % 100 != 0 && year % 4 == 0 || year % 400 == 0 ) System.out.printf("%d是闰年\n" , year); else System.out.printf("%d不是闰年\n" , year); } }
循环语句 与 C++
、Python
类似
while
循环1 2 3 4 5 int i = 0 ;while (i < 5 ){ System.out.println(i); i++; }
do...while
循环1 2 3 4 5 int i = 0 ;do { System.out.println(i); i++; }while (i < 5 );
for
循环1 2 3 for (int i = 0 ; i < 5 ; i ++){ System.out.println(i); }
数组 Java
中的数组和 C++
中的数组类似
初始化 和C++
类似,初始化定长数组,长度可以是变量,可以在初始化时赋值。
1 2 3 4 5 int [] a = new int [5 ]; int n = 10 ;float [] b = new float [n]; char [] c = {'a' , 'b' , 'c' }; char [] d = c;
数组元素的读取与写入 和C++
类似。
1 2 3 4 5 6 7 8 int []a = new int [5 ];for (int i = 0 ; i < 5 ; i++){ a[i] = i; } for (int i = 0 ; i < 5 ; i++){ System.out.println(a[i] * a[i]); }
多维数组 和 C++
类似
1 2 3 4 5 6 7 8 9 int [][]a = new int [2 ][3 ];a[1 ][2 ] = 1 ; int [][] = { {1 , 2 , 3 }, {4 , 5 , 6 }, }; System.out.println(a[1 ][2 ]); System.out.println(b[1 ][1 ]);
常用API
属性length
:返回数组长度,注意不加小括号
Arrays.sort()
:数组排序
Arrays.fill(int[] a, int val)
:填充数组
Arrays.toString()
:将数组转化为字符串
Arrays.deepToString()
:将多维数组转化为字符串
数组不可变长
字符串 String
类
1 2 3 4 5 6 7 8 String a = "Hello World" ;String b = "My name is" ;String x = b; String c = b + "yxc" ; String d = "My age is " + 18 ; String str = String.format("My age is %d" , 18 ); String money_str = "123.45" ;double money = Double.parseDouble(money_str);
1 2 String a = "hello" ;a += "world" ;
1 2 3 4 String str = "hello world" ;for (int i = 0 ; i < str.length(); i++){ System.out.println(str.charAt(i)); }
常用的API
length()
:返回长度
split(String regex)
:分割字符串
indexOf(char c)
、indexOf(String str)
:查找,找不到返回-1
equals()
:判断两个字符串是否相等,注意不能直接用==
compareTo()
:判断两个字符串的字典序大小,负数表示小于,0表示相等,正数表示大于
startsWith()
:判断是否以某个前缀开头
endsWith()
:判断是否以某个后缀结尾
trim()
:去掉首位的空白字符
toLowerCase()
:全部用小写字符
toUpperCase()
:全部用大写字符
replace(char oldChar, char newChar)
:替换字符
replace(String oldRegex, String newRegex)
:替换字符串
substring(int beginIndex, int endIndex)
:返回[beginIndex, endIndex)
中的子串
StringBuilder
、StringBuffer
String
不能被修改,如果打算修改字符串,可以使用StringBuilder
和StringBuffer
。
StringBuffer
线程安全,速度较慢;StringBuilder
线程不安全,速度较快。
1 2 3 4 5 6 7 8 9 StringBuilder sb = new StringBuilder ("Hello " ); sb.append("World" ); System.out.println(sb); for (int i = 0 ; i < sb.length(); i ++ ) { sb.setCharAt(i, (char )(sb.charAt(i) + 1 )); } System.out.println(sb);
常用API
函数 Java
的所有变量和函数都要定义在类中。
函数或变量前加 static
表示静态对象,类似于全局变量。
静态对象属于class
,而不属于class
的具体实例
静态函数中只能调用静态函数和静态变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import java.util.Arrays;public class Main { public static void main (String[] args) { System.out.println(max(3 , 4 )); int [][] a = new int [3 ][4 ]; fill(a, 3 ); System.out.println(Arrays.deepToString(a)); int [][] b = getArray2d(2 , 3 , 5 ); System.out.println(Arrays.deepToString(b)); } private static int max (int a, int b) { if (a > b) return a; return b; } private static void fill (int [][] a, int val) { for (int i = 0 ; i < a.length; i ++ ) for (int j = 0 ; j < a[i].length; j ++ ) a[i][j] = val; } private static int [][] getArray2d(int row, int col, int val) { int [][] a = new int [row][col]; for (int i = 0 ; i < row; i ++ ) for (int j = 0 ; j < col; j ++ ) a[i][j] = val; return a; } }
类与接口 day03:配置git环境与项目创建 项目设计
项目包含的模块
PK模块:匹配界面(微服务)、实况直播界面(WebSocket协议)
对局列表模块:对局列表界面、对局录像界面
排行榜模块:Bot排行榜界面
用户中心模块:注册界面、登录界面、我的Bot界面、每个Bot的详情界面
前后端分离模式
SpringBoot
实现后端
Vue3
实现web端和AcApp端
配置git环境
安装Git Bash
https://gitforwindows.org/
进入家目录生成密钥:执行命令ssh-keygen
在Ac Git
上注册账号, Ac Git
将id_rsa.pub
的内容复制到Ac Git
上
创建项目后端
https://start.spring.io/
加载慢的话,可以换成:https://start.aliyun.com
创建项目 这边选项里的JDK最好选择Oracle OpenJDk
的1.8,我选择了本地的会产生错误。
然后在下一页面只需要选择 Web/Spring Web
和 Templa Engines/thymeleaf
两个选项。
编写controller类
则可以根据localhost:8080/pk/index/
路径跳转至index.html
页面
修改后端端口号 在application.properties
文件中修改server.port=8080
为server.port=3000
,则访问路径变为:localhost:3000
创建项目Web端与AcApp端
创建vue项目 创建vue
项目时,使用Power Shell
进行创建,可以现在Power Sehll
里进入需要创建的目录中,再打开vue ui
进行项目创建。
安装插件 创建好项目后,需要安装插件:
安装依赖 然后web
端需要再装依赖jquery
和bootstrap
运行项目
day04:创建菜单与游戏页面 1 Vue创建组件 组件作用 当所有页面都共享一部分内容时,可将该内容写为一个组件。
在 components/
目录下创建。
注意: 组件名称必须有两个字母大写,没有或只有一个会报错
1 2 3 4 5 6 7 8 9 10 11 12 <template> </template> <script> </script> <style scoped> /* scoped的作用是在本页面写的css会加上随机字符串,不会影响别的页面 */ </style>
创建导航栏 效果图:
Bootstrap添加导航栏 在 Bootstrap 中搜索Navbar就可以找到各种样式的导航栏,选择其中一种即可。
在components/
目录下创建NavBar.vue
组件,并修改组件中的一些名称信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <template> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <!-- router-link 使用该标签代替a标签,点击时页面不会刷新 --> <router-link class="navbar-brand" :to="{ name: 'home' }">King Of Bots</router-link> <div class="collapse navbar-collapse" id="navbarText"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li class="nav-item"> <router-link :class="route_name == 'pk_index' ? 'nav-link active' : 'nav-link'" :to="{ name: 'pk_index' }"> 对战 </router-link> </li> <li class="nav-item"> <router-link :class="route_name == 'record_index' ? 'nav-link active' : 'nav-link'" :to="{ name: 'record_index' }">对局列表</router-link> </li> <li class="nav-item"> <router-link :class="route_name == 'ranklist_index' ? 'nav-link active' : 'nav-link'" :to="{ name: 'ranklist_index' }">排行榜</router-link> </li> </ul> <ul class="navbar-nav"> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> xzt </a> <ul class="dropdown-menu" aria-labelledby="navbarDropdown"> <li> <router-link class="dropdown-item" :to="{ name: 'user_bot_index' }">我的Bot</router-link> </li> <li> <hr class="dropdown-divider"> </li> <li><a class="dropdown-item" href="#">退出</a></li> </ul> </li> </ul> </div> </div> </nav> </template> <script> import { useRoute } from 'vue-router' // 用于获取当前在哪个页面 import { computed } from 'vue' export default { setup() { const route = useRoute(); let route_name = computed(() => route.name) return { route_name } } } </script> <style scoped> </style>
会遇到的问题 在创建好组件后,会提示需要安装依赖@popperjs/core
,去vue页面下载安装即可。
将页面和导航栏链接地址映射
在views/
目录下创建若干目录,分别保存一个对应的.vue
文件,格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!-- RanklistIndexView.vue --> <template> <ContentField>排行榜</ContentField> </template> <script> import ContentField from '@/components/ContentField.vue' // 写的一个组件,所有页面共享的一个组件 export default{ components: { ContentField } } </script> <style> </style>
ContentField.vue
组件,实现一个卡片,在bootstrap
中搜索 card 即可得到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!-- ContentField.vue --> <template> <div class="container content-field"> <div class="card"> <div class="card-body"> <slot></slot> </div> </div> </div> </template> <script> </script> <style scoped> div.content-field { margin-top: 20px; } </style>
在router/
目录下创建index.js
,在文件中填写路由
需要输入根目录重定向至pk页面
输入地址格式错误或乱码,则重定向至404页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import { createRouter, createWebHistory } from 'vue-router' import PkIndexView from '@/views/pk/PkIndexView' import RecordIndexView from '@/views/record/RecordIndexView' import RanklistIndexView from '@/views/ranklist/RanklistIndexView' import NotFound from '@/views/error/NotFound' import UserBotIndexView from '@/views/user/bot/UserBotIndexView' const routes = [ { path : "/" , name : "home" , redirect : "/pk/" , }, { path : "/pk/" , name : "pk_index" , component : PkIndexView , }, { path : "/record/" , name : "record_index" , component : RecordIndexView , }, { path : "/ranklist/" , name : "ranklist_index" , component : RanklistIndexView , }, { path : "/404/" , name : "404" , component : NotFound , }, { path : "/user/bot/" , name : "user_bot_index" , component : UserBotIndexView , }, { path : "/:catchAll(.*)" , redirect : "/404/" }, ] const router = createRouter ({ history : createWebHistory (), routes }) export default router
导航栏中存在的问题
每次点击某个链接,页面会进行刷新,可以将<a>
标签改为<router-link>
标签,避免这种问题
1 2 3 <router-link :class="route_name == 'pk_index' ? 'nav-link active' : 'nav-link'" :to="{ name: 'pk_index' }"> 对战 </router-link>
:class
判断是否在当前页面,若在的话则可以添加active
实现该链接名高亮。
:to
点击时跳转的页面,通过上面router/index.js
页面的映射名字进行跳转。
最终效果
创建对战页面的地图和障碍物 需要实现的效果
创建游戏对象基类 目的: 因为每秒游戏的对象都会刷新一遍,所以需要将所有游戏的对象都存入一个数组中
在assets/
目录下创建scripts/
和 images/
目录,然后在scripts/
目录下创建 AcGameObject.js
编写递归函数:若this.has_called_start = false
则代表没执行过start()
函数,执行该函数即可。否则,需要执行update()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 const AC_GAME_OBJECTS = [];export class AcGameObject { constructor ( ) { AC_GAME_OBJECTS .push (this ); this .timedelta = 0 ; this .has_called_start = false ; } start ( ) { } update ( ){ } on_destory ( ) { } destory ( ) { this .on_destory (); for (let i in AC_GAME_OBJECTS ) { const obj = AC_GAME_OBJECTS [i]; if (obj === this ){ AC_GAME_OBJECTS .splice (i); break ; } } } } let last_timestamp; const step = timestamp =>{ for (let obj of AC_GAME_OBJECTS ) { if (!obj.has_called_start ){ obj.has_called_start = true ; obj.start (); } else { obj.timedelta = timestamp - last_timestamp; obj.update (); } } last_timestamp = timestamp; requestAnimationFrame (step) } requestAnimationFrame (step)
创建游戏地图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <!-- GameMap.vue --> <template> <div ref="parent" class="gamemap"> <canvas ref="canvas"></canvas> </div> </template> <script> import { GameMap } from "@/assets/scripts/GameMap"; import { ref, onMounted } from "vue"; export default { setup() { let parent = ref(null); let canvas = ref(null); onMounted(() => { new GameMap(canvas.value.getContext('2d'), parent.value) // 创建GameMap对象,并传入参数 }); return { parent, canvas } } } </script> <style scoped> div.gamemap{ width: 100%; height: 100%; display: flex; /* 居中,可以使水平和垂直都居中 */ justify-content: center; align-items: center; } </style>
PlayGround
:主要显示 pk 页面的内容,除了游戏地图可能还包含积分板等其他内容,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <!-- PlayGround.vue --> <template> <div class="playground"> <GameMap /> </div> </template> <script> import GameMap from "./GameMap.vue"; export default { components: { GameMap, } } </script> <style scoped> div.playground { width: 60vw; height: 70vh; /* background-color: lightblue; */ /* 背景颜色可以去掉 */ margin: 40px auto; } </style>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <PlayGround /> </template> <script> import PlayGround from '../../components/PlayGround.vue' export default{ components: { PlayGround } } </script> <style> </style>
GameMap.js
创建格子地图定义初始变量
1 2 3 4 5 6 7 8 9 10 11 constructor (ctx, parent ) { super (); this .ctx = ctx; this .parent = parent; this .L = 0 ; this .rows = 13 ; this .cols = 13 ; }
首先需要计算每个小正方形的大小。规定游戏地图由13行,每行13个小正方形组成。
所以小正方形的边长求解办法为:min{ height / rows, width / cols}
,编写update_size()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 update_size ( ) { this .L = parseInt (Math .min (this .parent .clientWidth / this .cols , this .parent .clientHeight / this .rows )); this .ctx .canvas .width = this .L * this .cols ; this .ctx .canvas .height = this .L * this .rows ; } update ( ) { this .update_size (); this .render (); } render ( ) { this .ctx .fillStyle = 'green' ; this .ctx .fillRect (0 , 0 , this .ctx .canvas .width , this .ctx .canvas .height ); }
定义两种颜色,如果是偶数显示一种颜色,否则显示另一种颜色。修改render()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 render ( ) { const color_even = "#AAD751" , color_odd = "#A2D149" ; for (let r = 0 ; r < this .rows ; r ++){ for (let c = 0 ; c < this .cols ; c ++){ if ((r + c) % 2 == 0 ){ this .ctx .fillStyle = color_even; } else { this .ctx .fillStyle = color_odd; } this .ctx .fillRect (c * this .L , r * this .L , this .L , this .L ); } } } update ( ) { this .update_size (); this .render (); }
创建墙 在scripts/
目录下创建Wall.js`
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { AcGameObject } from "./AcGameObject" ;export class Wall extends AcGameObject { constructor (r, c, gamemap ) { super (); this .r = r; this .c = c; this .gamemap = gamemap; this .color = "#B37226" ; } update ( ) { this .render (); } render ( ) { const L = this .gamemap .L ; const ctx = this .gamemap .ctx ; ctx.fillStyle = this .color ; ctx.fillRect (this .c * L, this .r * L, L, L); } }
修改GameMap.js
给四周添加墙 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 constructor (ctx, parent ) { super (); this .ctx = ctx; this .parent = parent; this .L = 0 ; this .rows = 13 ; this .cols = 13 ; this .wall = []; } creat_Walls ( ) { const g = []; for (let r = 0 ; r < this .cols ; r ++ ) { g[r] = []; for (let c = 0 ; c < this .cols ; c ++ ) { g[r][c] = false ; } } for (let r = 0 ; r < this .rows ; r ++ ) { g[r][0 ] = g[r][this .cols - 1 ] = true ; } for (let c = 0 ; c < this .cols ; c ++ ) { g[0 ][c] = g[this .rows - 1 ][c] = true ; } for (let r = 0 ; r < this .rows ; r ++ ) { for (let c = 0 ; c < this .cols ; c ++ ) { if (g[r][c]) { this .wall .push (new Wall (r, c, this )); } } } } start ( ) { this .create_Walls (); }
在地图中随机生成墙 要求:
在地图中随机生成墙,并且是沿主对角线轴对称生成,
左下角和右上角不会生成,用来做蛇的初始位置
保证从左下角到右上角是连通的。这边使用 flood fill
算法实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 check_connectivity (g, sx, sy, tx, ty ){ if (sx == tx && sy == ty) return true ; g[sx][sy] = true ; let dx = [-1 , 0 , 1 , 0 ], dy = [0 , 1 , 0 , -1 ]; for (let i = 0 ; i < 4 ; i ++){ let x = sx + dx[i], y = sy + dy[i]; if (!g[x][y] && this .check_connectivity (g, x, y, tx, ty)) return true ; } return false ; } create_Walls ( ) { const g = []; for (let r = 0 ; r < this .rows ; r ++){ g[r] = []; for (let c = 0 ; c < this .cols ; c ++){ g[r][c] = false ; } } for (let r = 0 ; r < this .rows ; r ++){ g[r][0 ] = g[r][this .cols - 1 ] = true ; } for (let c = 0 ; c < this .cols ; c ++){ g[0 ][c] = g[this .rows - 1 ][c] = true ; } for (let i = 0 ; i < this .inner_walls_count / 2 ; i ++) { for (let j = 0 ; j < 1000 ; j ++) { let r = parseInt (Math .random () * this .rows ); let c = parseInt (Math .random () * this .cols ); if (g[r][c] || g[c][r]) continue ; if (r == this .rows - 2 && c == 1 || r == 1 && c == this .cols - 2 ) continue ; g[r][c] = g[c][r] = true ; break ; } } const copy_g = JSON .parse (JSON .stringify (g)); if (!this .check_connectivity (copy_g, this .rows - 2 , 1 , 1 , this .cols - 2 )) return false ; for (let r = 0 ; r < this .rows ; r ++){ for (let c = 0 ; c < this .cols ; c ++){ if (g[r][c]){ this .walls .push (new Wall (r, c, this )); } } } return true ; } start ( ) { for (let i = 0 ; i < 1000 ; i ++ ) if (this .create_Walls ()) break ; }
day05:创建菜单与游戏界面 2 修改地图 原因: 上次实现的地图是一个13 * 13
的正方形,可能会造成一种情况:在某一时刻后,两个选手的操作会造成两条蛇头走到同一个格子。降低了游戏的公平性。因此需要进行修改。
思想: 我们只需要将两条蛇的坐标 改为和为奇数 的情况,也就是说将整张地图改为13 * 14
大小。
修改代码:
1 2 3 this .rows = 13 ;this .cols = 14 ;
连带修改: 原因: 修改地图后会造成游戏地图变成长方形,不能实现主对角线轴对称,所以需要把轴对称改为中心对称。实现方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 for (let i = 0 ; i < this .inner_walls_count / 2 ; i ++) { for (let j = 0 ; j < 1000 ; j ++) { let r = parseInt (Math .random () * this .rows ); let c = parseInt (Math .random () * this .cols ); if (g[r][c] || g[this .rows - 1 - r][this .cols - 1 - c]) continue ; if (r == this .rows - 2 && c == 1 || r == 1 && c == this .cols - 2 ) continue ; g[r][c] = g[this .rows - 1 - r][this .cols - 1 - c] = true ; break ; } }
实现蛇头 实现思想: 本质上蛇是由一堆联系的格子组成的序列,所以只需要把格子记录下来即可。
实现Cell.js
创建Cell.js
用于存储蛇所占用的格子信息,
1 2 3 4 5 6 7 8 9 10 export class Cell { constructor (r, c ){ this .r = r; this .c = c; this .x = c + 0.5 ; this .y = r + 0.5 ; } }
实现Snake.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import { AcGameObject } from "./AcGameObject" ;import { Cell } from "./Cell" ;export class Snake extends AcGameObject { constructor (info, gamemap ) { super (); this .id = info.id ; this .color = info.color ; this .gamemap = gamemap; this .cells = [new Cell (info.r , info.c )]; } start ( ) { } update ( ) { this .render (); } render ( ) { const L = this .gamemap .L ; const ctx = this .gamemap .ctx ; ctx.fillStyle = this .color ; for (const cell of this .cells ) { ctx.beginPath (); ctx.arc (cell.x * L, cell.y * L, L / 2 , 0 , Math .PI * 2 ); ctx.fill (); } } }
修改GameMap.js
,在GameMap.js
中创建两条蛇的对象,方便每帧渲染一次。
1 2 3 4 5 this .Snakes = { new Snake ({id : 0 , color : "#4876ec" , r : this .rows - 2 , c : 1 }, this ), new Snake ({id : 1 , color : "#f94848" , r : 1 , c : this .cols - 2 }, this ), }
成功界面
实现蛇的移动 实现思想:
移动应该是连贯的,但是蛇的身体是由一格一格连续的格子组成的
中间保持不动,头和尾动,在头部创建一个新的节点,朝着目的地移动。尾巴朝着目的地动
蛇移动的条件
同时获取到 两个人 / 两个机器 的操作才能够移动
最简单的移动 在Snake.js
中添加代码,实现蛇头的向右移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { AcGameObject } from "./AcGameObject" ;import { Cell } from "./Cell" ;export class Snake extends AcGameObject { constructor (info, gamemap ) { super (); this .id = info.id ; this .color = info.color ; this .gamemap = gamemap; this .cells = [new Cell (info.r , info.c )]; this .speed = 5 ; } update_move ( ) { this .cells [0 ].x += this .speed * this .timedelta / 1000 ; } update ( ) { this .update_move (); this .render (); } }
所呈现的效果是:两个小球一直向右移动。
实现连贯的移动 由于可能会产生一些问题, 也就是中间某个状态,没有完全移出去,蛇的身子会出现问题。
中间不动,首尾动!创建的虚拟节点朝着目的地移动。只有两个点动。
考虑蛇什么时候动? 回合制游戏,两个人都有输入的时候,才可以移动。
修改Snake.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { AcGameObject } from "./AcGameObject" ;import { Cell } from "./Cell" ;export class Snake extends AcGameObject { constructor (info, gamemap ) { super (); this .id = info.id ; this .color = info.color ; this .gamemap = gamemap; this .cells = [new Cell (info.r , info.c )]; this .speed = 5 ; this .direction = -1 ; this .status = "idle" ; } }
需要有一个”裁判“来判断两条蛇是否进行移动,但是”运动员”不能当”裁判“,所以需要把判断的代码写在GameMap.js
中
1 2 3 4 5 6 7 8 check_ready ( ) { for (const snake of this .snakes ) { if (snake.status !== "idle" ) return false ; if (snake.direction === -1 ) return false ; } return true ; }
然后在 Snake.js
中更新下一步蛇的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 this .next_cell = null ; this .dr = [-1 , 0 , 1 , 0 ]; this .dc = [0 , 1 , 0 , -1 ]; this .step = 0 ; next_step ( ) { const d = this .direction ; this .next_cell = new Cell (this .cells [0 ].r + this .dr [d], this .cells [0 ].c + this .dc [d]); this .direction = -1 ; this .status = "move" ; this .step ++ ; }
在GameMap.js
中实现每帧更新下状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 next_step ( ) { for (const snake of this .snake ) { snake.next_step (); } } update ( ) { this .update_size (); if (this .check_ready ()) { this .next_step (); } this .render (); }
实现读取键盘的操作 从键盘获取w
a
s
d
和 ↑
↓
←
→
来控制两条蛇。
在GameMap.vue
中修改
1 <canvas ref="canvas" tabindex="0"></canvas>
在Snake.js
中加入一个辅助函数,用来获取给的移动方向。
1 2 3 4 5 set_direction (d ) { this .direction = d; }
在GameMap.js
中修改,添加事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 add_listening_events ( ) { this .ctx .canvas .focus (); const [snake0, snake1] = this .snakes ; this .ctx .canvas .addEventListener ("keydown" , e => { if (e.key === 'w' ) snake0.set_direction (0 ); else if (e.key === 'd' ) snake0.set_direction (1 ); else if (e.key === 's' ) snake0.set_direction (2 ); else if (e.key === 'a' ) snake0.set_direction (3 ); else if (e.key === 'ArrowUp' ) snake1.set_direction (0 ); else if (e.key === 'ArrowRight' ) snake1.set_direction (1 ); else if (e.key === 'ArrowDown' ) snake1.set_direction (2 ); else if (e.key === 'ArrowLeft' ) snake1.set_direction (3 ); }); }
在Snake.js
中更新状态,让每一帧执行一次更新状态。
1 2 3 4 5 6 7 8 update ( ) { if (this .status === 'move' ) { this .uppdate_move () } this .render (); }
实现真正的移动 在Snake.js
中修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 import { AcGameObject } from "./AcGameObject" ;import { Cell } from "./Cell" ;export class Snake extends AcGameObject { constructor (info, gamemap ) { super (); this .id = info.id ; this .color = info.color ; this .gamemap = gamemap; this .cells = [new Cell (info.r , info.c )]; this .next_cell = null ; this .speed = 5 ; this .direction = -1 ; this .status = "idle" ; this .dr = [-1 , 0 , 1 , 0 ]; this .dc = [0 , 1 , 0 , -1 ]; this .step = 0 ; this .eps = 1e-2 ; } start ( ) { } set_direction (d ) { this .direction = d; } next_step ( ) { const d = this .direction ; this .next_cell = new Cell (this .cells [0 ].r + this .dr [d], this .cells [0 ].c + this .dc [d]); this .direction = -1 ; this .status = "move" ; this .step ++ ; const k = this .cells .length ; for (let i = k; i > 0 ; i -- ) { this .cells [i] = JSON .parse (JSON .stringify (this .cells [i - 1 ])); } } update_move ( ) { const dx = this .next_cell .x - this .cells [0 ].x ; const dy = this .next_cell .y - this .cells [0 ].y ; const distance = Math .sqrt (dx * dx + dy * dy); if (distance < this .eps ) { this .cells [0 ] = this .next_cell ; this .next_cell = null ; this .status = "idle" ; } else { const move_distance = this .speed * this .timedelta / 1000 ; this .cells [0 ].x += move_distance * dx / distance; this .cells [0 ].y += move_distance * dy / distance; } } update ( ) { if (this .status === 'move' ) { this .update_move (); } this .render (); } render ( ) { const L = this .gamemap .L ; const ctx = this .gamemap .ctx ; ctx.fillStyle = this .color ; for (const cell of this .cells ) { ctx.beginPath (); ctx.arc (cell.x * L, cell.y * L, L / 2 , 0 , Math .PI * 2 ); ctx.fill (); } } }
蛇尾移动 在Snake.js
中添加代码,判断蛇尾是否增长。
1 2 3 4 5 6 check_tail_increasing ( ) { if (step <= 10 ) return true ; if (step % 3 === 1 ) return true ; return false ; }
修改Snake.js
,判断蛇尾在下一步中是否增长
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 this .next_cell = null ; this .dr = [-1 , 0 , 1 , 0 ]; this .dc = [0 , 1 , 0 , -1 ]; this .step = 0 ;this .eps = 1e-2 next_step ( ) { const d = this .direction ; this .next_cell = new Cell (this .cells [0 ].r + this .dr [d], this .cells [0 ].c + this .dc [d]); this .direction = -1 ; this .status = "move" ; this .step ++ ; const k = this .cells .length ; for (let i = k; i > 0 ; i -- ) { this .cells [i] = JSON .parse (JSON .stringify (this .cells [i - 1 ])); } } update_move ( ) { const dx = this .next_cell .x - this .cells [0 ].x ; const dy = this .next_cell .y - this .cells [0 ].y ; const distance = Math .sqrt (dx * dx + dy * dy); if (distance < this .eps ) { this .cells [0 ] = this .next_cell ; this .next_cell = null ; this .status = "idle" ; if (!this .check_tail_increasing ()) { this .cells .pop (); } } else { const move_distance = this .speed * this .timedelta / 1000 ; this .cells [0 ].x += move_distance * dx / distance; this .cells [0 ].y += move_distance * dy / distance; if (!this .check_tail_increasing ()) { const k = this .cells .length ; const tail = this .cells [k - 1 ], tail_target = this .cells [k - 2 ]; const tail_dx = tail_target.x - tail.x ; const tail_dy = tail_target.y - tail.y ; tail.x += move_distance * tail_dx / distance; tail.y += move_distance * tail_dy / distance; } } }
实现效果
美化蛇 修改Snake.js
,让蛇变得连贯,缩小一点,添加下列代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 render ( ) { const L = this .gamemap .L ; const ctx = this .gamemap .ctx ; ctx.fillStyle = this .color ; for (const cell of this .cells ) { ctx.beginPath (); ctx.arc (cell.x * L, cell.y * L, L / 2 * 0.8 , 0 , Math .PI * 2 ); ctx.fill (); } for (let i = 1 ; i < this .cells .length ; i ++ ) { const a = this .cells [i - 1 ], b = this .cells [i]; if (Math .abs (a.x - b.x ) < this .eps && Math .abs (a.y - b.y ) < this .eps ) continue ; if (Math .abs (a.x - b.x ) < this .eps ) { ctx.fillRect ((a.x - 0.4 ) * L, Math .min (a.y , b.y ) * L, L * 0.8 , Math .abs (a.y - b.y ) * L); } else { ctx.fillRect (Math .min (a.x , b.x ) * L, (a.y - 0.4 ) * L, Math .abs (a.x - b.x ) * L, L * 0.8 ); } } }
实现效果
实现当蛇撞墙或者撞自己的身体或者撞对手身体,判定死亡。
1 2 3 4 5 6 7 8 9 10 11 12 13 next_step ( ) { if (!this .gamemap .check_valid (this .next_cell )) { this .status = "die" ; } } render ( ) { if (this .status === "die" ) { ctx.fillStyle = "white" ; } }
实现后效果,当判定失败后,不能进行任何操作。
实现眼睛 修改Snake.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 this .eye_direction = 0 ;if (this .id === 1 ) this .eye_direction = 2 ;this .eye_dx = [ [-1 , 1 ]; [1 , 1 ]; [1 , -1 ]; [-1 , -1 ]; ]; this .eye_dy = [ [-1 , -1 ]; [-1 , 1 ]; [1 , 1 ]; [1 , -1 ]; ]; next_step ( ) { this .eye_direction = d; } render ( ) { ctx.fillStyle = "black" ; for (let i = 0 ; i < 2 ; i ++ ) { const eye_x = (this .cells [0 ].x + this .eye_dx [this .eye_direction ][i] * 0.15 ) * L; const eye_y = (this .cells [0 ].y + this .eye_dy [this .eye_direction ][i] * 0.15 ) * L; ctx.beginPath (); ctx.arc (eye_x, eye_y, L * 0.05 , 0 , Math .PI * 2 ); ctx.fill (); } }
最终效果