第1章:构建一个点击游戏

对于这个项目,我们将一起构建一个 点击游戏,完全集成到 Odoo 中。在这个游戏中,目标是积累大量的点击次数,并自动化系统。有趣的是,我们将使用 Odoo 用户界面作为我们的游乐场。例如,我们将在网页客户端的某些随机部分隐藏奖励。

要开始,请确保你有一个正在运行的 Odoo 服务器和一个开发环境。在进入练习之前,请确保你已经按照此 教程简介 中描述的所有步骤进行了操作。

目标

../../../_images/final.png

每个章节练习的解决方案都托管在 官方 Odoo 教程仓库 中。

1. 创建系统托盘项

要开始,我们想在系统托盘中显示一个计数器。

  1. 创建一个 clicker_systray_item.js`(以及 `xml)文件,其中包含一个 hello world 的 Owl 组件。

  2. 将其注册到系统托盘注册表,并确保它是可见的。

  3. 将条目的内容更新为显示以下字符串:点击次数:0,并在右侧添加一个按钮以增加该值。

../../../_images/systray.png

就这样,我们完成了一个完全运行的点击游戏!

2. 统计外部点击次数

说实话,这还不是很有趣。那么,让我们添加一个新功能:我们希望统计用户界面中的所有点击,这样用户就会有动力尽可能多地使用 Odoo!但显然,有意点击主计数器的次数应该算得更多。

  1. 使用 useExternalListener 来监听 document.body 上的所有点击事件。

  2. 每次点击都应该使计数器的值增加 1。

  3. 将代码修改为:每次点击计数器时,数值增加 10

  4. 确保单击计数器时,值不会增加 11!

  5. 另外一项挑战:确保外部监听器能够捕获事件,以免遗漏任何点击。

3. 创建客户端动作

目前,当前用户界面非常小:它只是一个系统托盘项。我们显然需要更多的空间来显示更多的游戏内容。为此,让我们创建一个客户端动作。客户端动作是一个由网页客户端管理的主要动作,用于显示一个组件。

  1. 创建一个 client_action.js`(和 `xml)文件,包含一个“你好,世界”组件。

  2. 在动作注册表中以名称 awesome_clicker.client_action 注册该客户端动作

  3. 在系统托盘项上添加一个文本为 打开 的按钮。单击该按钮应通过动作服务打开客户端动作 awesome_clicker.client_action

  4. 为了避免干扰员工的工作流,我们更希望客户端动作在弹出窗口中打开,而不是全屏模式。修改 doAction 调用以在弹出窗口中打开。

    小技巧

    你可以在 doAction 中使用 target: "new",以在弹出窗口中打开该动作:

    {
       type: "ir.actions.client",
       tag: "awesome_clicker.client_action",
       target: "new",
       name: "Clicker"
    }
    
../../../_images/client_action.png

4. 将状态移动到服务

目前,我们的客户端动作只是一个“你好,世界”组件。我们希望它能显示我们的游戏状态,但当前该游戏状态仅在系统托盘项中可用。这意味着我们需要更改状态的存储位置,以便所有组件都能访问它。这是一个使用服务的绝佳场景。

  1. 创建一个 clicker_service.js 文件并添加相应的服务。

  2. 此服务应导出一个响应式值(点击次数)以及几个用于更新它的函数:

    const state = reactive({ clicks: 0 });
    ...
    return {
       state,
       increment(inc) {
          state.clicks += inc
       }
    };
    
  3. 在系统托盘项和客户端动作中访问状态(别忘了 useState)。修改系统托盘项以移除其自身的本地状态并使用它。同时,你也可以删除 +10 clicks 按钮。

  4. 在客户端动作中显示状态,并在其添加一个 +10 点击按钮。

../../../_images/increment_button.png

另请参见

5. 使用自定义钩子

目前,所有需要使用我们点击器服务的代码部分都必须导入 useServiceuseState。由于这非常常见,让我们使用一个自定义钩子。同时,这样也能更强调 clicker 部分,而减少对 service 部分的强调。

  1. 导出 useClicker 钩子。

  2. 将所有对点击器服务的现有使用更新为新的钩子:

    this.clicker = useClicker();
    

另请参见

6. 使显示的值更人性化

我们将来会显示大数字,所以现在就做好准备吧。有一个 humanNumber 函数,可以以更易于理解的方式格式化数字:例如,1234 可以格式化为 1.2k

  1. 用于在系统托盘项和客户端动作中显示我们的计数器。

  2. 创建一个 ClickValue 组件来显示值。

    注解

    Owl 允许仅包含文本节点的组件!

../../../_images/humanized_number.png

