分类 软件开发 下的文章

在完成了产品的基础开发以后,接下来需要进行一些周边的工作,这些周边工具将会帮助下一步优化产品。

为什么要加应用统计和 Crash 收集

不少开发者在开发的时候,很少会意识到需要添加应用统计和 Crash 收集。但对于一个合格的应用来说,这些是必须的。

  • 应用统计:应用统计会在后续进行产品迭代的时候给予数据的支持,让能够明确为什么要更新、要更新什么以及为什么这么做。
  • Crash 收集:人无完人,很难开发出一个完美的应用,就随时有可能会出现应用报错的情况。出现报错以后由于用户的水平不同,有效的反馈其实很少。Crash 收集则可以帮助收集必要的 Crash 信息,从而在后续开发的过程中,有针对性的修复 Bug。

应该使用哪些工具?

在应用统计领域,可选项非常多,大部分人使用的是 Google Analytics ,不过由于这个产品的面向用户主要是国内的用户,因此我更倾向选择加载速度更快的产品,最终我选择的是来自腾讯的移动应用分析(MTA),腾讯的移动应用分析中,提供了 HTML5 产品的接入,因此可以完成 TLDR 的统计。

在 Crash 收集方面,大家用的比较普遍的是 Sentry.io ,不过因为 Linux 中国并没有足够多的产品业务需要使用 Sentry 来收集 Crash ,因此,一直使用的是官网的免费使用版本。同样因为网络加载速度的原因,选择使用了国内的竞品 —— FunDebug

接入工具

腾讯移动应用分析

腾讯移动应用分析的接入并不复杂,首先,你需要登陆其官网,创建一个 HTML5 应用。

并在创建完成后,根据你自己的需要配置相应的能力,这里我开启了所有的数据统计,用以支持后续的产品迭代决策。

配置完成后,你会获得具体的统计的代码,接下来就可以进行接入。

一个比较简单的方法是直接将代码复制,并粘贴到 public/index.html 中,从而实现统计。不过,这样嵌入会导致如果需要自定义统计时,会无法通过 ESLint 的规则,因此我选择了第二种方式,使用 Vue 插件的方式接入。

使用 Vue 插件接入时,需要使用 mars-mta 这个包。

先使用 npm 安装依赖,然后在 main.js 中加入相应的统计代码,就可以实现自动的统计。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify';

// 以下为新增代码
import Mars from 'mars-mta'
Vue.use(Mars, {
    open: true, // 开关,若为false,则不会发出上报
    config: {
      sid: '500710182', // 必填,统计用的appid
      cid: '500710183', // 如果开启自定义事件,此项目为必填,否则不填
      autoReport: 1, // 是否开启自动上报(1:init完成则上报一次,0:使用pgv方法才上报)
      senseHash: 1, // hash锚点是否进入url统计
      senseQuery: 1, // url参数是否进入url统计
      performanceMonitor: 1, // 是否开启性能监控
    }
})
// 以上为新增代码

new Vue({
  router,
  vuetify,
  render: h => h(App),
  beforeCreate: async function(){
        const auth = this.$tcb.auth({ persistence: 'local' });
        await auth.signInAnonymously();
  }
}).$mount('#app')

在我的代码中,配置了 sidcid ,这些信息你都需要在腾讯 MTA 的应用管理后台获取。

而下方的配置,则根据你自己的实际需求选择开启即可。

对应的提交:https://github.com/LCTT/tldr.linux.cn/commit/61821aff4bf75fda3e81d96c6cd34a51efd00773

FunDebug

Fundebug 是我之前在开发小程序的时候用过的 Crash 收集应用。这次刚好也用上了。

因为预算的问题,这里我使用的是免费版本,有几个 tab 是看不到的,但是基本的 Debug 也是够用了。

Fundebug 的安装也很简单, 访问 https://www.fundebug.com/ ,注册账号, 并创建一个项目,你会获得一个 API Key,后续你可以使用这个 API Key 来初始化你的项目。

执行如下命令来安装依赖:

npm install fundebug-javascript fundebug-vue --save

并在 main.js 中添加如下代码(将 API-KEY 替换为你自己的 API KEY)并保存,就可以引入 Fundebug 来进行 Crash 收集了。

import * as fundebug from "fundebug-javascript";
import fundebugVue from "fundebug-vue";
fundebug.init({
    apikey: "API-KEY"
})
fundebugVue(fundebug, Vue);

对应的提交:https://github.com/LCTT/tldr.linux.cn/commit/225ca9d38e80eb55defac6383f5b9c228bdab6fe

优化

在开发的过程中经常会出现 Error,这个是难以避免的。在开启了 Crash 收集以后,这些 ERROR 也会被收集到 Fundebug 上,这样会浪费每个月 3000 条的免费额度,因此,需要一个方法在开发环境不启用这些拓展。类似的,也不希望 MTA 统计本地开发的 Page View ,会影响到后续的数据分析。因此,我使用了一些方法来避开这个问题。

if (process.env.NODE_ENV === 'production') {
    Vue.use(Mars, {
      open: true, // 开关,若为false,则不会发出上报
      config: {
        sid: 'xxx', // 必填,统计用的appid
      }
  })
  fundebug.apikey = "xxx"
  fundebugVue(fundebug, Vue);
}

通过将引用统计的代码包裹在环境的判断代码中,可以实现在渲染的时候,只有生产环境才会渲染出相应的统计代码,从而实现了在开发环境不调用 fundebug 和 mta 统计,避免了开发环境的数据干扰。

对应的提交:https://github.com/LCTT/tldr.linux.cn/commit/62f87b51fabd7c25cd905560157a546fd62babf2

