在Lazarus下的Free Pascal编程教程——应用程序配置数据的管理与使用

news/2025/1/30 13:00:05/文章来源:https://www.cnblogs.com/lexyao/p/18665361

0.前言

我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:

学习编程从游戏开始——编程计划(目录) - lexyao - 博客园

我已经在下面这篇文章中介绍了使用LCL和FCL组件构建一个项目(pTetris)的过程,后续的使用Lazarus的文章中使用的例子都是以向这个项目添加新功能的方式表述的:

在Lazarus下的Free Pascal编程教程——用向导创建一个使用LCL和FCL组件的项目(pTetris) - lexyao - 博客园

在前面写的文章中我们已经构建了pTetris项目的框架,并逐步添加了一些功能,作为示例的应用程序俄罗斯方块游戏已经达到了可玩的程度,并提供了丰富的操作方法。在这篇文章中我们将通过示例讲述更多组件的使用方法。

俄罗斯方块游戏中操作方块是核心。在前面的示例中我们已经让作为示例的应用程序俄罗斯方块游戏已经达到了可玩的程度,并提供了丰富的操作方法,在这篇文章中我们将增加定制游戏计分方法、加速算法和难度设置的功能,而在设置的操作界面中加入了能够识别当前情景的智能感知能力。

在这篇文章里,我主要讲述以下几个方面的内容:

  1. 应用程序配置数据管理概述
  2. 给pTetris项目定制配置数据管理的类
  3. 在pTetris项目中使用配置数据
  4. 结束语

1.应用程序配置数据管理概述

应用程序的配置数据是需要保存在磁盘文件中的,否则就需要每次打开应用程序时都需要重新设置一次。
早期的应用程序配置数据通常是保存在扩展名为cfg或ini的文件中,前者的格式因应用程序而异,后者是一种通用的格式。Windows早期版本的注册表就是使用了这种格式,现在还有很多应用程序在使用这种格式。
ini格式的文件有很大的局限性,或许这就是Windows的注册表更换了文件格式的原因吧。
随着xml格式文件得到认可,越来越多的应用程序开始使用xml格式的文件来保存配置数据。xml的全称是可扩展标记语言(Extensible Markup Language),是W3C​​推荐的在不同系统之间交换信息语言。它是一种基于文本的方式来存储信息。
在互联网上作为数据交换使用的另一种格式是json (JavaScript Object Notation)。json是一种轻量级的数据交换格式,它提供了简单的文本标准化数据交换格式。正如其名称所暗示的,它是基于JavaScript编程语言的一个子集,但它与语言无关,除了易于阅读和编写,更易于机器解析和生成。json作为xml的有力竞争者,在应用程序数据管理中占有很重要的地位,也是非常适合用于保存配置数据的一种格式。
无论是Delphi还是Lazarus,都有支持ini、xml和json的类,其中作为配置文件使用的类,包括:

  • TIniFile:Delphi和Lazarus中都可以使用的用于存取ini格式文件的一个类。可参阅Using INI Files/zh CN - Free Pascal wiki
  • TXMLConfig:Lazarus定制的用于管理xml格式配置数据管理的类。可参阅xmlconf - Free Pascal wiki
  • TJsonConfig:Lazarus定制的用于管理xml格式配置数据管理的类。可参阅fcl-json/zh CN - Free Pascal wiki

其中,TXMLConfig和TJsonConfig提供了相似的成员函数,用户使用时只需要使用他们提供的成员函数存取数据,而不需要关心它内部时怎样使用xml或json格式的数据。

2.给pTetris项目定制配置数据管理的类

2.1 管理配置数据的类的实现方法

  • 构建与存储格式无关的数据管理类,在存取函数中实现数据格式的转换
    • 在存储数据的函数中将配置数据转换成特定格式的数据存入磁盘文件
    • 在读取数据的函数中将从磁盘文件中读入特定格式的数据转换成配置数据
  • 构建基于特定格式的数据管理类,使用特定格式数据管理对象管理配置数据
    • 在配置数据管理类中设置一个管理特定格式数据的对象
    • 特定格式数据管理对象负责配置数据在内存中的管理、数据的存盘和读取

我们在pTetris中将采用后一种方式。
数据存盘文件可以有多种格式可供选择:

  • 二进制数据文件:与内存中的数组结构相对应,读入内存后不需要解析就可以直接使用,便捷快速,但记录结构发生变化后无法读取原来的数据,所以兼容性差
    • 简单的记录文件:这类文件通常由特定的应用程序约定和存取
    • 复杂的数据库文件
      • 单文件数据库:一个数据库文件中保存若干个表
      • 多文件数据库:一个数据库由多个文件构成,每个文件包含一个表
    • 复杂格式文件:这类文件通常由特定的应用程序或者规范约定,采用专用的应用程序存取,比如图形文件、视频文件、音频文件
  • 文本文件:读入内存时按需要将字符串转换为其他数据类型,消耗时间比二进制要长一些,但由于是按标识符识别数据,所以不受记录格式的影响,兼容能力强
    • 多行文本文件
      • 简单表格文件:每个行存在用特定符号分隔的数据,所有行的数据格式和个数相同。
      • 复杂表格文件:每一行都有一个特定的标识符,对应的数据格式各不相同。比如注册表文件
      • 复杂格式文件:这类文件通常由特定的应用程序或者规范约定,采用专用的应用程序存取
    • 按一定格式组织的文本文件
      • 文件格式有json、xml等多种格式,每一种格式的数据都有成熟的类

在编写应用程序时管理数据的对象尽可能采用成熟的数据管理工具,这样可以减少工作量又能提供可靠的数据管理能力。
配置数据通常数量较少,结构简单,有升级版本后的兼容性要求,所以不需要复杂的数据结构,但优选以文本格式保存。
基于以上分析,我们确定在pTetris项目中采用json或xml结构的文本格式保存配置数据。

2.2 json、xml简介

2.2.1 关于在Lazarus中使用json的介绍

JSON (JavaScript Object Notation) 是种数据表现形式,它提供了简单的文本标准化数据交换格式。正如其名称所暗示的,它是基于JavaScript编程语言的一个子集,但它与语言无关,除了易于阅读和编写,更易于机器解析和生成。相对于XML,它易于阅读。
想了解更多关于json以及在Lazarus中使用json的知识,请阅读以下链接中的文章:

  • JSON官方网站
  • JSON/zh CN - Free Pascal wiki
  • fcl-json/zh CN - Free Pascal wiki
  • Streaming JSON/zh CN - Lazarus wiki
  • 【delphi】 JSON 操作详解(TJSONObject)_delphi tjsonobject-CSDN博客:Delphi官方的json支持方案
  • Delphi 与 JSON - 随笔分类 - 万一 - 博客园:虽然写的是SuperObject在Delphi中的应用,但内容同样适用于Lazarus
  • GitHub - pult/SuperObject.Delphi: Pascal (Delphi, FPC) json parser library SuperObject:SuperObject的下载及使用说明
  • SuperObject 库的强大功能:JSON 数据处理的不二之选-易源AI资讯 | 万维易源

