一款对弈小游戏【阴阳五行转】项目总结

1,357 阅读5分钟

我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

阴阳五行转是一款结合阴阳五行原理实现的一款对弈小游戏。

游戏体验地址:阴阳五行转

创意思路

起初的想法来源五行相生相克之理,以五行为棋子,有生克之变。

以太极阴阳为棋盘,圆环八个棋子对应八卦。

对弈时,每轮下子可棋盘顺时旋转一个45°,使其一直存在变化之中。

一步一步走太慢了,又设置了多步走,和加入中心点棋位,使其可以直接到对面棋位,同时可以随机变化。

总之围绕五行生克变化,同时棋位变化。哈哈,还有点意思,但就是还有点费脑。

要是棋逢对手,发现好像也能难决胜负。的确,长远的看好,好似一切都在轮回之中。

玩法说明

进入页面看下游戏规则

点击开始游戏

取个帅气的名字加入

点击去准备等待其他玩家邀请 或者 直接邀请已准备玩家

去玩吧开始游戏

游戏规则
  1. 棋盘有 9 个下子位置

  2. 双方各执五子,分别为:金,木,水,火,土

  3. 双方轮流下子,每轮下完后棋盘会自动顺时针旋转45°

  4. 棋盘内,棋子每一轮操作可顺时针拖动或者转换棋子

  • · 棋子可顺时针拖动前方连续任意空白位置,若前方有子与其相克则会被吃掉,否则不能前进。

  • · 双击棋子会变化成相生的棋子,而中心棋子会随机变化

  1. 当棋子走到太极两眼相连位置时可以进入中心或对面位置

  2. 博弈后,当一方没有棋子时,棋多者胜。

额,人机模式还没有(没写过), 若没其他人玩只能自己左右手互博了(隐私模式 [ InPrivate 窗口] 打开另一个窗口,避免localStorage保存重名)

玩过几局大概就知道五行相生相克了

五行相生相克图

技术实现

想好了,撸起袖子就是

遇到了第一个技术选型的问题。

想到的就是各个游戏引擎,cocos, egret, laya, unity等。

结合实际发现我只是了解并不熟悉(第一次写)

还是用我熟悉的js+html+css写吧,简单,纯粹

虽然用canvas绘图比操作DOM会更节省性能,但这个游戏也不复杂,先现实再说。

想到最近world游戏很火,我想就参考了大致的布局。

于是整体页面框架有了,但游戏的核心逻辑还得自己写

主要的功能模块:棋盘布局,棋子放置,对弈通信等

首先是实现棋盘布局,通过三角函数sin,cos可以确定8个棋子的位置布局

// 棋子位置信息
function pieceItemStyle(piece: Piece, index: number) {
  const halfCheckBoardSize = checkerboard.height / 2;
  const halfPieceSize = checkerboard.pieceHeight / -2;
  const pieceItemRadian = ((index - 2) * 2 * Math.PI) / count.value;
  return {
    top:
      halfCheckBoardSize +
      halfCheckBoardSize * Math.sin(pieceItemRadian) +
      "px",
    left:
      halfCheckBoardSize +
      halfCheckBoardSize * Math.cos(pieceItemRadian) +
      "px",
    transform: `translate(${halfPieceSize}px, ${halfPieceSize}px) rotate(${
      (360 / count.value) * index
    }deg)`,
  };
}

再然后就是要确定被选中棋子可放置的位置的逻辑。

这里涉及五行生克,多步走的位置,以及过中心棋位的逻辑


watchEffect(() => {
  if (draging.piece && draging.piece.name) {
    if (draging.piece.isPlay) {
      if (draging.piece.isCenter) {
        connectCenterIndexs.value.forEach((index) => {
          indexCanMoveHandler(index);
        });
      } else {
        // 遍历圆环棋位是否下子
        for (
          let i = selectedIndex.value + 1, last = selectedIndex.value + pieces.value.length;
          i < last;
          i++
        ) {
          let index = i % pieces.value.length;
          indexCanMoveHandler(index);
          let piece = pieces.value[index];
          if (piece.name) {
            break;
          }
        }
        // 判断是否过圆心下子,如果是,则可以移动到中心,或者对面
        if (connectCenterIndexs.value.includes(selectedIndex.value)) {
          if (
            !centerPiece.value.name ||
            (centerPiece.value.name &&
              isRivalCamp(draging.piece, centerPiece.value) &&
              isRestriction(draging.piece, centerPiece.value))
          ) {
            centerPiece.value.canMove = true;
          }
          if (!centerPiece.value.name) {
            let oppositeIndex = getOppositeIndex(selectedIndex.value);
            indexCanMoveHandler(oppositeIndex);
          }
        }
      }
    } else {
      // 第一次上子,不可下到中心位置
      pieces.value.forEach((piece, index) => {
        if (!piece.name) {
          canMoveIndexs.value.push(index);
          piece.canMove = true;
        }
      });
    }
  } else {
    // 未选择,则取消
    canMoveIndexs.value = [];
    centerPiece.value.canMove = false;
    pieces.value.forEach((piece, index) => {
      piece.canMove = false;
    });
  }
});

最后对弈通信,这里通过websocket进行对应的通信,用到了socket.io库

 // 对弈数据交换逻辑
 async exchange() {
    const { ctx, app, service, logger } = this
    const nsp = app.io.of('/wuxing')
    const socket = ctx.socket
    const currentId = ctx.socket.id
    let startTime = 0

    // 获取客户端传来的数据包
    let message = ctx.args[0] || {}
    if (typeof message === 'string') {
      message = JSON.parse(message)
    }
    const { target, payload } = message.target
    switch (message.action) {
      case ExchangeEvent.PLAYER_STATE: 
          // 更新玩家状态
          if(payload.state) {
            let stateData =  { state: payload.state }
            const updatePlayerInfo = await service.wuxing.player.updatePlayerInfo(currentId, stateData)
          } 
          // 更新玩家列表
          const players = await service.wuxing.player.getPlayerList()
          const msg = ctx.helper.parseMsg(ExchangeEvent.PLAYER_LIST, {players}, { target })
          nsp.to(hall).emit('exchange', JSON.stringify(msg)) // 往大厅房间,发送信息
        }
        break
      case ExchangeEvent.JOIN:
          // 加入游戏
          break;
      case ExchangeEvent.START:
          // 开始游戏
          break;
      // 其他游戏状态交互操作
      ....

Socket.IO直通车

需要提前了解socket.io中定义命名空间,房间,socket之间的关系

深入理解 Socket.io 中的 Room 与 Namespace

比如一进来,所有人都会加大厅的房间,用于同步玩家的状态,同时显示玩家列表,而进入游戏则创建一个单独的房间用游戏双方的游戏通信。 类似于聊天室的群聊和私聊的区别

另外,由于我这边后端用的是egg.js框架,线上环境会开启多个进程,sticky 模式, 对应socket.io Using multiple nodes | Socket.IO

技术栈

前端:vite, vue3, vueuse

后端:egg.js, socket.io, redis

优化点

人机模式(没写过🤯)

排行榜(未加入用户系统)

参考

阴阳五行

汉字 Wordle

总结

好想法是很难得的。

我这个小游戏也是在老祖宗阴阳五行的理论下,做的微微创新而已。

技术实现并不难,但布局样式费了不少时间,交互逻辑清晰很重要。有想法就去实现。

最后,欢迎各位路过客官瞧一瞧,看一看

欢迎体验吐槽

游戏体验地址:阴阳五行转