总结

在这篇文章中,介绍了两个服务,分别是用于统计的腾讯移动分析 MTA 和用于做 Crash 收集的 fundebug,介绍了应该如何在 Vue 应用中接入这两种服务。此外,还根据实际的需求,优化了两个统计的位置,确保产品在开发环境不会工作,从而避免了影响到我们统计数据的准确性。

这篇文章涉及到的代码你都可以在 https://github.com/LCTT/tldr.linux.cn/blob/master/src/main.js 看到。

介绍

在完成了界面的实现后,接下来可以开始进行和云开发相关的数据对接。完成数据对接后,应用基础就打好了,接下来的就是发布上线以及一些小的功能的加入。

配置

在进行相关的配置调用的时候,你需要先登录腾讯云控制台,进行一些配置。使用你的小程序账号登录腾讯云,并在其中找到“云开发”产品。进入到“产品控制台”。

在“产品控制台”中找到你的环境,点击进入“详情页”:

在环境“详情页面”选择“用户管理”->“登录设置”->“匿名登录”:

启用匿名登录。

云开发的数据查询目前必须登录后才可以查询,因为希望给用户提供的是免登录的解决方案,因此,必须开通匿名登录,确保可以进行数据查询。

由于需要在网页中调用相应的函数,因此,也需要在同一个页面的 WEB 安全域名中添加应用的上线域名(本地调试用的 localhost 无需添加)。

为应用程序添加匿名登录的逻辑

此部分代码位置:https://github.com/LCTT/tldr.linux.cn/blob/master/src/main.js

由于希望用户可以打开网页就可以查询数据,因此,必须在用户无感的情况下,完成匿名登录逻辑。

根据对 Vue 生命周期的预研,将相应的逻辑放在了 beforeCreate 中,确保在应用初始化完成后,就可以自动完成匿名登录。

具体实现代码如下:

// main.js
new Vue({
  router,
  vuetify,
  render: h => h(App),
  beforeCreate: function(){                   // 新增匿名登陆逻辑
        const auth = this.$tcb.auth();        // 新增匿名登陆逻辑
        auth.signInAnonymously();             // 新增匿名登陆逻辑
  }                                           // 新增匿名登陆逻辑
}).$mount('#app')

加入完成后,你可以使用云开发的数据库等命令,来完成相应的数据库调用,验证自己的调用是否正常。

在这里需要注意,由于 Vue 默认的 ESLint 规则限制,默认是无法在 Vue 项目代码中使用 console.log 的,你需要使用一些命令来跳过相应的检查 只需要在你需要打印变量的前一行加入 // eslint-disable-next-line 就可以避免对应的检查了。

调用数据

此部分代码位置:https://github.com/LCTT/tldr.linux.cn/blob/master/src/views/Result.vue

完成初始化后,就需要完成相应的数据调用,这里不再针对每一个进行讲解,选择一个例子来说明。

const db = this.$tcb.database();
const cmd = db.collection('command');
if(this.id){ // 这里的 id 是 props 传入的参数,为命令对应的 doc id
    cmd.doc(this.id).get().then(res => {
      this.command = res.data
    })
    }else{
    cmd.where({
      name: this.$route.params.cmd // 命令可以从 Route 中获取,但实际场景下,因为开启了 `props: true`,也可以直接从 props 中获取。
    }).limit(1).get().then(res => {
      this.command = res.data[0]
    }).catch((err) => {
      alert("命令查询出错,请联系我们")
      // eslint-disable-next-line
      console.error(err)
    })
}

在这段代码中,首先是前期挂载的 $tcb 中抽取 database() 对象,并基于 database() 对象构建 collection() 对象。 然后就是使用 collection() 对象进行查询。

由于这里涉及到不同的页面逻辑,使用了一个 if 来判断数据。上下两种分别是获取单个数据和使用多个数据的方法。获取到数据以后,将数据更新,同步到 Vue 的 Data 中,完成相应的逻辑的调用。

云开发登录的坑

此部分代码位置:https://github.com/LCTT/tldr.linux.cn/blob/master/src/main.js

由于为用户提供的是快速查询功能,因此希望用户无论何时都是无感进行查询的。但实际测试的时候,发现用户如果直接通过命令行登录的时候,会导致报错。根据控制台返回的信息来看,是用户登录状态尚未完成,就进行了数据查询。

通过查询云开发的文档,发现云开发的 auth 对象在登录的时候,可以传入一个 persistence 参数来控制身份信息的持久化。

由于默认使用的是会话 ,所以导致用户的登录态丢失。为了确保应用的状态正常进行,将 persistence 设置为 local,确保应用在一次登录后可以将用户登录状态下放到用户的存储 中,这样可以避免用户总是会遭遇请求失败的问题。

// main.js
new Vue({
  router,
  vuetify,
  render: h => h(App),
  beforeCreate: async function(){
        const auth = this.$tcb.auth({ persistence: 'local' });
        await auth.signInAnonymously();
  }
}).$mount('#app')

总结

在实际开发中,如果你需要通过云开发的 Web SDK 调用相应的数据,则需要先行开启云开发的匿名登录并配置 Web 安全域名;在数据调用的部分和在小程序端调用云开发没有太大的区别;并通过设置 presistence 设置搞定了登录状态丢失的问题。

工作量分析

在我们进行这部分开发的时候,接下来我们需要进行相应的功能安排和分类。

简单看来,我需要开发 3 个页面:

  1. 首页:首页负责用户默认访问。
  2. 列表页:列表页面则是在搜索过程中,如果有多个结果,则进入到列表页面。如果有单个结果,则应该进入到详情页面。
  3. 结果页:结果页负责显示命令的具体的翻译结果。