本来想写一篇在Lazarus中使用json的专题文章,可在网上搜索的时候,发现这方面的文章太多了,而且都比我写得好,所以我就不敢写了。
无论是Delphi还是Lazarus,作为官方首推的json支持都有各自的特点,在网上也有大量的第三方的可用的实现方法。在这些方法中,我曾经对能找到的方法都做了测试,经过对比,我最终选择了SuperObject。我编写的很多应用程序的配置数据管理都是使用了SuperObject,甚至有些内部数据管理也使用了SuperObject。
SuperObject最大的好处是使用方便,但并不是说它是完美无缺的。以下是我的几点体会:

  • SuperObject采用的是接口技术,使用的时候不必为了内存漏洞或者说内存残留而烦恼,这是因为接口是自动释放的,不用了也就释放了内存,不会出现残留
  • SuperObject定义了丰富的数据存取函数,几乎涵盖了我们能够用到的所有数据格式
  • SuperObject的路径管理很方便,使用的时候你只需要关心你的路径(小数点分隔的字符串),而不需要关心节点对象
  • SuperObject的容错能力有一点缺陷,我在用它来解析网页中的json数据的时候,经常因为数据错误而造成解析失败,后来我重写了它的解析函数,提高了容错能力
  • SuperObject作为内存数据管理虽然方便,但若干用来管理频繁使用的大量数据是不合适的,这是因为它解析路径的方法是单字符循环分析,耗费时间太长导致程序运行变得很慢。对于少量的数据管理,这种变慢是可以忽略的,但大量数据就不一样了,会让你能够感觉到程序运行中短暂的“停顿”

如果你想使用SuperObject,可以从网上下载,上面给出的链接中有一个是SuperObject的下载地址。下载的文件包中有两个文件:

    • superobject.pas:提供了完整的json支持,我们使用的就是这个文件
    • superxmlparser.pas:提供了xml格式转换为json格式的方法,算是一个辅助文件,如果你不需要数据转换就可以不用管这个文件

不过,既然我们介绍的是Lazarus程序设计,还是使用Lazarus提供的方案更合适。

2.2.2 关于在Lazarus中使用xml的介绍

以下这段话是摘录的百度百科《可扩展标记语言_百度百科》中关于xml的介绍:

可扩展标记语言 (Extensible Markup Language, XML) ,标准通用标记语言的子集,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 XML是标准通用标记语言 可扩展性良好,内容与形式分离,遵循严格的语法要求,保值性良好等优点。
在电子计算机中,标记指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种的信息比如文章等。它可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 它非常适合万维网传输,提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。是Internet环境中跨平台的、依赖于内容的技术,也是当今处理分布式结构信息的有效工具。早在1998年,W3C就发布了XML1.0规范,使用它来简化Internet的文档信息传输。

想了解更多关于xml以及在Lazarus中使用xml的知识,请阅读以下链接中的文章:

  • XML Tutorial/zh CN - Free Pascal wiki
  • XML Decoders/zh CN - Lazarus wiki
  • fcl-xml - Free Pascal wiki
  • Category:XML - Lazarus wiki
  • XML从入门到深入(超详细) - 蚂蚁小哥 - 博客园
  • 【学习】lazarus的Laz2_XMLCfg用法 - 秋·风 - 博客园
  • Using TXMLDocument - RAD Studio:Delphi中的xml支持,Lazarus同样适用
  • NativeXml (1):下载、安装、测试 - 万一 - 博客园:NativeXml是一个非常优秀的第三方xml实现代码,深受Delphi用户的欢迎
  • JCL Help:TJclSimpleXML - Project JEDI Wiki:TJclSimpleXML 是JEDI的jcl中的xml支持方案,我比较喜欢这个,真正的简单易用,但功能一点也不简单。

Delphi官方推出的xml支持使用的是TXMLDocument,由于使用比较繁琐,我不太喜欢这个。Lazarus中的xml也是使用了TXMLDocument,我还没有研究它与Delphi中TXMLDocument有什么不一样,估计没有太多的差别。
在使用Delphi的时候,我们尝试了很多xml的支持方案,最终确定使用的是TJclSimpleXML ,真正的简单易用,但功能一点也不简单。

2.3 给pTetris项目定制配置数据管理的类

2.3.1 在项目中使用管理配置数据的方法分析

Lazarus提供了TXMLConfig和TJsonConfig来管理配置数据,有了这样的类可以达到我们的目的,我们就没有必要再费心劳神去编写自己的代码。那么,我们怎样利用这些现成的代码去管理配置数据呢?
具体说来,管理和使用配置数据需要做以下几个方面的工作:

  • 创建一个变量指向配置数据管理类,可以命名为FConfigData。在这里变量FConfigData的类型应该改是TXMLConfig或TJsonConfig
  • 建立一个函数ConfigData用来引用变量FConfigData,之所以需要使用函数,目的是确保在引用变量之前对变量进行检查,确保变量已经创建并初始化,具体包括以下几个方面:
    • 如果变量FConfigData是空的,则创建一个新的对象,然后:
      • 如果存在以前保存的配置数据文件,则读入配置文件
      • 如果还没有配置文件,则初始化FConfigData所指对象的数据成员为默认值
  • 在使用配置数据的地方使用函数ConfigData引用FConfigData
    • 在选择配置数据的页面的事件代码中将改变的配置数据存入FConfigData对象的成员
    • 在展示配置页面之前使用上次保存的或默认的配置数据设置配置页面组件的显示值
    • 在使用配置数据的地方将FConfigData成员中的配置数据提供给应用程序
      • 游戏开始时使用配置数据设置初始速度、初始方块高度
      • 游戏中使用配置数据设置方块组合样式、方块表面图案或颜色
      • 移动一组方块结束时使用配置数据计算得分、加速、
  • 在程序结束之前
    • 如果FConfigData中的数据有修改,则保存到磁盘文件
    • 释放FConfigData所指的对象

实现以上功能的代码可以有两种方式:

  • 在TfrmMain中定义变量和函数
    • 在TfrmMain中使用可直接调用函数
    • 在cxBoxQueue、cxBoxMove、cxBoxHeap等类中使用可通过变量frmMain引用TfrmMain的成员函数
      • 需要在pTetrisUint的uses中添加pTetrisMain
  • 在pTetrisUint单元中定义变量和函数
    • 直接定义变量和函数
      • 可以使用全局函数,也可以打包在一个静态类中
      • 定义一个变量指向配置数据类的实例
      • 定义一个函数引用配置数据类
        • TfrmMain、cxBoxQueue、cxBoxMove、cxBoxHeap通过这个函数引用配置数据类的实例
        • 操作函数通过这个函数引用配置数据类的实例
      • 定义多个函数操作配置数据类:初始化、读入、保存
        • TfrmMain、cxBoxQueue、cxBoxMove、cxBoxHeap通过函数使用配置数据
      • 在finalization释放这个类的实例
    • 将变量和函数打包在一个类中
      • 配置数据类的变量、引用、操作函数等都打包在一个类中
      • 定义一个变量指向这个类的实例
      • 定义一个函数引用这个类
        • TfrmMain、cxBoxQueue、cxBoxMove、cxBoxHeap通过使用这个类的实例使用配置数据
      • 在finalization释放这个类的实例

以上分析中使用的方法说不上哪一个更好,都是合理的方法,选用哪一个全在个人的习惯。

2.3.2 配置数据管理函数的实现

从面向对象的程序设计考虑,在这里我们选择将函数打包在一个类中。下面开始编写配置数据管理函数代码编写的步骤。
TXMLConfig与TJsonConfig具有相同的成员函数,使用参数的格式也是相同的,所以在这里我们以TXMLConfig为例。如果想使用TJsonConfig,只需要将下面代码中的TXMLConfig替换为TJsonConfig就行了,其他的代码不需要任何改动,差别只是存盘文件的格式发生了变化。
第一步、在pTetrisUint单元的uses中添加xmlConf,然后创建配置数据管理类cxConfig的框架,其中:

  • 变量FConfigData: TXMLConfig用于管理配置数据
    • cxConfig类的所有成员函数都是为了操作FConfigData中的数据而设置的
  • 使用属性ConfigData引用FConfigData变量所指的对象,这是封装的习惯做法
    • 在类的外部使用ConfigData而不能使用FConfigData
  cxConfig = class(TComponent)privateFConfigData: TXMLConfig;protectedpublicproperty ConfigData: TXMLConfig read FConfigData;end;  