7. 在 ClickValue 组件中添加提示信息

使用 humanNumber 函数时,我们在界面上实际上丢失了一些精度。让我们将实际数字作为提示信息显示。

  1. 提示需要一个 HTML 元素。将 ClickValue 修改为在 <span/> 元素中包裹该值

  2. 为动态添加 data-tooltip 属性以显示确切值。

../../../_images/humanized_tooltip.png

8. 购买 ClickBots

让我们让游戏更加有趣:当玩家第一次获得1000次点击时,游戏应该解锁一个新功能:玩家可以用1000次点击购买机器人。这些机器人每10秒会产生10次点击。

  1. 在我们的状态中添加一个 level 数字。这是一个会在某些里程碑处递增的数字,并会开启新的功能。

  2. 在我们的状态中添加一个 clickBots 数字,它表示已购买的机器人的数量。

  3. 修改客户端动作,仅当 level >= 1 时显示点击机器人的数量,并显示一个“购买”按钮,该按钮在 clicks >= 1000 时启用。“购买”按钮应将点击机器人的数量增加 1。

  4. 在服务中设置一个 10 秒的间隔,该间隔将每秒增加点击次数 10*clickBots

  5. 确保如果玩家点击次数不足,购买按钮处于禁用状态。

../../../_images/clickbot.png

9. 重构为类模型

当前的代码采用了一种较为函数式的风格。但为了实现这一点,我们必须在我们的点击器对象中导出状态及其所有更新函数。随着这个项目规模的增长,这可能会变得越来越复杂。为了简化它,让我们将业务逻辑从服务中分离出来,放入一个类中。

  1. 创建一个 clicker_model 文件,该文件导出一个响应式类。将服务中的所有状态和更新函数移动到模型中。

    小技巧

    你可以通过从 @web/core/utils/reactive 导入 Reactive 类来扩展 ClickerModelReactive 类会将模型包装成一个响应式代理。

  2. 重写点击器服务,以实例化并导出点击器模型类。

10. 当达到里程碑时发送通知

当达到 1000 次点击时,我们并没有得到太多反馈说明发生了变化。让我们使用 effect 服务来清晰地传达这一信息。问题是,我们的点击模型无法访问服务。此外,我们希望尽可能将用户界面(UI)相关的逻辑从模型中分离出来。因此,我们可以探索一种新的通信策略:事件总线(event buses)。

  1. 更新点击器模型,以实例化一个 bus,并在首次达到 1000 次点击时触发 MILESTONE_1k 事件。

  2. 将点击器服务更改为在模型总线中监听相同的事件。

  3. 当发生这种情况时,使用 effect 服务显示一个彩虹人。

  4. 添加一些文字以说明用户现在可以购买点击机器人。

../../../_images/milestone.png

11. 添加BigBots

显然,我们需要一种方式为玩家提供更多选择。让我们添加一种新的点击机器人:BigBots,它们更为强大:每10秒提供100次点击,但它们的成本是5000次点击。

  1. 当达到 5k 时增加 `level`(应为 2)

  2. 将状态更新以跟踪 bigbots

  3. bigbots 应在 level >=2 时可用

  4. 在客户端动作中显示对应的信息

小技巧

如果你需要在模板中使用 <> 作为 JavaScript 表达式,请注意这可能会与 XML 解析器冲突。为了解决这个问题,可以使用以下特殊别名:gt, gte, ltlte。有关更多信息,请参阅 Owl 模板表达式文档页面

../../../_images/bigbot.png

12. 添加一种新的资源类型:电力

现在,要添加另一个缩放点,让我们添加一种新的资源:一个力量倍数。这是一个可以在 level >= 3 时增加的数字,它会乘以机器人的动作(也就是说,点击机器人现在会给我们提供 multiplier 次点击)。

  1. 当达到 100k 时增加 `level`(应为 3)。

  2. 将状态更新为跟踪电源(初始值为 1)。

  3. 将机器人改为使用该数字作为乘数。

  4. 将用户界面更新为显示并让用户购买新的威力等级(费用:50,000)。

../../../_images/bigbot.png

13. 定义一些随机奖励