根据实际的工作拆分组件的化,我需要有一个 Layout 组件来负责整体的页面的环境渲染。但是,考虑到组件的复用,于是决定将首页的 Title 进行优化,使首页和详情页保持一致。

在新版的布局情况下,我就可以将顶部的的 title 和底部的 Link 放在最外侧的组件中。

创建 Router & Page

在思考情况后,接下来我们来创建 Router 和 Page。首先,删除 views 下的 About.vue(因为这个页面我们不需要)。然后创建 List.vueResult.vue ,用作后续的开发准备。

创建完成后,修改 router/index.js 中的 routes 部分:

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
  },
  {
    path: '/list/:cmd',
    name: 'list',
    component: () => import(/* webpackChunkName: "list" */ '../views/List.vue')
  },
  {
    path: '/cmd/:cmd',
    name: 'command',
    component: () => import(/* webpackChunkName: "cmd" */ '../views/Result.vue')
  }
]

完成定义后,我们就可以通过形如 https://tldr.linux.cn/list/ls和https://tldr.linux.cn/cmd/ls 这样的方式来访问具体的命令了。

定义页面

接下来需要编写 Home、List 和 Result 这三个页面。由于这三个页面在内容方面没有太多的可以借鉴的点,所以我们更多关注于使用页面中 <script> 的部分。

<template>
  <div class="home">
        <v-text-field
        v-model="cmd"
        @keydown="onKeyDown"
        autofocus
        ></v-text-field>        
  </div>
</template>

<script>
export default {
  name: 'home',
  data:function(){
    return {
      cmd:""
    }
  },
  methods:{
    onKeyDown: function(e)  {
     
    }
  },
  computed:{
    isLoaded:function(){
        return !this.loaded
    }
  }
}
</script>

上述代码是我在三个页面几乎都会使用到的结构,删除其中的一些无用的代码以后,基本上在每个页面都可以看到。这里我重点说一下其中的一些比较特殊的用法。

首先第一个是在 v-text-field 上加入的 @keydown="onKeyDown" 这个绑定,这个绑定将会帮助实现用户点击回车以后,自动触发事件。这样在用户输入完命令后,按下回车就自动执行后续的操作,而不需要再移动鼠标指针去点击按钮启动搜索。

其次,在 v-text-field 上加入了autofocus ,来实现进入页面后,自动为输入框加入focus,从而实现页面加载完成后,用户就可以输入命令。

这样的一些配置,可以让用户的体验做到最好

除此之外,我还用到了 computed ,来做数据调整,确保我可以控制内容。

一些小的特性的点

使用骨架图来优化体验

由于我们的应用在列表页面和详情页面存在数据的查询时间,为了让应用在加载的时候,不会因为加载中而退出页面,我加入了 v-skeleton-loader 组件,这样用户在数据查询的时候,看骨架图来缓解用户的焦虑。

在组件层面,我配置了 v-if 来做显示的控制,并将 type 设置为 card,article,card,article 来实现多样化的组件加载支持。

 <v-skeleton-loader
    v-if="isLoaded"
    type="card,article,card,article"
    min-height="800"
    ></v-skeleton-loader>

总结

在这一部分中,借助 Vue 的 methodonkeydowncomputed 实现了页面基本逻辑的构建。并借助 Vuetify 的一些基础组件来构建页面。

在这一部分,我想告诉大家的更多是在 UI 的部分,我们在做的时候不仅仅需要考虑的是界面,更多还需要考虑在 UX 侧体验的优化,组件库提供给我们的配置项目,可以优化产品体验。

“Linux” 小程序是 Linux 中国在 2019 年 2 月(恰恰是去年春节)发布的小程序,该小程序采用众包模式,对著名的 TLDR 项目中的 Linux 命令示例进行了翻译,并通过微信小程序的形式展现。在 2020 年的春节,我们面向更多的 PC 端用户,开放了 Web 版的 Linux 命令查询。Web 版和小程序版数据同步,让你在使用桌面计算机时也可以进行命令查询。

从今天起,我们将在 Linux 中国官网和公众号上,连载 TL;DR Web 应用开发背后的故事。接下来,请看来自开发 gg 的文章。

背景描述

Linux 中国曾在过去的一年开发和运行了一个中国版 TL;DR 客户端。不过当时做的版本是小程序的版本,一直以来,受限于小程序·云开发没有 Web SDK ,因此无法将应用能力迁移到更多的平台上。刚好最近云开发提供了 Web SDK,并已经逐步成熟,于是我们便可以借此机会,将业务实现 PC 化,服务更多人群。

在开发出初步的版本之后,我们决定以这个项目作为范例,将我们开发经验分享给大家,大家可以参考开发自己的云应用。

项目设计

在进行项目开发时,先对项目进行了基本的 UI 设计:

主页

详情页

这里用到的是 balsamiq 的手绘线框图来完成产品设计,以避免我个人过度追求完美,而让产品延期迟迟不能上线的问题(这样的事情在历史上发生了非常多次)

技术选项

由于需要的是一个前端页面,因此,在技术选型方面,几乎没有太多的异议。使用最为熟悉的技术栈来完成。

  • 前端框架:Vue
  • 路由器:Vue Router
  • CSS 框架:Vuetify.js

镜像配置

因为身处国内, npm 的速度必然不太好,因此需要进行相应的镜像设定,确保 npm 和 yarn 在安装依赖。这里使用的是腾讯云提供的镜像。

# Npm 设置
npm config set registry http://mirrors.cloud.tencent.com/npm/

# yarn 设置
yarn config set registry http://mirrors.cloud.tencent.com/npm/ -g

