项目实战——创建菜单与游戏页面(上)

目录

PS:整篇文章全是实现前端的工作,如果大家不了解vue3,建议补一下前置知识~~

一、整体框架

二、实现导航栏

 三、导航栏中实现页面跳转

四、实现地图

五、实现墙体

六、生成障碍物 


PS:整篇文章全是实现前端的工作,如果大家不了解vue3,建议补一下前置知识~~

一、整体框架

项前端框架

 

二、实现导航栏

实现导航栏的组件我们可以在 bootstrap 中获得

Bootstrap v5 中文文档 · Bootstrap 是全球最受欢迎的 HTML、CSS 和 JS 前端工具库。 | Bootstrap 中文网 (bootcss.com)

 在componets 中创建组件 NavBar.vue

在template标签中导入模板

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <div class="container-fluid">
    <a class="navbar-brand" href="#">Navbar w/ text</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarText">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <li class="nav-item">
          <a class="nav-link active" aria-current="page" href="#">Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">Features</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">Pricing</a>
        </li>
      </ul>
      <span class="navbar-text">
        Navbar text with an inline element
      </span>
    </div>
  </div>
</nav>

App.vue中添加 NavBar 组件(App.vue 就是我们的首页页面)

<template>
  <NavBar></NavBar>
  <router-view></router-view>
</template>

<script>

import NavBar from "@/components/NavBar.vue";
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap"

export default {
  components:{
    NavBar
  }
}
</script>

<style>
body {
  background-image: url("./assets/images/background.png");
  background-size: cover;
}
</style>

 安装依赖

成功之后的导航栏~ 

 

 三、导航栏中实现页面跳转

由于我们每个页面可能设计很多组件,因此我们每个页面建一个文件夹比较好~

需要用到 5 个界面

pk + record + ranklist + userbots + 404

 这里我启动vue时发生了错误 : 

解决方法:删掉node_modules,重新cnpm install一下就好了

 

每个页面的模板如下,不同的页面修改div里面的字体就可。

对战的修改对战、对局列表就修改为对局列表等。

<template>
    <div>对战</div>
</template>

<script>

</script>

<style scoped> 
</style>

如何把地址和页面产生关联呢?

在router/index.js 中定义~~

import PkIndexView from '../views/pk/PkIndexView'
import RanklistIndexView from '../views/ranklist/RanklistIndexView'
import RecordIndexView from '../views/record/RecordIndexView'
import UserBotIndexView from '../views/user/bot/UserBotIndexView'
import NotFound from '../views/error/NotFound'

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: "/user/bot",
    name: "user_bot_index",
    component: UserBotIndexView,
  },
  {
    path: "/404/",
    name: "404",
    component: NotFound,
  },
  {
    path: "/:catchAll(.*)",
    redirect: "/404/",
  }
]

这里有个小细节:如果输入了根路径的网址,我们把它重定向到PK页面~~ 如果是一个不存在的网址,我们跳转到404(从上到下匹配,最后的全是404) 

还需要在 NavBar 中更改跳转的路径:

在这里插入图片描述

 如何实现 点击不同页面,但不刷新呢?

把 <a> 换成  <router-link>  ,按照以下格式

<router-link class="nav-link" :to="{name: 'pk_index'}">对战</router-link>

在这里插入图片描述

 每个页面有一部分都是公共的,因此我们把这一部分抽取出来作为一个组件

在Commponents中新建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>

在不同界面引入组件,比如 PK 界面:

<template>
    <ContentField>
        对战
    </ContentField>
</template>


<script>
import ContentField from '../../components/ContentField'

export default {
    components: {
        ContentField
    }
}
</script>

<style scoped> 
</style>

如何实现我们在哪个界面,那个界面上边的标题高亮呢?

 

我们需要添加三元运算符来判断 是否添加 active 属性:

<template>
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
      <router-link class="navbar-brand" :to="{name: 'home'}">King of Bots</router-link>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <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="router_name == 'record_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'record_index'}">对局列表</router-link>
          </li>
          <li class="nav-item">
            <router-link :class="router_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">
            RedFlower
          </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/reactivity";

export default {
    setup() {
      const route = useRoute();
      let route_name = computed(() => route.name)
      return {
        route_name
      }
    }
}

</script>

<style scoped>

</style>

四、实现地图

地图的特征:

1)大小:13*13

2)中心对称,边缘是墙体,左下角和右下角生成两条蛇,并且左下角和右下角联通

3)地图上会随机生成不同的障碍物

在这里插入图片描述

绘制游戏区域

 在assets 目录下新建文件夹 scripts

新建 AcGameObjects.js (我们创建游戏的经典步骤 hh)

const AC_GAME_OBJECTS = [];

export class AcGameObject {
    contructor() {
        AcGame_Object.push(this);
        this.timedelta = 0; // 时间间隔每一帧
        this.has_called_start = false;
    }

    start() { // 只执行一次

    }

    update() { //每一帧执行一次

    }

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

    }

    distory() {
        this.on_destroy();

        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)

实现地图类:GameMap.js

import { AcGameObject } from "./AcGameObject";

export class GameMap extends AcGameObject {
    constructor(ctx, parent) {
        super();

        this.ctx = ctx;
        this.parent = parent;
        this.L = 0;

    }

