0%

AcWing SpringBoot框架课

学习平台

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 修饰

1
final int N = 110;

类型转换

  • 显示转化: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;

/**
* @author xzt
* @version 1.0
*/
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); // 格式化输出,float与double都用%f输出
  • 方式二:输出效率较高,输出规模较大时使用,需要抛出异常
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;

/**
* @author xzt
* @version 1.0
*/
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;

/**
* @author xzt
* @version 1.0
*/
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;

/**
* @author xzt
* @version 1.0
*/
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;

/**
* @author xzt
* @version 1.0
*/
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];  // 初始化长度为5的int数组,初始值为0
int n = 10;
float[] b = new float[n]; // 初始化长度为n的float数组,初始值为0.0F
char[] c = {'a', 'b', 'c'}; // 初始化长度为3的char数组,初始值为:'a', 'b', 'c'
char[] d = c; // d与c地址相同,更改c中的元素,d中的元素也会改变

数组元素的读取与写入

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可以通过加号拼接
String d = "My age is " + 18; // int会被隐式转化成字符串"18"
String str = String.format("My age is %d", 18); // 格式化字符串,类似于C++中的sprintf
String money_str = "123.45";
double money = Double.parseDouble(money_str); // String转double
  • 只读变量,不能修改,例如:
1
2
String a = "hello";
a += "world"; // 会构造一个新的字符串
  • 访问String中的字符串
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)中的子串

StringBuilderStringBuffer

String不能被修改,如果打算修改字符串,可以使用StringBuilderStringBuffer

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
  • reverse():翻转字符串

函数

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环境

  1. 安装Git Bash https://gitforwindows.org/
  2. 进入家目录生成密钥:执行命令ssh-keygen
  3. Ac Git上注册账号, Ac Git
  4. id_rsa.pub的内容复制到Ac Git

创建项目后端

  • https://start.spring.io/ 加载慢的话,可以换成:https://start.aliyun.com

创建项目

这边选项里的JDK最好选择Oracle OpenJDk的1.8,我选择了本地的会产生错误。

image-20220711085513041

然后在下一页面只需要选择 Web/Spring WebTempla Engines/thymeleaf 两个选项。

编写controller类

image-20220711085752419

则可以根据localhost:8080/pk/index/路径跳转至index.html页面

修改后端端口号

application.properties文件中修改server.port=8080server.port=3000,则访问路径变为:localhost:3000

创建项目Web端与AcApp端

创建vue项目

创建vue 项目时,使用Power Shell进行创建,可以现在Power Sehll里进入需要创建的目录中,再打开vue ui进行项目创建。

image-20220713204639004

  • 需要创建两个前端项目:webacapp

image-20220713204546107

安装插件

创建好项目后,需要安装插件:

image-20220713205101757

安装依赖

然后web端需要再装依赖jquerybootstrap

image-20220713205210434

运行项目

image-20220713205342173

day04:创建菜单与游戏页面 1

Vue创建组件

组件作用

当所有页面都共享一部分内容时,可将该内容写为一个组件。

components/ 目录下创建。

注意:组件名称必须有两个字母大写,没有或只有一个会报错

image-20220713210047829

  • 每个.vue文件由三部分组成:
1
2
3
4
5
6
7
8
9
10
11
12
<template>

</template>


<script>

</script>

<style scoped> /* scoped的作用是在本页面写的css会加上随机字符串,不会影响别的页面 */

</style>

创建导航栏

效果图:

2a9d61c76c0c44bd5ddb30e4522148c.png

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页面下载安装即可。

image-20220721152928047

image-20220721153032064

将页面和导航栏链接地址映射

  • 创建页面

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/", // 输入根目录重定向至pk页面
},
{
path: "/pk/",
name: "pk_index",
component: PkIndexView, // 地址栏输入 localhost:8080/pk/ 即可显示 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(.*)", // 输入格式错误或乱码,则重定向至404页面
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页面的映射名字进行跳转。

最终效果

image-20220721155438423

创建对战页面的地图和障碍物

需要实现的效果

336af4ed02ac84321dea23ad81ba97a.png

创建游戏对象基类

目的:因为每秒游戏的对象都会刷新一遍,所以需要将所有游戏的对象都存入一个数组中

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函数是否被执行

}

start() { // 创建时执行,只执行一次

}

update(){ // 每一帧执行一次,除第一帧之外

}

on_destory() { // 删除之前执行

}

destory() { // 删除当前元素
this.on_destory();

for(let i in AC_GAME_OBJECTS) { // of 遍历的是值,in 遍历的是下标
const obj = AC_GAME_OBJECTS[i];
if(obj === this){
AC_GAME_OBJECTS.splice(i); // 删除 i位置元素
break;
}
}
}

}

let last_timestamp; // 上一次执行的时刻
const step = timestamp =>{ // 传入参数 timestamp 当前时刻
for(let obj of AC_GAME_OBJECTS) { // of 遍历的是值,in 遍历的是下标
if(!obj.has_called_start){ // start 函数还没有被执行
obj.has_called_start = true;
obj.start();
}
else{
obj.timedelta = timestamp - last_timestamp;
obj.update();
}
}
last_timestamp = timestamp;
requestAnimationFrame(step)
}