初始化 Vue 项目

首先,需要安装 Vue CLI,以进行项目的生成,这里我已经完成安装,就不再赘述。(Vue CLI 的安装教程点击这里

执行如下命令初始化项目:

vue create tldr

等待其完成安装以后,进入项目,并启动项目。

cd tldr
yarn serve

随即,可以在浏览器中访问 localhost:8080 查看项目:

预览

记得引入 git 做版本控制,文章里就不介绍了。没意思。

安装 Vue Router

在完成 Vue 项目的初始化以后,接下来需要进行 Vue Router 的配置了。

Vue Router 的配置在引入了 Vue 3 以后,显得非常的简单,直接执行如下命令即可:

vue add router

执行过程中,会问你是否需要启用 History Mode,根据需要选取,我使用的是 History Mode。

设置完成以后,保存并重启 Vue 的开发服务器,你会在预览中看到 Router 添加的 “Home” 和 “About”。

安装 Vuetify.js

接下来安装的是 Vuetify.js ,由于框架提供了相应的支持,因此在开发时也非常简单,只需要执行如下命令就可以完成初始化。

vue add vuetify

会问你选择那种预设,直接使用 Default 即可。

保存并重启开发服务器,你会看到这样的界面,则说明配置完成。

部署测试应用

在进行下一步开发的时候,需要先进行一下项目的部署,从而获得一个测试的域名,方便后续的开发。

这里项目的开发我并没有使用云开发自己的 Web 托管 (因为我们不是按量付费套餐,所以没有办法开启),而是使用了 Now.sh 的,这里就不再过多赘述。

引入云开发 SDK

云开发提供了 Web SDK ,可以通过 npm 安装,并引用。

执行如下命令来安装:

yarn add tcb-js-sdk

安装完成后,在 main.js 中引入 tcb,并通过修改 Vue 的原型来实现挂载 Vue。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify';
const tcb = require('tcb-js-sdk')  // 新增的引入 TCB

Vue.config.productionTip = false

Vue.prototype['$tcb'] = tcb.init({ // 新增的修改原型
    env: 'prod-2c59c7'             // 新增的修改原型
})                                 // 新增的修改原型

new Vue({
  router,
  vuetify,
  render: h => h(App)
}).$mount('#app')

这样就可以在应用运行的整个周期中使用 this.$tcb 来调用云开发的相关逻辑。

总结

在完成了项目的初始化以后,回过头来看一看这在初始化项目过程中,都做了哪些事情。

  1. 配置 npm 镜像,以确保 Node 包的安装速度
  2. 使用 Vue CLI 来初始化项目
  3. 安装 Vue Router & Vuetify.js
  4. 部署应用
  5. 安装 tcb-js-sdk 以调用云开发数据

请期待本系列的下一篇关于界面开发的文章。

在本系列的第十一篇有关使用 Python Pygame 模块进行编程的文章中,显示玩家获得战利品或受到伤害时的得分。

这是仍在进行中的关于使用 Pygame 模块来在 Python 3 在创建电脑游戏的第十一部分。先前的文章是:

如果你已经跟随这一系列很久,那么已经学习了使用 Python 创建一个视频游戏所需的所有基本语法和模式。然而,它仍然缺少一个至关重要的组成部分。这一组成部分不仅仅对用 Python 编程游戏重要;不管你探究哪个计算机分支,你都必需精通:作为一个程序员,通过阅读一种语言的或库的文档来学习新的技巧。

幸运的是,你正在阅读本文的事实表明你熟悉文档。为了使你的平台类游戏更加美观,在这篇文章中,你将在游戏屏幕上添加得分和生命值显示。不过,教你如何找到一个库的功能以及如何使用这些新的功能的这节课程并没有多神秘。

在 Pygame 中显示得分

现在,既然你有了可以被玩家收集的奖励,那就有充分的理由来记录分数,以便你的玩家看到他们收集了多少奖励。你也可以跟踪玩家的生命值,以便当他们被敌人击中时会有相应结果。

你已经有了跟踪分数和生命值的变量,但是这一切都发生在后台。这篇文章教你在游戏期间在游戏屏幕上以你选择的一种字体来显示这些统计数字。

阅读文档

大多数 Python 模块都有文档,即使那些没有文档的模块,也能通过 Python 的帮助功能来进行最小的文档化。Pygame 的主页面 链接了它的文档。不过,Pygame 是一个带有很多文档的大模块,并且它的文档不像在 Opensource.com 上的文章一样,以同样易理解的(和友好的、易解释的、有用的)叙述风格来撰写的。它们是技术文档,并且列出在模块中可用的每个类和函数,各自要求的输入类型等等。如果你不适应参考代码组件描述,这可能会令人不知所措。

在烦恼于库的文档前,第一件要做的事,就是来想想你正在尝试达到的目标。在这种情况下,你想在屏幕上显示玩家的得分和生命值。

在你确定你需要的结果后,想想它需要什么的组件。你可以从变量和函数的方面考虑这一点,或者,如果你还没有自然地想到这一点,你可以进行一般性思考。你可能意识到需要一些文本来显示一个分数,你希望 Pygame 在屏幕上绘制这些文本。如果你仔细思考,你可能会意识到它与在屏幕上渲染一个玩家、奖励或一个平台并多么大的不同。

从技术上讲,你可以使用数字图形,并让 Pygame 显示这些数字图形。它不是达到你目标的最容易的方法,但是如果它是你唯一知道的方法,那么它是一个有效的方法。不过,如果你参考 Pygame 的文档,你看到列出的模块之一是 font,这是 Pygame 使得在屏幕上来使打印文本像输入文字一样容易的方法。

解密技术文档

font 文档页面以 pygame.font.init() 开始,它列出了用于初始化字体模块的函数。它由 pygame.init() 自动地调用,你已经在代码中调用了它。再强调一次,从技术上讲,你已经到达一个足够好的点。虽然你尚不知道如何做,你知道你能够使用 pygame.font 函数来在屏幕上打印文本。

然而,如果你阅读更多一些,你会找到这里还有一种更好的方法来打印字体。pygame.freetype 模块在文档中的描述方式如下:

pygame.freetype 模块是 pygame.fontpygame 模块的一个替代品,用于加载和渲染字体。它有原函数的所有功能,外加很多新的功能。

pygame.freetype 文档页面的下方,有一些示例代码:

import pygame
import pygame.freetype

你的代码应该已经导入了 Pygame,不过,请修改你的 import 语句以包含 Freetype 模块:

import pygame
import sys
import os
import pygame.freetype

在 Pygame 中使用字体

font 模块的描述中可以看出,显然 Pygame 使用一种字体(不管它的你提供的或内置到 Pygame 的默认字体)在屏幕上渲染字体。滚动浏览 pygame.freetype 文档来找到 pygame.freetype.Font 函数:

pygame.freetype.Font
从支持的字体文件中创建一个新的字体实例。

Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font

pygame.freetype.Font.name
  符合规则的字体名称。

pygame.freetype.Font.path
  字体文件路径。

pygame.freetype.Font.size
  在渲染中使用的默认点大小

这描述了如何在 Pygame 中构建一个字体“对象”。把屏幕上的一个简单对象视为一些代码属性的组合对你来说可能不太自然,但是这与你构建英雄和敌人精灵的方式非常类似。你需要一个字体文件,而不是一个图像文件。在你有一个字体文件后,你可以在你的代码中使用 pygame.freetype.Font 函数来创建一个字体对象,然后使用该对象来在屏幕上渲染文本。

因为并不是世界上的每个人的电脑上都有完全一样的字体,因此将你选择的字体与你的游戏捆绑在一起是很重要的。要捆绑字体,首先在你的游戏文件夹中创建一个新的目录,放在你为图像而创建的文件目录旁边。称其为 fonts

即使你的计算机操作系统随附了几种字体,但是将这些字体给予其他人是非法的。这看起来很奇怪,但法律就是这样运作的。如果想与你的游戏一起随附一种字体,你必需找到一种开源或知识共享的字体,以允许你随游戏一起提供该字体。

专门提供自由和合法字体的网站包括:

当你找到你喜欢的字体后,下载下来。解压缩 ZIP 或 TAR 文件,并移动 .ttf.otf 文件到你的项目目录下的 fonts 文件夹中。

你没有安装字体到你的计算机上。你只是放置字体到你游戏的 fonts 文件夹中,以便 Pygame 可以使用它。如果你想,你可以在你的计算机上安装该字体,但是没有必要。重要的是将字体放在你的游戏目录中,这样 Pygame 可以“描绘”字体到屏幕上。

如果字体文件的名称复杂且带有空格或特殊字符,只需要重新命名它即可。文件名称是完全任意的,并且对你来说,文件名称越简单,越容易将其键入你的代码中。

现在告诉 Pygame 你的字体。从文档中你知道,当你至少提供了字体文件路径给 pygame.freetype.Font 时(文档明确指出所有其余属性都是可选的),你将在返回中获得一个字体对象:

Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font

创建一个称为 myfont 的新变量来充当你在游戏中字体,并放置 Font 函数的结果到这个变量中。这个示例中使用 amazdoom.ttf 字体,但是你可以使用任何你想使用的字体。在你的设置部分放置这些代码:

font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"fonts","amazdoom.ttf")
font_size = tx
myfont = pygame.freetype.Font(font_path, font_size)