    start() {

    }

    update() {
        this.rander();
    }

    //渲染函数
    render() {

    }
}

在 pk 界面创建一个游戏区域,用来显示对战。

在 commponts 写一个组件: PlayGround.vue

<template>
    <div class="playground">

    </div>
</template>

<script>


</script>

<style scoped>
div.playground {
    width: 60vw;
    height: 70vh;
    background: lightblue;
}
</style>

然后在 pk_index 中引入这个组件:

<template>
  <PlayGround/>
</template>


<script>
import PlayGround from '../../components/PlayGround.vue'

export default {
    components: {
        PlayGround
    }
}
</script>

<style scoped> 
</style>

因为在 pk 界面可能还包含记分板等不同的东西。

所以开一个新组件存放别的类型的组件 GameMap.vue

<template>
    <div class="gamemap"></div>
</template>

<script>

</script>

<style scoped>

div.gamemap {
    width: 100%;
    height: 100%;
}
</style>

在 PlayGround.vue 中引入 GameMap.vue

<template>
    <div class="playground">
        <GameMap/>
    </div>
</template>

<script>
import GameMap from "./GameMap.vue";

export default {
    components: {
        GameMap,
    }
}
</script>

在 GameMap.vue 中添加 canvas

<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);
        })
        return {
            parent,
            canvas
        }
    }
}

</script>

<style scoped>

div.gamemap {
    width: 100%;
    height: 100%;
}
</style>

地图展示:

在这里插入图片描述

我们需要在不同大小的界面时保持我们的地图维持一个正方形,因此需要动态计算内部面积

 在这里插入图片描述

 在 GameMap.js 中修改:

import { AcGameObject } from "./AcGameObject";

export class GameMap extends AcGameObject {
    constructor(ctx, parent) {
        super();

        this.ctx = ctx;
        this.parent = parent;
        this.L = 0;

        this.rows = 13;
        this.cols = 13;
    }

    start() {

    }

    update_size() {
        // 计算小正方形的边长
        this.L = 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);
    }
}

如何让我们的地图在区域居中?

在GameMap.vue中添加:

<style scoped>

div.gamemap {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}
</style>

绘制正方形作为我们的地图大致轮廓:

在这里插入图片描述

如何让我们的地图的每个小格子有辨识度呢?不同的格子有区分?

我们可以奇偶显示正方形区域内的小格子,用不同颜色即可:

 在 GameMap.js 中修改, 完整代码如下 :

import { AcGameObject } from "./AcGameObject";

export class GameMap extends AcGameObject {
    constructor(ctx, parent) {
        super();

        this.ctx = ctx;
        this.parent = parent;
        this.L = 0;

        this.rows = 13;
        this.cols = 13;
    }

    start() {

    }

    update_size() {
        // 计算小正方形的边长
        this.L = 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() {
        // 取颜色
        const color_eve = "#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_eve;
                } else {
                    this.ctx.fillStyle = color_odd;
                }
                //左上角左边,明确canvas坐标系
                this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
            }
    }
}

奇偶显示效果:

在这里插入图片描述

五、实现墙体

在 scripts 新建一个 wall.js

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 , 引入 Wall

import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";

export class GameMap extends AcGameObject {
    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.creat_walls();
    }

    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() {
        // 取颜色
        const color_eve = "#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_eve;
                } else {
                    this.ctx.fillStyle = color_odd;
                }
                //左上角左边,明确canvas坐标系
                this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
            }
    }
}

在这里插入图片描述

六、生成障碍物 

修改 GameMap.js ,随机生成障碍物,同时禁止在左下角和右上角生成障碍物。

import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";

export class GameMap extends AcGameObject {
    constructor(ctx, parent) {
        super();

        this.ctx = ctx;
        this.parent = parent;
        this.L = 0;

        this.rows = 13;
        this.cols = 13;

        this.inner_walls_count = 20;
        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 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;
            }
        }

        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.creat_walls();
    }

    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() {
        // 取颜色
        const color_eve = "#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_eve;
                } else {
                    this.ctx.fillStyle = color_odd;
                }
                //左上角左边,明确canvas坐标系
                this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
            }
    }
}

为了使我们的游戏拥有可玩性,两条蛇存在互动,因此两条蛇必须是“可接触的”,这里我们采用flood fill 算法:

import { AcGameObject } from "./AcGameObject";
import { Wall } from "./Wall";

export class GameMap extends AcGameObject {
    constructor(ctx, parent) {
        super();

        this.ctx = ctx;
        this.parent = parent;
        this.L = 0;

        this.rows = 13;
        this.cols = 13;

        this.inner_walls_count = 50;
        this.wall = [];
    }

    // flood fill算法
    // 参数 ,图 ,起点的x,y 重点的x, y
    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;
     }

    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 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)); // 复制到JSON再转换回来
        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.wall.push(new Wall(r, c, this));
                }
            }
        }

        return true;
    }

    start() {

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

    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() {
        // 取颜色
        const color_eve = "#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_eve;
                } else {
                    this.ctx.fillStyle = color_odd;
                }
                //左上角左边,明确canvas坐标系
                this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L);
            }
    }
}

在这里插入图片描述

大功告成!

记得git push 维护~~

 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值