requestAnimationFrame(step)

创建游戏地图

  • assets/scripts/目录下创建GameMap.js文件,实现渲染的逻辑

  • components/目录下创建GameMap.vuePlayGround.vue 组件,实现游戏地图的页面创建

  • 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
<!-- 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>
  • 修改后的 PkIndexView.vue
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>

image-20220721162638515

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;
}
// 左上角左边,明确canvas坐标系
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
}
}
}
update() {
this.update_size();
this.render();
}

image-20220721164332394

创建墙

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() {
// 墙 true 无 false
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();
}

image-20220721164838093

在地图中随机生成墙

要求:

  • 在地图中随机生成墙,并且是沿主对角线轴对称生成,
  • 左下角和右上角不会生成,用来做蛇的初始位置
  • 保证从左下角到右上角是连通的。这边使用 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 = [];
// 初始化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)); // 将 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)); // 将 [r,c] 位置设置为墙
}
}
}
return true;
}

start() {
for(let i = 0; i < 1000; i ++ )
if(this.create_Walls())
break;
}

day05:创建菜单与游戏界面 2

修改地图

原因:上次实现的地图是一个13 * 13的正方形,可能会造成一种情况:在某一时刻后,两个选手的操作会造成两条蛇头走到同一个格子。降低了游戏的公平性。因此需要进行修改。

思想:我们只需要将两条蛇的坐标 改为和为奇数 的情况,也就是说将整张地图改为13 * 14大小。

修改代码:

1
2
3
// GameMap.js 中
this.rows = 13;
this.cols = 14;

连带修改:

原因:修改地图后会造成游戏地图变成长方形,不能实现主对角线轴对称,所以需要把轴对称改为中心对称。实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
// GameMap.js 中
// 创建随机障碍物
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;
}
}

image-20220723144936817

实现蛇头

实现思想:本质上蛇是由一堆联系的格子组成的序列,所以只需要把格子记录下来即可。

实现Cell.js

创建Cell.js用于存储蛇所占用的格子信息,

1
2
3
4
5
6
7
8
9
10
// Cell.js
export class Cell{
constructor(r, c){
this.r = r;
this.c = c;
// 替换为 canvas 的坐标系
this.x = c + 0.5;
this.y = r + 0.5;
}
}

实现Snake.js

  • 创建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
// Sanke.js
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";


export class Snake extends AcGameObject {
constructor(info, gamemap) {
super();

// 取出基本的id
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
// GameMap.js constructor 中添加
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),
}

成功界面

image-20220723145947427

实现蛇的移动

实现思想:

移动应该是连贯的,但是蛇的身体是由一格一格连续的格子组成的

中间保持不动,头和尾动,在头部创建一个新的节点,朝着目的地移动。尾巴朝着目的地动

蛇移动的条件

同时获取到 两个人 / 两个机器 的操作才能够移动

最简单的移动

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
// Snake.js
import { AcGameObject } from "./AcGameObject";
import { Cell } from "./Cell";

export class Snake extends AcGameObject {
constructor(info, gamemap) {
super(); // 继承AcGameObject的方法

this.id = info.id;
this.color = info.color;
this.gamemap = gamemap;

this.cells = [new Cell(info.r, info.c)]; // 存放蛇的身体, cell[0] 存放蛇头
// 表示移动速度
this.speed = 5;
}


update_move() {
// 向右移动
this.cells[0].x += this.speed * this.timedelta / 1000;
//向上移动
//this.cells[0].y -= 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
// Snake.js
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; // 蛇每秒走5格
// new add
this.direction = -1; // 移动方向,-1表示没有指令 0, 1, 2, 3表示上右下左
this.status = "idle"; // idle静止, move 移动 die 死亡
}
}

需要有一个”裁判“来判断两条蛇是否进行移动,但是”运动员”不能当”裁判“,所以需要把判断的代码写在GameMap.js

1
2
3
4
5
6
7
8
// GameMap.js
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
// Snake.js
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
// GameMap.js
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
// Snake.js
//辅助函数
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
// GameMap.js
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
// Snake.js
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
// Snake.js
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)]; // 存放蛇的身体,cells[0]存放蛇头
this.next_cell = null; // 下一步的目标位置

this.speed = 5; // 蛇每秒走5个格子
this.direction = -1; // -1表示没有指令,0、1、2、3表示上右下左
this.status = "idle"; // idle表示静止,move表示正在移动,die表示死亡

this.dr = [-1, 0, 1, 0]; // 4个方向行的偏移量
this.dc = [0, 1, 0, -1]; // 4个方向列的偏移量

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
// Snake.js
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;
}
}
}

实现效果

image-20220723164245732

美化蛇

修改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
// Snake.js
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);
}
}
}

实现效果

image-20220723164420044

实现当蛇撞墙或者撞自己的身体或者撞对手身体,判定死亡。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Snake.js
next_step() {
if (!this.gamemap.check_valid(this.next_cell)) {
this.status = "die";
}
}


render() {
if (this.status === "die") {
ctx.fillStyle = "white";
}
}

实现后效果,当判定失败后,不能进行任何操作。

image-20220723164620339

实现眼睛

修改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
// Snake.js
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();
}
}

最终效果

image-20220723164809072

正在加载今日诗词....