在 Pygame 中显示文本

现在你已经创建一个字体对象,你需要一个函数来绘制你想绘制到屏幕上的文本。这和你在你的游戏中绘制背景和平台是相同的原理。

首先,创建一个函数,并使用 myfont 对象来创建一些文本,设置颜色为某些 RGB 值。这必须是一个全局函数;它不属于任何具体的类:

def stats(score,health):
    myfont.render_to(world, (4, 4), "Score:"+str(score), WHITE, None, size=64)
    myfont.render_to(world, (4, 72), "Health:"+str(health), WHITE, None, size=64)

当然,你此刻已经知道,如果它不在主循环中,你的游戏将不会发生任何事,所以在文件的底部添加一个对你的 stats 函数的调用:

    for e in enemy_list:
        e.move()
    stats(player.score,player.health) # draw text
    pygame.display.flip()

尝试你的游戏。

当玩家收集奖励品时,得分会上升。当玩家被敌人击中时,生命值下降。成功!

 title=

不过,这里有一个问题。当一个玩家被敌人击中时,健康度会一路下降,这是不公平的。你刚刚发现一个非致命的错误。非致命的错误是这些在应用程序中小问题,(通常)不会阻止应用程序启动或甚至导致停止工作,但是它们要么没有意义,要么会惹恼用户。这里是如何解决这个问题的方法。

修复生命值计数

当前生命值系统的问题是,敌人接触玩家时,Pygame 时钟的每一次滴答,健康度都会减少。这意味着一个缓慢移动的敌人可能在一次遭遇中将一个玩家降低健康度至 -200 ,这不公平。当然,你可以给你的玩家一个 10000 的起始健康度得分,而不用担心它;这可以工作,并且可能没有人会注意。但是这里有一个更好的方法。