第二步、创建、初始化和销毁FConfigData对象

  • 创建和初始化FConfigData对象:在使用中的FConfigData中的配置数据之前必须先创建FConfigData对象并初始化FConfigData中的数据,具体需要完成以下操作:
    • 创建TXMLConfig类的实例保存在FConfigData中
    • 初始化FConfigData对象中的数据,包括以下两种情况:
      • 如果不存在配置数据文件,需要FConfigData能够提供配置数据的默认值
      • 如果存在配置数据文件,需要将配置数据文件中的数值装入FConfigData对象中
  • 销毁FConfigData对象:一般情况下,销毁FConfigData对象的代码放在重写的析构函数destructor Destroy中最合适。这个没有争议。我们把定义为组件,TXMLConfig也是组件,只要FConfigData的Owner不是空值,FConfigData会在它的Owner所指的组件销毁之前销毁,我们不必再编写销毁FConfigData的代码

在cxConfig类定义中添加以下代码:

  privateFConfigData: TXMLConfig;publicproperty ConfigData: TXMLConfig read FConfigData;procedure Init;  

在定义的代码中,函数Init将包含创建和初始化FConfigData对象的代码。在这里,我们先加入创建FConfigData对象的代码,初始化的代码将在后面加入。

procedure cxConfig.Init;
beginif not Assigned(FConfigData) then  begin//创建配置文件对象FConfigData := TXMLConfig.Create(Self);end;
end;  

第三步、设定配置数据的初始值
按着一般的编程习惯,程序员会选择每个配置选项最常用的数值作为选项的初始值(有时也称作默认值)。在用户第一次运行程序的时候,所有选项的当前值都是初始值,这样做可以保证多数用户不做任何选项的修改就可以使用应用程序。
一般情况下,程序员需要为每个选项定义一个常数作为默认值,定义一个读取配置数据的函数,当配置数据中没有这个选项的数据时,就返回默认值。
为了减少编写代码,在这里我们采用了另一种方法:

  • 在界面设计中将选项的组件当前值设置成默认值
  • 在第一次运行应用程序的时候将组件的当前值保存到配置文件中,这样确保配置文件中有每个选项的值,不再需要单独为其设置默认值常数

为此,我们添加两个函数:

  • ValueFrom(ACtrl: TComponent)将选项组件ACtrl的当前值保存到配置文件FConfigData中
    • cxConfig.PathOf函数用来生成保存文件的路径,如果想修改路径,只需要修改这个函数的代码就可以了
    • 在ValueFrom中采用按组件分类,采用组件名作为保存数据的路径,这样可以避免为每一个选项编写一个存取函数、定义一个路径常数,从而减少的代码量。
      需要提示一点:ValueFrom的这种设计方法是一种技巧,这种技巧的好处将会在后面的代码中体现出来
  • DataInit(ACtrl: TComponent)从配置表的顶层容器PageControl1开始,通过递归的方法将其包含的所有选项组件通过ValueFrom函数保存数据到配置文件FConfigData中
    • 为了保证使用FConfigData的时候在FConfigData中有选项的数据,需要在其他使用FConfigData的代码之前运行DataInit函数的代码
function cxConfig.PathOf(ACtrl: TControl): string;
beginResult := ACtrl.Name;
end; procedure cxConfig.ValueFrom(ACtrl: TComponent);
varpth: string;
beginif not (ACtrl is TControl) then Exit;//获得数据保存的路径pth := PathOf(ACtrl as TControl);//为了简化代码,按组件类型保存组件属性值作为配置数据if ACtrl is TPageControl thenConfigData.SetValue(pth + '/ActivePageIndex', (ACtrl as TPageControl).ActivePageIndex)else if ACtrl is TCheckBox thenConfigData.SetValue(pth + '/Checked', (ACtrl as TCheckBox).Checked)else if ACtrl is TRadioGroup thenConfigData.SetValue(pth + '/ItemIndex', (ACtrl as TRadioGroup).ItemIndex)else if ACtrl is TComboBox thenConfigData.SetValue(pth + '/ItemIndex', (ACtrl as TComboBox).ItemIndex)else if ACtrl is TColorButton thenConfigData.SetValue(pth + '/ButtonColor', (ACtrl as TColorButton).ButtonColor)else if ACtrl is TTrackBar thenConfigData.SetValue(pth + '/Position', (ACtrl as TTrackBar).Position);
end;  procedure cxConfig.DataInit(ACtrl: TComponent);
vari: integer;ctrl: TWinControl;child: TControl;
begin//采用递归方法遍历所有组件if ACtrl is TWinControl thenbeginctrl := ACtrl as TWinControl;for i := 0 to ctrl.ControlCount - 1 dobeginchild := ctrl.Controls[i];DataInit(child);end;end;//保存组件当前值到配置文件
  ValueFrom(ctrl);
end; 

第四步、保存配置数据到磁盘文件。要把配置数据保存到磁盘文件,需要做以下三件事:

  • 确定存盘文件名,为此我们添加函数cxConfig.FileName
  • 编写存盘函数cxConfig.SaveToFile
  • 调用cxConfig.SaveToFile,我们在重写的析构函数destructor Destroy中调用
function cxConfig.FileName: string;   
varfn: string;
beginfn := Application.ExeName;Result := ChangeFileExt(fn, '.xml');
end;  procedure cxConfig.SaveToFile;
begin//仅在配置数据修改时保存配置数据到文件中if Assigned(FConfigData) and FConfigData.Modified thenbeginFConfigData.SaveToFile(FileName);end;
end; destructor cxConfig.Destroy;
beginSaveToFile;inherited Destroy;
end; 

第五步、从磁盘文件中读入配置数据
在上次使用应用程序的时候修改的配置数据在退出应用程序的时候保存到了磁盘文件,再次运行应用程序的时候要使用上次的配置数据,就需要从磁盘文件中读入配置数据,为此需要编写以下几个方面的代码:

  • 编写读入磁盘文件的函数cxConfig.LoadFromFile
  • 将读入的配置数据在配置表组件中重现,需要编写以下函数实现与ValueFrom、DataInit相反的操作:
    • ValueTo(ACtrl: TComponent)将FConfigData中的配置数据恢复的对应的组件ACtrl中
    • DataReset(ACtrl: TComponent)从配置表的顶层容器PageControl1开始,通过递归的方法将其包含的所有选项组件通过ValueTo函数从配置文件FConfigData中获得上次的选项值
  • 为了保证使用FConfigData的时候在FConfigData中有选项的数据,需要在其他使用FConfigData的代码之前运行LoadFromFile、DataReset函数的代码