我们希望用户有时能获得奖励,以鼓励使用 Odoo。

  1. click_rewards.js 中定义一个奖励列表。奖励是一个包含以下内容的对象: - 一个 description 字符串。 - 一个 apply 函数,该函数接收游戏状态作为参数,并可以对其进行修改。 - 一个可选的 minLevel 数字,表示该奖励在哪个解锁等级时可用。 - 一个可选的 maxLevel 数字,表示该奖励在哪个解锁等级后不再可用。

    例如:

    export const rewards = [
       {
          description: "Get 1 click bot",
          apply(clicker) {
                clicker.increment(1);
          },
          maxLevel: 3,
       },
       {
          description: "Get 10 click bot",
          apply(clicker) {
                clicker.increment(10);
          },
          minLevel: 3,
          maxLevel: 4,
       },
       {
          description: "Increase bot power!",
          apply(clicker) {
                clicker.multipler += 1;
          },
          minLevel: 3,
       },
    ];
    

    你可以将任何内容添加到该列表中!

  2. 定义一个函数 getReward,该函数将从与当前解锁等级匹配的奖励列表中选择一个随机奖励。

  3. 在函数 choose 中提取从数组中随机选择的代码,你可以将其移动到另一个 utils.js 文件中。

14. 打开表单视图时提供奖励

  1. 修补表单控制器。每次创建表单控制器时,它应该随机决定(1% 的概率)是否给予奖励。

  2. 如果答案是肯定的,在模型上调用方法 getReward

  3. 该方法应选择一个奖励,发送一条置顶通知,其中包含一个 领取 按钮,用于随后应用奖励,最后应打开 点击器 客户端动作。

../../../_images/reward.png

15. 在命令调色板中添加命令

  1. 在命令调色板中添加命令 Open Clicker Game

  2. 添加另一个命令:购买 1 个点击机器人

../../../_images/command_palette.png

16. 添加另一个资源:树木

现在是时候引入一种全新的资源类型了。这里有一个应该不会引起争议的例子:树木。我们现在将允许用户种植(收集?)果树。一棵树需要100万次点击,但它会为我们提供水果(可能是梨或樱桃)。

  1. 将状态更新为跟踪各种类型的树:梨树/樱桃树及其果实。

  2. 添加一个计算总树木数和果实数的函数。

  3. 点击次数 >= 1 000 000 时定义一个新的解锁等级。

  4. 更新客户端用户界面,以显示树木和果实的数量,并且可以购买树木。

  5. 每 30 秒为每棵树增加 1 个水果。

../../../_images/trees.png

17. 为系统托盘项使用下拉菜单

我们的游戏开始变得有趣了。但目前,系统托盘只显示点击总数。我们希望看到更多信息:总共有多少棵树和果实。此外,能够快速访问一些命令和更多信息也会很有帮助。让我们使用一个下拉菜单吧!

  1. 将系统托盘项替换为下拉菜单。

  2. 它应该显示点击次数、树木和果实的数量,每个都带有漂亮的图标。

  3. 点击它应该会打开一个下拉菜单,显示更详细的信息:每种树和水果的类型。

  4. 另外,还有一些下拉菜单项,包含一些命令:打开点击器游戏、购买一个点击机器人,…

../../../_images/dropdown.png

18. 使用 Notebook 组件

我们现在跟踪了更多的信息。让我们通过使用 Notebook 组件,将信息和功能组织到不同的选项卡中,以改进我们的客户端界面:

  1. 使用 Notebook 组件。

  2. 所有 click 内容应在同一个标签页中显示。

  3. 所有 tree/fruits 内容应在另一个标签页中显示。

../../../_images/notebook.png

19. 保存游戏状态

你一定注意到了我们游戏中一个明显的问题:它是临时的。每次用户关闭浏览器标签页时,游戏状态都会丢失。让我们来修复这个问题。我们将使用本地存储来持久化保存游戏状态。

  1. 导入 browser@web/core/browser/browser 以访问本地存储。

  2. 每 10 秒序列化一次状态(在相同的间隔代码中),并将其存储在本地存储中。

  3. clicker 服务启动时,它应该从本地存储(如果有的话)加载状态,否则进行初始化。

20. 引入状态迁移系统

一旦你将状态保存到某个地方,就会出现一个新的问题:当你更新代码时,状态的结构可能会发生变化,而用户使用旧版本创建的状态打开浏览器时会发生什么呢?欢迎进入迁移问题的世界!

可能尽早解决这个问题是明智的。在这里,我们将向状态中添加一个版本号,并引入一种机制,如果状态不是最新的,该机制会自动更新状态。

  1. 将版本号添加到状态中。

  2. 定义一个(空的)迁移列表。迁移是一个包含 fromVersion 数字、toVersion 数字和 apply 函数的对象。

  3. 每当代码从本地存储加载状态时,都应该检查版本号。如果状态不是最新的,应该应用所有必要的迁移。

21. 添加另一种类型的树

为了测试我们的迁移系统,让我们添加一种新的树类型:桃树。

  1. 添加 peach 树。

  2. 增加状态版本号。

  3. 定义一个迁移。

../../../_images/peach_tree.png