当前,你的代码侦查出一个玩家和一个敌人发生碰撞的时候。生命值问题的修复是检测两个独立的事件:什么时候玩家和敌人碰撞,并且,在它们碰撞后,什么时候它们停止碰撞。

首先,在你的玩家类中,创建一个变量来代表玩家和敌人碰撞在一起:

        self.frame = 0
        self.health = 10
        self.damage = 0

在你的 Player 类的 update 函数中,移除这块代码块:

        for enemy in enemy_hit_list:
            self.health -= 1
            #print(self.health)

并且在它的位置,只要玩家当前没有被击中,检查碰撞:

        if self.damage == 0:
            for enemy in enemy_hit_list:
                if not self.rect.contains(enemy):
                    self.damage = self.rect.colliderect(enemy)

你可能会在你删除的语句块和你刚刚添加的语句块之间看到相似之处。它们都在做相同的工作,但是新的代码更复杂。最重要的是,只有当玩家当前没有被击中时,新的代码才运行。这意味着,当一个玩家和敌人碰撞时,这些代码运行一次,而不是像以前那样一直发生碰撞。

新的代码使用两个新的 Pygame 函数。self.rect.contains 函数检查一个敌人当前是否在玩家的边界框内,并且当它是 true 时, self.rect.colliderect 设置你的新的 self.damage 变量为 1,而不管它多少次是 true

现在,即使被一个敌人击中 3 秒,对 Pygame 来说仍然看作一次击中。

我通过通读 Pygame 的文档而发现了这些函数。你没有必要一次阅读完全部的文档,并且你也没有必要阅读每个函数的每个单词。不过,花费时间在你正在使用的新的库或模块的文档上是很重要的;否则,你极有可能在重新发明轮子。不要花费一个下午的时间来尝试修改拼接一个解决方案到一些东西,而这些东西已经被你正在使用的框架的所解决。阅读文档,知悉函数,并从别人的工作中获益!

最后,添加另一个代码语句块来侦查出什么时候玩家和敌人不再接触。然后直到那时,才从玩家减少一个生命值。

        if self.damage == 1:
            idx = self.rect.collidelist(enemy_hit_list)
            if idx == -1:
                self.damage = 0   # set damage back to 0
                self.health -= 1  # subtract 1 hp

注意,只有当玩家被击中时,这个新的代码才会被触发。这意味着,在你的玩家在你的游戏世界正在探索或收集奖励时,这个代码不会运行。它仅当 self.damage 变量被激活时运行。

当代码运行时,它使用 self.rect.collidelist 来查看玩家是否仍然接触在你敌人列表中的敌人(当其未侦查到碰撞时,collidelist 返回 -1)。在它没有接触敌人时,是该处理 self.damage 的时机:通过设置 self.damage 变量回到 0 来使其无效,并减少一点生命值。

现在尝试你的游戏。

得分反应

现在,你有一个来让你的玩家知道它们分数和生命值的方法,当你的玩家达到某些里程碑时,你可以确保某些事件发生。例如,也许这里有一个特殊的恢复一些生命值的奖励项目。也许一个到达 0 生命值的玩家不得不从一个关卡的起始位置重新开始。

你可以在你的代码中检查这些事件,并且相应地操纵你的游戏世界。你已经知道该怎么做,所以请浏览文档来寻找新的技巧,并且独立地尝试这些技巧。

这里是到目前为止所有的代码:

#!/usr/bin/env python3
# draw a world
# add a player and player control
# add player movement
# add enemy and basic collision
# add platform
# add gravity
# add jumping
# add scrolling
# add loot
# add score

# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.

import pygame
import sys
import os
import pygame.freetype

'''
Objects
'''
       
class Platform(pygame.sprite.Sprite):
    # x location, y location, img width, img height, img file    
    def __init__(self,xloc,yloc,imgw,imgh,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img)).convert()
        self.image.convert_alpha()
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc

class Player(pygame.sprite.Sprite):
    '''
    Spawn a player
    '''
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.damage = 0
        self.collide_delta = 0
        self.jump_delta = 6
        self.score = 1
        self.images = []
        for i in range(1,9):
            img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect  = self.image.get_rect()

    def jump(self,platform_list):
        self.jump_delta = 0

    def gravity(self):
        self.movey += 3.2 # how fast player falls
       
        if self.rect.y > worldy and self.movey >= 0:
            self.movey = 0
            self.rect.y = worldy-ty
       
    def control(self,x,y):
        '''
        control player movement
        '''
        self.movex += x
        self.movey += y
       
    def update(self):
        '''
        Update sprite position
        '''
       
        self.rect.x = self.rect.x + self.movex
        self.rect.y = self.rect.y + self.movey

        # moving left
        if self.movex < 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[self.frame//ani]

        # moving right
        if self.movex > 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[(self.frame//ani)+4]

        # collisions
        enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        if self.damage == 0:
            for enemy in enemy_hit_list:
                if not self.rect.contains(enemy):
                    self.damage = self.rect.colliderect(enemy)

        if self.damage == 1:
            idx = self.rect.collidelist(enemy_hit_list)
            if idx == -1:
                self.damage = 0   # set damage back to 0
                self.health -= 1  # subtract 1 hp

        loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False)
        for loot in loot_hit_list:
            loot_list.remove(loot)
            self.score += 1
            print(self.score)

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.collide_delta = 0 # stop jumping
            self.movey = 0
            if self.rect.y > p.rect.y:
                self.rect.y = p.rect.y+ty
            else:
                self.rect.y = p.rect.y-ty
           
        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.movey = 0
            self.rect.y = worldy-ty-ty
            self.collide_delta = 0 # stop jumping
            if self.rect.y > g.rect.y:
                self.health -=1
                print(self.health)
               
        if self.collide_delta < 6 and self.jump_delta < 6:
            self.jump_delta = 6*2
            self.movey -= 33  # how high to jump
            self.collide_delta += 6
            self.jump_delta    += 6
           
class Enemy(pygame.sprite.Sprite):
    '''
    Spawn an enemy
    '''
    def __init__(self,x,y,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img))
        self.movey = 0
        #self.image.convert_alpha()
        #self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

               
    def move(self):
        '''
        enemy movement
        '''
        distance = 80
        speed = 8

        self.movey += 3.2
       
        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance*2:
            self.rect.x -= speed
        else:
            self.counter = 0
       
        self.counter += 1

        if not self.rect.y >= worldy-ty-ty:
            self.rect.y += self.movey

        plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)
        for p in plat_hit_list:
            self.movey = 0
            if self.rect.y > p.rect.y:
                self.rect.y = p.rect.y+ty
            else:
                self.rect.y = p.rect.y-ty

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.rect.y = worldy-ty-ty

       
class Level():
    def bad(lvl,eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0],eloc[1],'yeti.png') # spawn enemy
            enemy_list = pygame.sprite.Group() # create enemy group
            enemy_list.add(enemy)              # add enemy to group
           
        if lvl == 2:
            print("Level " + str(lvl) )

        return enemy_list

    def loot(lvl,tx,ty):
        if lvl == 1:
            loot_list = pygame.sprite.Group()
            loot = Platform(200,ty*7,tx,ty, 'loot_1.png')
            loot_list.add(loot)

        if lvl == 2:
            print(lvl)

        return loot_list

    def ground(lvl,gloc,tx,ty):
        ground_list = pygame.sprite.Group()
        i=0
        if lvl == 1:
            while i < len(gloc):
                ground = Platform(gloc[i],worldy-ty,tx,ty,'ground.png')
                ground_list.add(ground)
                i=i+1

        if lvl == 2:
            print("Level " + str(lvl) )

        return ground_list

    def platform(lvl,tx,ty):
        plat_list = pygame.sprite.Group()
        ploc = []
        i=0
        if lvl == 1:
            ploc.append((20,worldy-ty-128,3))
            ploc.append((300,worldy-ty-256,3))
            ploc.append((500,worldy-ty-128,4))

            while i < len(ploc):
                j=0
                while j <= ploc[i][2]:
                    plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'ground.png')
                    plat_list.add(plat)
                    j=j+1
                print('run' + str(i) + str(ploc[i]))
                i=i+1

        if lvl == 2:
            print("Level " + str(lvl) )

        return plat_list

def stats(score,health):
    myfont.render_to(world, (4, 4), "Score:"+str(score), SNOWGRAY, None, size=64)
    myfont.render_to(world, (4, 72), "Health:"+str(health), SNOWGRAY, None, size=64)

'''
Setup
'''
worldx = 960
worldy = 720

fps = 40 # frame rate
ani = 4  # animation cycles
clock = pygame.time.Clock()
pygame.init()
main = True

BLUE  = (25,25,200)
BLACK = (23,23,23 )
WHITE = (254,254,254)
SNOWGRAY = (137,164,166)
ALPHA = (0,255,0)
   
world = pygame.display.set_mode([worldx,worldy])
backdrop = pygame.image.load(os.path.join('images','stage.png')).convert()
backdropbox = world.get_rect()
player = Player() # spawn player
player.rect.x = 0
player.rect.y = 0
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10
forwardx = 600
backwardx = 230

eloc = []
eloc = [200,20]
gloc = []
tx = 64 #tile size
ty = 64 #tile size

font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"fonts","amazdoom.ttf")
font_size = tx
myfont = pygame.freetype.Font(font_path, font_size)
   
i=0
while i <= (worldx/tx)+tx:
    gloc.append(i*tx)
    i=i+1

enemy_list = Level.bad( 1, eloc )
ground_list = Level.ground( 1,gloc,tx,ty )
plat_list = Level.platform( 1,tx,ty )
loot_list = Level.loot(1,tx,ty)