procedure cxConfig.ValueTo(ACtrl: TComponent);
varpth: string;iDef: Integer;
begin                if not (ACtrl is TControl) then Exit;//获得数据保存的路径pth := PathOf(ACtrl as TControl);//为了简化代码,按组件类型获取组件属性值作为配置数据if ACtrl is TPageControl then(ACtrl as TPageControl).ActivePageIndex := ConfigData.GetValue(pth + '/ActivePageIndex', 0)else if ACtrl is TCheckBox then(ACtrl as TCheckBox).Checked := ConfigData.GetValue(pth + '/Checked', True)else if ACtrl is TRadioGroup then(ACtrl as TRadioGroup).ItemIndex := ConfigData.GetValue(pth + '/ItemIndex', 0)else if ACtrl is TComboBox then(ACtrl as TComboBox).ItemIndex := ConfigData.GetValue(pth + '/ItemIndex', 0)else if ACtrl is TColorButton then(ACtrl as TColorButton).ButtonColor := ConfigData.GetValue(pth + '/ButtonColor', 0)else if ACtrl is TTrackBar thenbegin//如果有不同的默认值,可以按组件名识别组件,确定不同的默认值if IndexText(ACtrl.Name, ['trcStartTime', 'trcNextNumber'])>=0 theniDef := (ACtrl as TTrackBar).MaxelseiDef := (ACtrl as TTrackBar).Min;(ACtrl as TTrackBar).Position := ConfigData.GetValue(pth + '/Position', iDef);end;
end; procedure cxConfig.DataReset(ACtrl: TComponent);
varchild: TControl;ctrl: TWinControl;i: integer;
beginif ACtrl is TWinControl thenbegin//采用递归方法遍历所有组件ctrl := ACtrl as TWinControl;for i := 0 to ctrl.ControlCount - 1 dobeginchild := ctrl.Controls[i];DataReset(child);end;//用配置文件中的数据设置组件的值
    ValueTo(ctrl);end;
end;  

第六步、集成初始化FConfigData的代码
前面提到,为了保证使用FConfigData的时候在FConfigData中有选项的数据,需要在其他使用FConfigData的代码之前运行LoadFromFile、DataReset、DataInit函数的代码。现在我们把调用这三个函数的代码集成在一个函数Init中:

procedure cxConfig.Init;
beginif not Assigned(FConfigData) thenbegin//创建配置文件对象FConfigData := TXMLConfig.Create(Self);//从磁盘中装入上次退出时保存的配置数据if LoadFromFile thenDataReset(Owner)   //如果存在上次的配置数据,则用配置数据设置配置界面中的组件elseDataInit(Owner);   //如果不存在上次的配置数据,则用配置界面中组件的值为配置数据的初始值end;
end; 

第七步、确定运行cxConfig.Init的位置
cxConfig.Init函数包含了创建和初始化FConfigData对象的代码,运行Init函数的位置需要满足以下两个要求:

  • 需要在创建cxConfig类的实例之后运行
  • 需要在使用FConfigData中数据的代码之前运行

满足以上两个条件的位置有以下几种:

  • 在cxConfig类内部隐式调用,通常有两种方式
    • 在cxConfig的构造函数中,需要重写constructor Create(AOwner: TComponent); override; 
    • 在cxConfig的AfterConstruction函数中,需要重写procedure AfterConstruction; override;
  • 在cxConfig类外部显示调用
    • 需要紧跟在代码 FConfig := cxConfig.Create(...)之后添加代码FConfig.Init;

方案一:在cxConfig的构造函数中调用Init,代码如下:

constructor cxConfig.Create(AOwner: TComponent);
begininherited Create(AOwner);Init;
end; 

方案二:在cxConfig.AfterConstruction函数中调用Init,代码如下:

procedure cxConfig.AfterConstruction;
begininherited AfterConstruction;Init;
end; 

方案三:在cxConfig类之外显示调用Init,需要在创建cxConfig类实例的地方添加以下两行代码代码:

  FConfig := cxConfig.Create(ARootCtrl);FConfig.Init;    

为了简化代码,我们可以把这两行代码打包在一个函数中:

class procedure cxConfig.CreateAndInit(ARootCtrl: TComponent);
beginFConfig := cxConfig.Create(ARootCtrl);FConfig.Init;
end; 

这样在创建cxConfig类实例的地方使用cxConfig.CreateAndInit(...)就行了。
如果使用方案一或者方案二,也可以使用CreateAndInit函数,只是需要注释掉其中的FConfig.Init。
第八步、创建cxConfig类的实例。需要添加以下代码:
定义保存cxConfig实例的变量。由于需要在pTetrisUint、pTetrisMain两个单元中使用,在pTetrisUint单元中有多个类使用,所以将变量定义为pTetrisUint单元中的全局变量:

varFConfig: cxConfig; 

创建cxConfig类的实例的代码要在所有使用它之前运行,而创建后需要设置配置表组件的值,需要在TfrmMain.FormCreate之后执行,所以选择在TfrmMain.FormShow中添加创建cxConfig类的实例的代码:

cxConfig.CreateAndInit(PageControl1); 

至此,我们就可以编译运行pTetris项目进行代码测试了。

按着在Delphi中编写程序的经验,第七步的三种方式都没有问题,但在Lazarus下编写的隐式调用Init的代码(方案一和方案二)却出现了运行错误,经过多次调试没有找出原因。

 

方案三运行正常,我们只好选择方案三的代码。
现在进行测试智能看能不能正常运行,从界面上是看不到变化的。我们能看到的只有保存的配置文件。由于配置文件在退出应用程序的时候才会保存,所以我们运行pTetris后退出程序,到pTetris.exe所在的文件夹找到一个pTetris.xml的文件,这就是存盘后的配置数据文件,里面的内容是配置表界面中组件的初始值。用记事本打开pTetris.xml文件,你会看到如下内容:

<?xml version="1.0" encoding="utf-8"?>
<CONFIG><cbBoxLine ItemIndex="0"/><grpBoxLine ItemIndex="1"/><trcScoreBase Position="1"/><ckScoreHeight Checked="True"/><trcScoreHeight Position="1"/><ckScoreDesBase Checked="True"/><trcScoreDesBase Position="1"/><ckScoreDesRows Checked="True"/><trcScoreDesRows Position="1"/><ckScoreDesHeight Checked="True"/><trcScoreDesHeight Position="1"/><trcSpeedBase Position="1"/><ckSpeedKey Checked="True"/><trcSpeedKey Position="1"/><ckSpeedTimer Checked="True"/><trcSpeedTimer Position="1"/><grpTimeCalc ItemIndex="0"/><trcStartTime Position="9"/><trcNextNumber Position="4"/><trcStartHeight Position="0"/><grpBoxStyle ItemIndex="0"/><ckPenetrate Checked="False"/><PageControl1 ActivePageIndex="2"/>
</CONFIG>

第九步、保存修改的配置数据
在第八步的测试中你会看到,无论在配置表中怎么修改选项,当再次运行pTetris应用程序的时候,修改的选项又恢复了原来的数值。这是因为我们的配置数据保存的是设计时在窗体设计器和属性列表中设定的组件的值,也就是选项的初始值,修改的值并没有保存在文件中。
要想保存修改的值,就需要添加代码将修改后的数据保存到FConfigData中,这样在退出pTetris的时候修改的数据就保存了。
我们先在一个组件中添加代码进行一个测试,查看保存配置数据的效果。选择配置表的起始难度页面中的方块组合样式,也就是组件grpBoxStyle,给它添加OnSelectionChanged事件的处理函数,在实现中添加如下代码:

procedure TfrmMain.grpBoxStyleSelectionChanged(Sender: TObject);
beginFConfig.ValueFrom(Sender as TComponent);
end;  

