likes
comments
collection

给羊了个羊做一个地图编辑器,顺便实现了个状态库

作者站长头像
站长
· 阅读数 12

在上一篇文章中我们实现了一个基本的羊了个羊游戏,这次我们在原来的基础上增加地图编辑器的功能 juejin.cn/post/714572…

先上例子🌰

给羊了个羊做一个地图编辑器,顺便实现了个状态库

效果图如上

需求分析

  • 地图编辑器(上图左侧):
    • UI:一个网格矩阵,每个勾选的格子(红色)表示卡牌放置的位置
    • 保存地图: 保存当前数据到本地,并且触发游戏区渲染
    • 清除缓存:清除已编辑的地图内容
  • 游戏区:
    • 🐑了个羊基本玩法

    • 根据地图编辑器的数据渲染出地图

    • 游戏是三张牌触发消除,所以地图也画3层(3的倍数即可)

实现原理

地图编辑器

  • 数据模型

首先定义网格的数据模型,这里使用一个二位数组来表示网格,0表示空,1表示勾选

interface IViewState {
  blocks: number[][] // 0 / 1
}

初始化数据,默认全部填充0

const defaultState: () => IViewState = () => {
  return {
    blocks: Array(25).fill(1).map(() => {
      return Array(25).fill(0)
    }),
  };
};
  • 画点方法

点击矩阵之后,需要填充对应的格子,这里使用一个draw方法实现

draw(x: number, y: number) {
  this.setState(s => {
    s.blocks[x][y] = s.blocks[x][y] === 0? 1 : 0
  })
}
  • 渲染层
    • 直接双层循环渲染网格即可,在对应的网格上绑定draw方法

{editorP.state.blocks.map((u, x) => {
  return <div className='editor-row'>{
    u.map((v, y) => {
      return <div onClick={() => {
        editorP.draw(x, y)
      }} className={`editor-block ${v ? 'selected' : ''}`}></div>;
    })
  }</div>;
})}

具体源码github.com/lulusir/mal…

游戏区

  • 游戏的基本逻辑在上一篇中有描述,这里就不在重复赘述,感兴趣的可以看看juejin.cn/post/714572…

  • 渲染卡片initCards

根据地图编辑器的数据来初始化游戏区需要的数据,具体逻辑看下面代码👇🏻

  initCards(blocks = mockBlocks) {
    const levels = 3 // 需要是3的倍数

    // 已勾选的网格
    let n = 0
    blocks.forEach(u => {
      u.forEach(v => {
        if (v === 1) {
          n += 1
        }
      })
    })
    let nums = n * levels

    // 卡片的类型
    const types = Array(nums / levels)
      .fill(0)
      .map((_, i) => {
        return i % 7;
      });
    // 生成基本卡组
    const cards: Card[] = [];
    for (let i = 0; i < nums; i++) {
      const element = new Card(types[i % (nums / levels)]);
      cards.push(element);
    }

    // 洗牌
    const data = this.shuffle(cards);

    function getCard() {
      const c = data.pop();
      return c;
    }
    // 初始化层级关系
    for (let i = 0; i < levels; i++) {
      this.layer.init(
        blocks.map(v => {
          return v.map(u => {
            return u === 1 ? getCard() : 0;
          })
        }),
        i * 4,
        i * 6,
        levelZ(i + 1),
      );
    }
    this.allCard = cards;
  }

状态管理

  • 基类只有一个update方法,在useClass调用之后会覆写

class ForceUpdateState {
  update() {}
}
  • 传入一个ForceUpdateState实例,给这个实例绑定update的方法,来达到触发页面渲染的功能

function useClass<T extends ForceUpdateState>(instance: T) {
  const [_, update] = useState(0)
  const ref = useRef(instance)

  ref.current.update = useCallback(
    () => {
      update(s => s + 1)
    },
    [],
  )
  return ref.current
}

使用方式,需要先实例化对象,然后在react组件中使用useClass获取实例

class GamePresenter extends ForceUpdateState {...}

const gamePresenter = (new GamePresenter(new CardSlot(), new Layer()))

.
.
.

export default function HomePage() {
  const p = useClass<GamePresenter>(gamePresenter)

.
.
.

当然,如果使用clean-js来管理状态,那就更加简洁方便了,还支持依赖注入功能, 具体API参考lulusir.github.io/clean-js/ap…

@injectable()
export class GamePresenter extends Presenter<IViewState> {
  constructor(public slot: CardSlot, public layer: Layer) {
    super();
    this.state = defaultState();
  }

.
.
.
export default function HomePage() {
 const { p, s } = usePresenter(GamePresenter);

github.com/lulusir/cle…

github.com/lulusir/mal… 期待宝子们的star⭐️


其他文章 什么?在React中也可以使用vue响应式状态管理 clean-js | 自从写了这个辅助库,我已经很久没有加过班了… clean-js | 在hooks的时代下,使用class管理你的状态 clean-js | 手把手教你写一个羊了个羊麻将版 写给前端的数据库入门 | 序 写给前端的数据库入门 | docker & 数据库 有没有一种可能,你从来都没有真正理解async 三分钟实现前端写JAVA这件事——装环境 三分钟实现前端写JAVA这件事——VS code