'''
Main loop
'''
while main == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit(); sys.exit()
            main = False

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                print("LEFT")
                player.control(-steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                print("RIGHT")
                player.control(steps,0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                print('jump')

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps,0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                player.jump(plat_list)

            if event.key == ord('q'):
                pygame.quit()
                sys.exit()
                main = False

    # scroll the world forward
    if player.rect.x >= forwardx:
        scroll = player.rect.x - forwardx
        player.rect.x = forwardx
        for p in plat_list:
            p.rect.x -= scroll
        for e in enemy_list:
            e.rect.x -= scroll
        for l in loot_list:

            l.rect.x -= scroll
               
    # scroll the world backward
    if player.rect.x <= backwardx:
        scroll = backwardx - player.rect.x
        player.rect.x = backwardx
        for p in plat_list:
            p.rect.x += scroll
        for e in enemy_list:
            e.rect.x += scroll
        for l in loot_list:
            l.rect.x += scroll

    world.blit(backdrop, backdropbox)
    player.gravity() # check gravity
    player.update()
    player_list.draw(world) #refresh player position
    enemy_list.draw(world)  # refresh enemies
    ground_list.draw(world)  # refresh enemies
    plat_list.draw(world)   # refresh platforms
    loot_list.draw(world)   # refresh loot
    for e in enemy_list:
        e.move()
    stats(player.score,player.health) # draw text
    pygame.display.flip()
    clock.tick(fps)

via: https://opensource.com/article/20/1/add-scorekeeping-your-python-game

作者:Seth Kenlon 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这本免费的电子书使经验丰富的程序员更深入了解 Linux 中进程间通信(IPC)的核心概念和机制。

让一个软件过程与另一个软件过程进行对话是一个微妙的平衡行为。但是,它对于应用程序而言可能是至关重要的功能,因此这是任何从事复杂项目的程序员都必须解决的问题。你的应用程序是否需要启动由其它软件处理的工作;监视外设或网络上正在执行的操作;或者检测来自其它来源的信号,当你的软件需要依赖其自身代码之外的东西来知道下一步做什么或什么时候做时,你就需要考虑 进程间通信 inter-process communication (IPC)。

这在 Unix 操作系统上已经由来已久了,这可能是因为人们早期预期软件会来自各种来源。按照相同的传统,Linux 提供了一些同样的 IPC 接口和一些新接口。Linux 内核具有多种 IPC 方法,util-linux 包包含了 ipcmkipcrmipcslsipc 命令,用于监视和管理 IPC 消息。

显示进程间通信信息

在尝试 IPC 之前,你应该知道系统上已经有哪些 IPC 设施。lsipc 命令提供了该信息。

RESOURCE DESCRIPTION               LIMIT  USED  USE%
MSGMNI   Number of message queues  32000     0 0.00%
MSGMAX   Max size of message (byt.. 8192     -     -
MSGMNB   Default max size of queue 16384     -     -
SHMMNI   Shared memory segments     4096    79 1.93%
SHMALL   Shared memory pages       184[...] 25452 0.00%
SHMMAX   Max size of shared memory 18446744073692774399
SHMMIN   Min size of shared memory     1     -     -
SEMMNI   Number of semaphore ident 32000     0 0.00%
SEMMNS   Total number of semaphore 1024000.. 0 0.00%
SEMMSL   Max semaphores per semap  32000     -     -
SEMOPM   Max number of operations p  500     -     -
SEMVMX   Semaphore max value       32767     -     -

你可能注意到,这个示例清单包含三种不同类型的 IPC 机制,每种机制在 Linux 内核中都是可用的:消息(MSG)、共享内存(SHM)和信号量(SEM)。你可以用 ipcs 命令查看每个子系统的当前活动:

$ ipcs

------ Message Queues Creators/Owners ---
msqid     perms     cuid      cgid  [...]

------ Shared Memory Segment Creators/Owners
shmid     perms    cuid    cgid  [...]
557056    700      seth    users [...]
3571713   700      seth    users [...]
2654210   600      seth    users [...]
2457603   700      seth    users [...]

------ Semaphore Arrays Creators/Owners ---
semid     perms     cuid      cgid  [...]

这表明当前没有消息或信号量阵列,但是使用了一些共享内存段。

你可以在系统上执行一个简单的示例,这样就可以看到正在工作的系统之一。它涉及到一些 C 代码,所以你必须在系统上有构建工具。必须安装这些软件包才能从源代码构建软件,这些软件包的名称取决于发行版,因此请参考文档以获取详细信息。例如,在基于 Debian 的发行版上,你可以在 wiki 的构建教程部分了解构建需求,而在基于 Fedora 的发行版上,你可以参考该文档的从源代码安装软件部分。

创建一个消息队列

你的系统已经有一个默认的消息队列,但是你可以使用 ipcmk 命令创建你自己的消息队列:

$ ipcmk --queue
Message queue id: 32764

编写一个简单的 IPC 消息发送器,为了简单,在队列 ID 中硬编码:

#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>

struct msgbuffer {
  char text[24];
} message;

int main() {
    int msqid = 32764;
    strcpy(message.text,"opensource.com");
    msgsnd(msqid, &message, sizeof(message), 0);
    printf("Message: %s\n",message.text);
    printf("Queue: %d\n",msqid);
    return 0;
        }

编译该应用程序并运行:

$ gcc msgsend.c -o msg.bin
$ ./msg.bin
Message: opensource.com
Queue: 32769

你刚刚向你的消息队列发送了一条消息。你可以使用 ipcs 命令验证这一点,可以使用 ——queue 选项将输出限制到该消息队列:

$ ipcs -q

------ Message Queues --------
key        msqid   owner  perms  used-bytes  messages
0x7b341ab9 0       seth   666    0          0
0x72bd8410 32764   seth   644    24         1

你也可以检索这些消息:

#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>

struct msgbuffer {
    char text[24];
} message;

int main() {
    int msqid = 32764;
    msgrcv(msqid, &message, sizeof(message),0,0);
    printf("\nQueue: %d\n",msqid);
    printf("Got this message: %s\n", message.text);
    msgctl(msqid,IPC_RMID,NULL);
    return 0;

编译并运行:

$ gcc get.c -o get.bin
$ ./get.bin

Queue: 32764
Got this message: opensource.com

下载这本电子书

这只是 Marty Kalin 的《Linux 进程间通信指南》中课程的一个例子,可从 Opensource.com 下载的这本最新免费(且 CC 授权)的电子书。在短短的几节课中,你将从消息队列、共享内存和信号量、套接字、信号等中了解 IPC 的 POSIX 方法。认真阅读 Marty 的书,你将成为一个博识的程序员。而这不仅适用于经验丰富的编码人员,如果你编写的只是 shell 脚本,那么你将拥有有关管道(命名和未命名)和共享文件的大量实践知识,以及使用共享文件或外部消息队列时需要了解的重要概念。

如果你对制作具有动态和具有系统感知的优秀软件感兴趣,那么你需要了解 IPC。让这本书做你的向导。


via: https://opensource.com/article/20/1/inter-process-communication-linux

作者:Seth Kenlon 选题:lujun9972 译者:laingke 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出