编译运行pTetris项目,修改方块组合样式的选中的项目,然后退出pTetris,再次运行pTetris,看修改的选项是不是恢复到了你上次选中的项目?这说明我们添加的代码起作用了。
在添加的保存选项的代码中我们使用了事件处理函数参数中的Sender作为ValueFrom函数的参数,这样就不必为每个选项编写一个函数或路径了。
给需要保存数据的组件都添加这行代码,这样凡是添加了这行代码的组件修改后的选项都会保存下来:

  FConfig.ValueFrom(Sender as TComponent);   

不同类型的组件添加这行代码的事件是不同的,以下是各种组件使用事件处理函数:

  • TPageContro、TComboBoxl、TTrackBar、TCheckBox的事件是OnChange
  • TRadioGroup的事件是OnSelectionChanged
  • TColorButton的事件是OnColorChanged

需要保存数据的组件都添加了这行代码后再编译运行pTetris项目,测试修改选项,当再次运行的时候所有的选项都会重现修改后的数据而不是设计时设置的初始值。打开pTetris.xml文件,你也会看到其中的数据改成了你在选项表中设置的数值。

3.在pTetris项目中使用配置数据

在应用程序中使用配置数据的有多个方面,在这里我们先保证游戏的基本玩法,完成计分、加速和起始难度的基本实现。具体需要考虑的因素在《在Lazarus下的Free Pascal编程教程——打造有智能感知的用户设置操作界面 - lexyao - 博客园》我们已经有了比较详细的描述,在这里我们要把那些想法中最基本的内容实现。

3.1 pTetris中计分的实现

3.1.1 计分的需求分析

以下是我们在《在Lazarus下的Free Pascal编程教程——打造有智能感知的用户设置操作界面 - lexyao - 博客园》细化的计分规则:

  • 每移动一个方块到达堆积区得基础分1分,按方块个数计算每组的总基础得分。比如,每组4个方块,可得总基础得分为4分
  • 以下可选的加分规则如果选中,则按设定的加分比例乘以基础分。基础分乘以所有加分倍数得到一组方块的最终得分
    • 放置高度加分:按放置方块的基点所在的行号(即5x5方格的中心点到堆积区底部的距离)计算加分倍数
      • 从最低部的行开始依次为1倍、2倍……
    • 消除行加分:放下方块后形成满行(满行的方块将被消除)会得到加分奖励
      • 消除一行得分增加的基础倍数,可选1-9
        • 消除行的高度加分:消除行的基础倍数乘以消除行所在高度
        • 同时消除多行加分:消除行的基础倍数乘以同时消除的行数

下面我们针对以上的要求确定编写代码需要的工作:

  • 计分基础是移动的方块,可通过当前盒子中方块的个数计算基础得分
    • 作为一种成绩,我们需要一个变量来统计移动方块的个数:property BoxCount: integer
  • 每完成一个方块盒子的移动要进行得分计算,需要有变量来统计移动当前方块盒子的得分和累计得分
    • 当前盒子得分property BoxScore: integer
    • 累计得分property BoxScores: integer
  • 计算放置高度加分,可通过当前移动方块盒子的基点获得,要使用基点的高度,有两种情况:
    • 在打开盒子之前计分,可直接使用当前盒子的基点值
    • 在打开盒子之后计分,需要在打开盒子时保存盒子的基点备用
  • 计算消除行加分,需要在消除满行之前统计满行的数量和满行所在的高度
    • 满行在打开盒子后才能统计
    • 消除满行之前需要完成满行统计结果,或者在消除满行前将统计结果保存下来
    • 作为一种成绩,我们需要一个变量来统计消除满行数:property BoxFullRows: integer
  • 打开盒子是在DoTimerVx的当前版本中完成的,所以统计和计算的工作在DoTimerV5中调用

3.1.2 计分的实现

根据以上分析,为了统计战绩,我们在TfrmMain中添加以下属性:

    property BoxCount: integer read FBoxCount write SetBoxCount;property BoxScore: integer read FBoxScore write SetBoxScore;property BoxScores: integer read FBoxScores write SetBoxScores;property BoxFullRows: integer read FBoxFullRows write SetBoxFullRows; 

统计数字在游戏开始的时候是从0开始计数的,所以在游戏开始的时候要设置这些属性的初始值为0。
在以前的文章中我们曾经有一个函数GameBegin用来做游戏开始的一些初始化工作,并为计分代码预留的位置。现在我们就将计分属性初始化的代码添加到GameBegin中:

procedure TfrmMain.GameBegin;
begin......{ #todo : 计时计分归零 }BoxCount := 0;BoxFullRows := 0;BoxScores := 0;......
end;   

 在以前的代码中,判断和消除满行的代码是在cxBoxHeap.ClearFullRows中实现的,我们通过在DoTimerV4中使用boxHeap.ClearFullRows消除满行。cxBoxHeap.ClearFullRows是一个过程(procedure),没有返回值。我们要在在DoTimerVx的当前版本中完成移动方块得分的计算,需要用到满行的行数和满行所在的高度,这就需要cxBoxHeap.ClearFullRows能够返回这两个值。现在我们修改这个过程,给它添加两个参数,可以返回满行的行数和高度。不过,为了保持DoTimerV4的代码不被修改,我们重写cxBoxHeap.ClearFullRows,保持一个不带参数的版本。当然,我们已经使用了DoTimerV5,而DoTimerV4已经不需要了,如果已经通过注释的方法去掉了DoTimerV4,那就不需要cxBoxHeap.ClearFullRows不带参数的版本了。

{销毁满行,上部方块下落}
procedure cxBoxHeap.ClearFullRows;
variRows, iRowHeight: integer;
beginClearFullRows(iRows, iRowHeight);
end;procedure cxBoxHeap.ClearFullRows(var iRows, iRowHeight: Integer);
vari, j: integer;rw: cxDustbin;bx: cxBox;xy: TPoint;
begin//销毁满行
  iRows := 0;iRowHeight:= 0;for i := RowCount - 1 downto 0 dobeginif RowFull(i) thenbeginrw := Rows(i);rw.Free;
      Inc(iRows);Inc(iRowHeight, i);end;end;//如果有销毁的行,则需要重新显示堆积的方块(向下移动)......end;  

计算得分的代码在DoTimerV5中实现,为了代码的可读性,我们将计算得分的代码放到一个函数OpenBoxAndCalcScore中,然后在DoTimerV5中调用OpenBoxAndCalcScore函数。在OpenBoxAndCalcScore函数中也包含了以前在DoTimerV5中使用的boxHeap.BoxsOpen和boxHeap.ClearFullRows,其中boxHeap.ClearFullRows是新版的带参数的版本。

procedure TfrmMain.OpenBoxAndCalcScore(bxs: cxBoxs);
variScoreDesRows, iScoreDesHeight, iScoreDesBase: integer;iScoreDes, iRows, iRowHeight: integer;iScoreHeight, iScoreBase: integer;iScore: integer;
begin//--计算基础分数iScoreBase := FConfig.ConfigData.GetValue('trcScoreBase/Position', 1);iScore := iScoreBase * bxs.BoxCount;//--计算放置高度加分if FConfig.ConfigData.GetValue('ckScoreHeight/Checked', True) thenbeginiScoreHeight := FConfig.ConfigData.GetValue('trcScoreHeight/Position', 1);iRowHeight := boxHeap.RowFromGrid(bxs.BaseY);if iRowHeight > 0 theniScore := iScore * iScoreHeight * iRowHeight;end;//--打开盒子BoxCount := BoxCount + 1;boxHeap.BoxsOpen(bxs);//bxs.Free;//--消除满行
  boxHeap.ClearFullRows(iRows, iRowHeight);BoxFullRows := BoxFullRows + iRows;//--计算消行加分if (iRows > 0) and FConfig.ConfigData.GetValue('ckScoreDesBase/Checked', True) thenbeginiScoreDesBase := FConfig.ConfigData.GetValue('trcScoreDesBase/Position', 1);iScoreDes := iScoreDesBase;if (iRowHeight > 0) and FConfig.ConfigData.GetValue('ckScoreDesHeight/Checked', True) thenbeginiScoreDesHeight := FConfig.ConfigData.GetValue('trcScoreDecHeight/Position', 1);iScoreDes := iScoreDes * iScoreDesHeight * iRowHeight;end;if (iRows > 0) and FConfig.ConfigData.GetValue('ckScoreDesRows/Checked', True) thenbeginiScoreDesRows := FConfig.ConfigData.GetValue('trcScoreDecHeight/Position', 1);iScoreDes := iScoreDes * iScoreDesRows * iRows;end;iScore := iScore * iScoreDes;end;//--统计得分BoxScore := iScore;BoxScores := BoxScores + iScore;
end;  

在以上代码中,我们使用FConfig.ConfigData.GetValue获取配置数据的当前值,而GetValue参数中的路径则要与cxConfig.ValueFrom中一致。

3.1.3 当前得分的显示

在界面布局时我们添加了一个组件pnInfo用于当前游戏信息的显示,现在我们就将当前的得分情况显示在这个组件中。
这是一个面板组件,通常用来占位或者作为容器的,现在我们用它的Caption属性来显示游戏信息。
首先,在属性列表中设置pnInfo的Wordwrap属性为true,这样它就可以自动换行,可以用来显示多行文本。
然后,我们编写一个函数,将将游戏的当前信息显示在pnInfo中:

procedure TfrmMain.ViewScores;
varsb: TStringBuilder;
beginsb := TStringBuilder.Create;sb.AppendLine(Format('移动方块%d组', [FBoxCount])); sb.AppendLine(Format('消除满行%d', [FBoxFullRows]));sb.AppendLine(Format('累计得分%d', [FBoxScores])); 
  sb.AppendLine(Format('本次得分%d', [FBoxScore]));  pnInfo.Caption := sb.toString;sb.Free;
end;  

为了让信息能够显示出来,还需要调用函数ViewScores。我们将函数添加到需要在ViewScores中显示的属性的设置函数中,这样每次有属性值发生改变的时候都会调用ViewScores函数,从而使得信息显示及时刷新。在这里需要添加ViewScores函数的属性设置函数包括:SetBoxCount、SetBoxFullRows、SetBoxScore、SetBoxScores。这几个函数的格式是一样的,我们在这里只粘贴一个函数的代码,其他的函数在同样的位置添加以下代码中的红色代码:

procedure TfrmMain.SetBoxCount(AValue: integer);
beginif FBoxCount = AValue then Exit;FBoxCount := AValue;ViewScores;
end;  

这样,编译运行pTetris项目,开始游戏后就能看到得分的信息了。

 

3.2 pTetris中加速的实现

3.2.1 加速的需求分析

以下是我们在《在Lazarus下的Free Pascal编程教程——打造有智能感知的用户设置操作界面 - lexyao - 博客园》细化的加速规则:

  • 方块从移动区顶部自动跳动下落,每次下落一行,两次跳动动作的时间间隔
    • 基础时间间隔在1-1000毫秒之间,默认初始值为1000,可在难度中设置初始值
    • 每放下一组方块跳动动作的时间间隔缩短,缩短的时间按基准时间计算所得
      • 计算方法为:缩短时间=基准时间×加速倍数/得分总数
      • 基准时间在1-10毫秒之间,默认为1毫秒
      • 加速倍数为以下可选项的倍数总和
        • 按下落跳动次数加倍
        • 每按键一次增加1-10倍,默认为1倍

下面我们针对以上的要求确定编写代码需要的工作:

  • 方块移动的速度有两个数值:一个是当前速度,一个是变化的时差。我们设置两个属性记录这两个数值。由于方块移动我们使用了计时器驱动,计时器的最小数值是1毫秒,而我们计算的时差有可能小于1毫秒,所以我们使用Delphi中表示时间的数据格式TDatetime,也就是double
    • property TimerInterval: double;方块移动的当前时间间隔,取整数后可作为Timer的Interval属性值
      • 在每一局游戏开始时初始化
      • 在每一组盒子移动完成后调整
    • property TimerAdjust: double;完成一组方块移动后调整时差的调整值,这是我们通过加速算法计算出来的
      • 在每一组盒子移动完成后计算出来
  • 加速算法考虑了两种因素:手动操作加速和自动下落加速
    • 手动操作加速:按手动操作的次数计算加速的倍数,操作越多加速也越多。为了避免加速过快,让用户尽量减少操作的次数。
      • 需要设置一个属性值统计操作的次数property MoveKey: integer
      • 每一组方块移动开始前要设置MoveKey:=0;
      • 每次操作要设置MoveKey+1
        • 在每个操作按钮的事件处理函数中实现
    • 自动下落加速:方块盒子在计时器的推动下向下跳动,跳动的次数越多加速也越多。为了避免加速过快,用户可以使用“跌落”操作让盒子直接到达底部
      • 需要设置一个属性值统计跳动的次数property MoveTimer: integer
      • 每一组方块移动开始前要设置MoveTimer:=0;
      • 时钟周期要设置MoveTimer+1
        • 在DoTimerVx的当前版本DoTimerV5中实现
  • 计算加速的算法有两种,计算加速的代码可以在一个方块盒子移动结束后,紧跟在计算得分的代码之后完成,也就是在DoTimerVx的当前版本DoTimerV5中调用

 

3.2.2 加速的实现

根据以上分析,为了统计战绩,我们在TfrmMain中添加以下属性:

    property MoveTimer: integer read FMoveTimer write SetMoveTimer;property MoveKey: integer read FMoveKey write SetMoveKey;property TimerInterval: double read FTimerInterval write SetTimerInterval;property TimerAdjust: double read FTimerAdjust write SetTimerAdjust;  

其中,SetTimerInterval中要完成计时器的调整和当前速度显示组件的调整:

procedure TfrmMain.SetTimerInterval(AValue: double);
beginif AValue < 0 then AValue := 0;if FTimerInterval = AValue then Exit;FTimerInterval := AValue;//Timer1.Enabled := False;Timer1.Interval := round(AValue);//Timer1.Enabled := True;trcStart.SelEnd := 1000 - round(AValue);  

在每一局游戏开始时要设置初始化这些属性值:

procedure TfrmMain.GameBegin;
begin......{ #todo : 计时计分归零 }......MoveKey := 0;MoveTimer := 0;{ #todo : 计时器恢复默认的开始速度 }TimerInterval := FConfig.StartTime;......
end;   

在这里看到一个函数StartTime。由于在起始难度中设置的方块跳动的起始时间间隔在配置文件中使用的数值需要换算为毫秒,所以添加了这个函数:

function cxConfig.StartTime: integer;
begin//由于需要换算,添加单独使用的函数完成数据换算Result := (ConfigData.GetValue('trcStartTime/Position', 9) + 1) * 100;
end; 

在DoTimerV5中获得一个新的方块盒子时或者在完成一个方块盒子的计算得分和时间调整后添加以下代码实现两个加速因子的归零。由于获得新盒子的代码隐藏在cxBoxMove中,所以代码添加在调整时间之后:

    //计数归零MoveKey := 0;MoveTimer := 0; 

在DoTimerV5中添加以下代码统计方块盒子自动跳动的次数:

MoveTimer := MoveTimer + 1; 

在左移、右移、旋转、跌落、下落五个按钮OnClick的事件处理函数中添加以下代码统计操作的次数:

MoveKey := MoveKey + 1;  

计算加速的代码在DoTimerV5中实现,为了代码的可读性,像计算得分一样,我们将计算加速的代码放到一个函数AdjustTimer中,然后在DoTimerV5中调用AdjustTimer函数。

procedure TfrmMain.AdjustTimer;
variSpeed, iSpeedBase, iSpeedKey, iSpeedTimer: integer;
begin//取得基础时差iSpeedBase := FConfig.ConfigData.GetValue('trcSpeedBase/Position', 1);iSpeed := iSpeedBase;//计算手动操作加速if FConfig.ConfigData.GetValue('ckSpeedKey/Checked', True) and (MoveKey > 0) thenbeginiSpeedKey := FConfig.ConfigData.GetValue('trcSpeedKey/Position', 1);iSpeed := iSpeed * iSpeedKey * MoveKey;end;//计算自动下落加速if FConfig.ConfigData.GetValue('ckSpeedTimer/Checked', True) and (MoveTimer > 0) thenbeginiSpeedTimer := FConfig.ConfigData.GetValue('trcSpeedKey/Position', 1);iSpeed := iSpeed * iSpeedTimer * MoveTimer;end;//计算时差调整值case FConfig.ConfigData.GetValue('grpTimeCalc/ItemIndex', 1) of0:beginTimerAdjust := iSpeed;end;1:beginTimerAdjust := iSpeed / BoxScore;end;end;TimerInterval := TimerInterval - TimerAdjust;
end;

以下是完成前面的代码之后DoTimerV5的完整代码:

procedure TfrmMain.DoTimerV5;
varbxs: cxBoxs;
beginif not Gaming then Exit;bxs := boxMove.CurrentBoxs;if bxs = nil then Exit;MoveTimer := MoveTimer + 1;if not TryBoxsToDown(bxs) thenbegin//已经到达底部//--打开盒子,放出方块,计算得分
    OpenBoxAndCalcScore(bxs);//--调整计时器时间
    AdjustTimer;//--检查游戏是否结束if boxHeap.RowCount >= grdBox.Rows thenGaming := False;//计数归零MoveKey := 0;MoveTimer := 0;end;
end;  

 

3.2.3 计时器使用方法的调整

完成上面的代码后编译运行应该不存在问题,但是在实际使用中却是出现问题了。经过跟踪调试,找打了出现问题的代码:

procedure TCustomTimer.UpdateTimer;
beginKillTimer;if (FEnabled) and (FInterval > 0)and (([csLoading,csDestroying]*ComponentState=[]))and Assigned (FOnTimer) then begin//DebugLn(['TCustomTimer.UpdateTimer ',dbgsName(Self),' WidgetSet.CreateTimer']);FTimerHandle := WidgetSet.CreateTimer(FInterval, @Timer);if FTimerHandle=0 then beginFTimerHandle:=cIdNoTimer;raise EOutOfResources.Create(SNoTimers);end;if Assigned(OnStartTimer) then OnStartTimer(Self);end;
end; 

在我们的代码中引起这个错误的代码是每次完成一组方块后调整时差的代码:

procedure TfrmMain.SetTimerInterval(AValue: double);
beginif AValue < 0 then AValue := 0;if FTimerInterval = AValue then Exit;FTimerInterval := AValue;//Timer1.Enabled := False;Timer1.Interval := round(AValue);//Timer1.Enabled := True;trcStart.SelEnd := 1000 - round(AValue);
end;  

从理论上来说,这个错误是不应该存在的,但实际操作中却发生了。
仔细分析问题的原因,是设置的新的Interval值后WidgetSet.CreateTimer重新构建计时器对象失败,也就出现了FTimerHandle=0的情况。
由于昨天调试代码时为了查找原因没有截图,今天编写这篇文章时为了获得截图,使用原来的代码重新编译运行,却能够正常运行了。也就是说昨天调试时出现的问题不存在了。
不管为什么不存在了,既然发生过就说明有存在错误的可能。再说解决方案已经完成了,作为一种处理问题的方法,我想在这里把解决方案记录下来。在我们的pTetris项目中可以使用以下方案,也可以保持原来的方案。
为了解决这个问题,我考虑了两种方案:

  • 将计时器的Interval设置为1,这样计时器每毫秒激发一次。定义一个变量TimerNext保存方块盒子下一次跳动的时间,在DoTimerV5中检查当前时间不否达到了TimerNext,没有达到则退出DoTimerV5,达到了才执行DoTimerV5中的代码。这个使用了TimerNext的新版本的DoTimerV5命名为DoTimerV6
  • 不再使用计时器,而改用线程。创建一个线程,这个而线程启动后休眠TimerInterval时间,然后向TfrmMain发送一个消息,在这个消息的处理函数中调用DoTimerV5。这个方法相当于用线程消息代替了计时器消息

这两个方案各有优缺点:

  • 计时器时间比较准确,但多次激发会占用cpu时间
  • 线程计时存在误差,有可能存在延迟,是否消耗cpu由操作系统决定

我们采用了前一种方案。
第一步、在属性列表中将Timer1的Interval属性值改为1。
第二步、在TfrmMain.SetTimerInterval中注释掉以下代码:

  //Timer1.Interval := round(AValue); 

第三步、添加一个新的属性

property TimerNext: double read FTimerNext write SetTimerNext; 

第四步、复制DoTimerV5的代码形成函数DoTimerV6,在DoTimerV6中添加设置和检查TimerNext代码:

procedure TfrmMain.DoTimerV6;
varbxs: cxBoxs;
beginif not Gaming then Exit;if Now < TimerNext then Exit;......if not TryBoxsToDown(bxs) thenbegin//已经到达底部
    ......end;//设置下一次的响应时间TimerNext := Now + TimerInterval / (24 * 3600 * 1000);
end;    

第五步、在TfrmMain.Timer1Timer中用DoTimerV6代替DoTimerV5

经过以上修改后与原来使用DoTimerV5时有同样的效果。

这样我就有了两个方案,这两个方案效果是相同的,可以任选其一:

  • 正统的方案:计时器Timer1.Interval:=TimerInterval,使用DoTimerV5
  • 变通的方案:计时器Timer1.Interval:=1,使用DoTimerV6

3.3 pTetris中起始难度的实现

3.3.1 起始难度的需求分析

以下是我们在《在Lazarus下的Free Pascal编程教程——打造有智能感知的用户设置操作界面 - lexyao - 博客园》确定的难度定制包括以下几个方面:

  • 游戏中可变因素的起始值设置,包括
    • 起始行数:指定游戏开始时已经存在方块的行数。这是一格双刃剑,一方面增加了难度,另一方面提供了游戏开始在高位获得高分的机会
    • 起始速度:指定游戏开始时两次动作之间的间隔毫秒数,取值0-1000毫秒。取值越小则移动速度越快,游戏难度也就越大
  • 游戏中不变的因素,包括:
    • 方块组合样式
    • 可击穿

从以上需求可以看出,作为起始难度的只有两个方面:起始行数和起始速度。下面我们针对以上的要求确定编写代码需要的工作:

  • 起始速度也就是起始难度定制页面中的方块跳动起始时间间隔。这一项的设置我们在加速部分已经实现了,也就是GameBegin函数中的TimerInterval := FConfig.StartTime
  • 起始行数是在开始时在方块移动区下部堆积一部分方块,可以分为两部分来实现:
    • 在cxBoxHeap中添加一个函数StartBox,在指定范围内随机放置一部分方块
    • 在GameBegin中调用StartBox函数实现放置初始方块的操作

3.3.2 起始难度的实现

 根据以上分析,我们编写实现起始行数的代码。
在开始编写代码之前先做一个准备工作:将cxBoxHeap.ClearFullRows中移动并显示堆积的方块的代码提取出来,形成一个函数MoveBox。修改后的代码如下:

procedure cxBoxHeap.ClearFullRows(var iRows, iRowHeight: Integer);
vari: integer;rw: cxDustbin;
begin//销毁满行iRows := 0;iRowHeight:= 0;for i := RowCount - 1 downto 0 dobeginif RowFull(i) thenbeginrw := Rows(i);rw.Free;Inc(iRows);Inc(iRowHeight, i);end;end;//如果有销毁的行,则需要重新显示堆积的方块(向下移动)if iRows > 0 thenbeginMoveBox;end;
end;   procedure cxBoxHeap.MoveBox;
varxy: TPoint;bx: cxBox;rw: cxDustbin;i,j: integer;
beginfor i := 0 to RowCount - 1 dobeginrw := Rows(i);xy.Y := RowToGrid(i);for j := 0 to rw.BoxCount - 1 dobeginbx := rw.BoxByIndex(j);xy.X := bx.Tag;Grid.MoveTo(bx, xy);end;end;
end;   

首先是定义函数cxBoxHeap.StartBox,在指定范围内随机放置方块,再用函数MoveBox将方块显示出来,代码如下:

procedure cxBoxHeap.StartBox(iRows: Integer);
varrow, col: integer;box: cxBox;
begin//添加堆积的方块for row := 0 to iRows - 1 dobeginfor col := 0 to Grid.Cols - 1 dobeginif Random(10) > 5 thenbeginbox := cxBox.Create(Grid);Recycle(box, col, row);end;end;end;//显示堆积的方块
  MoveBox;
end;  

然后是在GameBegin中调用StartBox函数,已经预留了位置,添加一行代码就行了,代码如下:

procedure TfrmMain.GameBegin;
begin......{ #todo : 设置移动区的初始方块 }boxHeap.StartBox(FConfig.ConfigData.GetValue('trcStartHeight/Position', 0));......
end;  

现在可以编译运行pTetris项目测试添加代码后的效果了。
运行pTetris后,在配置表起始难度页面调整方块堆积起始高度为一个大于0的值,比如10,然后重复点击“开始游戏”按钮。每次点击后游戏都会重新开始,在方块移动区下部会有预先设置的方块,每次开始游戏堆积的方块数量和排列都不相同。

 至此,我们编写的pTetris项目已经可以完整地用来玩游戏了。
当然,还有一部分功能没有实现,这些将在今后逐步添加,最终实现我们预定的全部目标。

4.结束语

在这篇文章里我们实现了配置数据的吧保存、恢复、使用。在编写代码的过程中,讲述了xml和json格式数据的使用,并应用xml格式保存了我们的配置数据。在确定保存配置数据的路径时我们使用了一点小技巧,达到了节省代码的效果。
任何一个应用程序都可能用到各种各样的配置数据,管理和使用配置数据的方法会有所不同,但大同小异,道理是一样的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/876106.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

算法学习笔记:扫描线

前言 之前没什么理解,一做就废,最近集训讲了这个,感觉认识深刻了很多,遂写笔记。 这里讲的扫描线,更精确来说指的是离线二维数点,即用扫描线维护一维,DS 维护另一维。 概念 我们把二维数点放到平面上来,那么一个询问或限制就对应平面上的一个矩形,定义这个矩形的 \(\t…

干掉visio,这个画图神器真的绝了!!!

前言 看过我以往文章的小伙伴可能会发现,我的大部分文章都有很多配图。我的文章风格是图文相结合,更便于大家理解。 最近有很多小伙伴发私信问我:文章中的图是用什么工具画的。他们觉得我画的图风格挺小清新的,能够让人眼前一亮。 先上几张图让大家看看效果:说实话,问我的…

面试题|线程池里有几个线程在运行

本文主要改编自https://www.sohu.com/a/391767502_355142。下面从一道面试题引入本文主题~~ 面试官:"假设有一个线程池,核心线程数为10,最大线程数为20,任务队列长度为100。如果现在来了100个任务,那么线程池里有几个线程在运行?" 粉丝豪:"应该是10吧!&…

开关电源1

EMI(参考链接)从左到右分别是安全电容(X电容),共模电感和安全电容(Y电容),黑色线是火线(L),白色线是零线(N),绿色线是地线(G) 大黄块:安全电容X电容,接在火线和零线之间,用于抑制差模干扰 红白相间线圈:共模电感(4个引脚),绕制方法是双线双向,作用是抑…

deepin 25 Preview 安装及体验

deepin 25 Preview(预览版)近期发布。本文让我们一起体验安装和使用感受吧! 下载下载建议用种子文件下载。作为国内屈指可数的厂商,也不套下CDN,下载也仅2M图片接下来,创建虚拟机。(根据自身情况配置虚拟机性能)选Debian系列 注意,安装磁盘容量至少70G 开始安装root登…

02人工智能创新型教师培育计划(第一期)0126

人工智能创新型教师培育计划(第一期)活动更新(1月24日 15:00更新): 感谢各位老师对本次活动的关注与支持,线上课程即将开始,请各位已报名老师注意以下事项: 1. 直播时间:1月25日 19:30—21:00 1月26日 19:30—21:00 2. 直播内容:课题:大模型赋能3小时入门Pyth…

java基础Day7 面向对象(2)

六、继承 Inheritance 6.1 继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。 extends:扩展。子类(派生类)是父类(基类)的扩展。 继承是类与类之间的关系。 java中只有单继承,没有多继承:一个儿子只能有一个爸爸,一个爸爸可以有多个儿子。Inheritance>…

java基础Day7 面向对象

六、继承 Inheritance 6.1 继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。 extends:扩展。子类(派生类)是父类(基类)的扩展。 继承是类与类之间的关系。 java中只有单继承,没有多继承:一个儿子只能有一个爸爸,一个爸爸可以有多个儿子。Inheritance>…

[每日 C] Remove Exactly Two

前言 做一下一场没打的 \(\textrm{div 2}\) 的 \(\rm{C}\) 最近思维能力还在下降, 无敌 前天还能打出思维题, 今天打不出 \(\textrm{div 2 C}\) 思路 首先转化题意给定一个 \(n\) 节点的树, 求删除两个节点及其连边之后, 最大连通块的数量不难发现删除一个节点, 会把树分成几个…

物体检测

滑窗 卷积实现的滑窗参考:Convolutional Implementation of Sliding Windows | Coursera\[y = \begin{bmatrix} p_c \\ b_x \\ b_y \\ b_h \\ b_w \\ c_1 \\ c_2 \\ c_3 \end{bmatrix} \]

k8s启用ingress错误案例

ingress-nginx访问流程:问题: 部署2个nginx-deployment内容v1&&v2,结合ingress作为入口分别访问。访问ingress-入口失败 排错步骤: 1、首先确认2个nginx访问无问题 2、确认宿主机访问不问题 3、访问ingress一直无法连接服务器 到这一步 找不到服务器地址 cm到后端…