在这里插入图片描述

在前面的文章中,我们实现了拼图、记忆翻牌和知识问答三个游戏,学习了状态管理、定时器和数据结构等核心技能。这次我们要实现一个数字消除游戏,这是一个考验数学计算和策略思维的游戏。通过实现这个游戏,你将学习到如何处理数字运算、实现选择逻辑、动态生成游戏内容等技能。

数字消除游戏的玩法

数字消除游戏的玩法很有趣:屏幕上显示一个4x4的数字网格,每个格子里是1到9的随机数字。游戏会给出一个目标数字,玩家需要选择若干个数字,使它们的和等于目标数字。选对后,这些数字会被消除,生成新的数字和新的目标,得分增加。玩家需要快速计算和选择,找到正确的数字组合。

这种玩法简单但有挑战性,既考验玩家的数学计算能力,也考验策略思维。有时候可能有多种组合方式,玩家需要选择最优的方案。游戏的节奏紧凑,每次成功消除都会带来成就感,激发玩家继续挑战的欲望。

页面结构的搭建

NumberGamePage是一个有状态组件,需要管理数字网格和游戏进度:

class NumberGamePage extends StatefulWidget {
  const NumberGamePage({super.key});

  
  State<NumberGamePage> createState() => _NumberGamePageState();
}

使用StatefulWidget是因为游戏状态会随着玩家的操作不断变化。每次玩家选择数字,选中状态就会改变。每次成功消除,数字网格就会重新生成。这些变化都需要通过State类来管理,并通过setState方法触发UI更新。相比前面的游戏,数字消除的状态管理涉及到更多的数学计算。

游戏状态的定义

在State类中,我们需要定义游戏的核心状态:

class _NumberGamePageState extends State<NumberGamePage> {
  List<int> numbers = [];
  int target = 0;
  int score = 0;
  List<int> selected = [];

  
  void initState() {
    super.initState();
    _generateNumbers();
  }

numbers是一个包含16个整数的列表,表示4x4网格中每个位置的数字。初始为空列表,会在initState中生成。target是目标数字,玩家需要选择数字使其和等于这个值。score记录玩家的得分,每次成功消除增加10分。selected是一个索引列表,记录玩家选中的数字位置。

initState方法在页面创建时调用一次,我们在这里调用_generateNumbers方法生成初始的数字网格和目标。这种在initState中初始化游戏的做法,确保用户看到页面时游戏已经准备好了。与前面的游戏不同,这个游戏的初始状态需要通过计算生成,而不是固定的。

数字生成逻辑

游戏开始和每次成功消除后,都需要生成新的数字网格和目标:

void _generateNumbers() {
  final random = Random();
  setState(() {
    numbers = List.generate(16, (_) => random.nextInt(9) + 1);
    target = random.nextInt(15) + 5;
    selected.clear();
  });
}

使用Random类生成随机数。List.generate创建一个包含16个元素的列表,每个元素是1到9的随机整数。random.nextInt(9)生成0到8的随机数,加1后得到1到9。target设置为5到19的随机数,这个范围确保游戏有合适的难度,既不会太简单也不会太难。

selected.clear()清空选中列表,准备新一轮的选择。这里使用setState包裹状态更新,触发UI重新构建,新的数字会立即显示在屏幕上。每次生成的数字和目标都不同,确保游戏的可玩性和挑战性。

Random类的使用很简单,但背后的随机算法很复杂。Dart使用线性同余生成器,这是一个经典的伪随机数生成算法。虽然不是真正的随机,但对于游戏来说已经足够了。如果需要更高质量的随机数,可以使用Random.secure(),但性能会稍差。

数字选择逻辑

当玩家点击数字时,需要处理选择逻辑:

void _selectNumber(int index) {
  if (selected.contains(index)) {
    setState(() => selected.remove(index));
  } else {
    setState(() => selected.add(index));
  }
  _checkSum();
}

_selectNumber方法接收数字的索引作为参数。首先检查这个索引是否已经在selected列表中。如果在,说明这个数字已经被选中,点击是取消选择,从列表中移除。如果不在,说明这是新选择,添加到列表中。

这种切换选择状态的逻辑很常见,在很多应用中都会用到。使用contains方法检查元素是否存在,使用remove方法移除元素,使用add方法添加元素。这些都是List的基本操作,简单但实用。

每次选择改变后,立即调用_checkSum方法检查总和是否等于目标。这种实时检查的设计,让玩家可以立即看到结果,不需要额外的确认操作。如果总和正确,游戏会自动进入下一轮,保持流畅的游戏节奏。

总和检查逻辑

检查选中数字的总和是否等于目标是游戏的核心逻辑:

void _checkSum() {
  int sum = selected.fold(0, (prev, i) => prev + numbers[i]);
  if (sum == target) {
    setState(() {
      score += 10;
      _generateNumbers();
    });
  }
}

使用fold方法计算总和。fold是一个归约操作,接收初始值和归约函数。归约函数接收累积值prev和当前元素i,返回新的累积值。这里我们将selected列表中的索引映射到numbers列表中的数字,然后累加。

fold方法虽然看起来复杂,但其实很强大。它可以将列表归约成单个值,不仅可以用于求和,还可以用于求积、求最大值、字符串拼接等。掌握fold方法,可以让代码更加简洁优雅。

如果总和等于目标,增加得分,调用_generateNumbers生成新的数字和目标。这里使用setState包裹状态更新,触发UI重新构建。玩家会看到得分增加,数字网格刷新,新的目标出现,可以继续下一轮游戏。

页面UI的构建

游戏页面的UI包括AppBar、得分显示、目标显示和数字网格:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('数字消除'),
      backgroundColor: const Color(0xFF16213e),
    ),
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('得分: $score', 
             style: TextStyle(fontSize: 24.sp, 
                            fontWeight: FontWeight.bold, 
                            color: Colors.amber)),
        SizedBox(height: 20.h),

AppBar的标题显示"数字消除",背景色使用深蓝色。Column垂直排列页面内容,mainAxisAlignment设置为center让内容居中显示。最上面显示得分,使用24号粗体琥珀色字体,非常醒目。琥珀色给人一种金色的感觉,象征着成就和奖励。

目标和选中总和的显示:

        Text('目标数字: $target', 
             style: TextStyle(fontSize: 28.sp, 
                            fontWeight: FontWeight.bold)),
        SizedBox(height: 10.h),
        Text('选中总和: ${selected.fold(0, (prev, i) => prev + numbers[i])}', 
             style: TextStyle(fontSize: 18.sp)),
        SizedBox(height: 30.h),

目标数字使用28号粗体字显示,让玩家清楚地看到需要达到的目标。选中总和实时计算并显示,使用与_checkSum相同的fold方法。这种实时反馈让玩家可以随时知道当前选择的总和,不需要自己心算。

注意这里在build方法中直接计算选中总和,虽然每次重建都会计算,但计算量很小,不会影响性能。如果计算量很大,可以考虑将结果缓存到状态变量中,避免重复计算。

数字网格的实现

数字网格是游戏的核心部分,展示所有的数字:

        Container(
          width: 320.w,
          height: 320.w,
          padding: EdgeInsets.all(8.w),
          child: GridView.builder(
            physics: const NeverScrollableScrollPhysics(),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 4,
              crossAxisSpacing: 8,
              mainAxisSpacing: 8,
            ),

Container设置宽度和高度都为320个单位,创建一个正方形的游戏区域。padding设置8个单位的内边距。GridView.builder用于构建4x4的网格,physics设置为NeverScrollableScrollPhysics禁用滚动。gridDelegate定义网格布局:crossAxisCount为4表示每行4个数字,crossAxisSpacing和mainAxisSpacing设置数字之间的间距为8个单位。

这种4x4的布局可以容纳16个数字,在手机屏幕上显示效果很好。数字之间的间距让每个数字都有明确的边界,玩家不会误触。Container的尺寸经过精心设计,既不会太大占满整个屏幕,也不会太小看不清数字。

数字格子的构建:

            itemCount: 16,
            itemBuilder: (context, index) {
              return GestureDetector(
                onTap: () => _selectNumber(index),
                child: Container(
                  decoration: BoxDecoration(
                    color: selected.contains(index) 
                        ? Colors.amber 
                        : Colors.purpleAccent,
                    borderRadius: BorderRadius.circular(12.r),
                  ),
                  child: Center(
                    child: Text('${numbers[index]}', 
                                style: TextStyle(fontSize: 28.sp, 
                                               fontWeight: FontWeight.bold)),
                  ),
                ),
              );
            },
          ),
        ),
      ],
    ),
  );
}

itemCount设置为16,表示网格中有16个数字。itemBuilder负责构建每个数字格子,GestureDetector包裹格子,点击时调用_selectNumber方法。Container的颜色根据选中状态决定:如果被选中使用琥珀色,否则使用紫色。borderRadius设置12个单位的圆角,让格子看起来更加柔和。

数字使用28号粗体字显示,居中对齐。这种大号的数字在小小的格子中非常醒目,玩家可以清楚地看到每个数字。琥珀色的选中状态与紫色的未选中状态有明显的视觉区别,玩家可以清楚地看到自己选择了哪些数字。

fold方法的深入理解

fold是Dart中非常强大的列表操作方法,让我们深入了解一下它的用法:

int sum = selected.fold(0, (prev, i) => prev + numbers[i]);

fold方法接收两个参数:初始值和归约函数。初始值是0,表示从0开始累加。归约函数接收两个参数:prev是累积值,i是当前元素。函数返回新的累积值。

fold的执行过程是这样的:首先将初始值0作为prev,selected的第一个元素作为i,调用归约函数得到新的prev。然后将新的prev和selected的第二个元素作为i,再次调用归约函数。重复这个过程,直到处理完所有元素,最后的prev就是结果。

除了求和,fold还可以用于很多其他场景:

// 求积
int product = numbers.fold(1, (prev, n) => prev * n);

// 求最大值
int max = numbers.fold(numbers[0], (prev, n) => prev > n ? prev : n);

// 字符串拼接
String str = words.fold('', (prev, w) => prev + w);

// 构建Map
Map<int, int> map = numbers.fold({}, (prev, n) {
  prev[n] = (prev[n] ?? 0) + 1;
  return prev;
});

fold是函数式编程的核心概念之一,掌握它可以让代码更加简洁优雅。虽然也可以用for循环实现相同的功能,但fold更加声明式,表达了"归约"这个操作的本质。

随机数生成的策略

游戏的可玩性很大程度上取决于随机数生成的策略。让我们深入分析一下我们的策略:

numbers = List.generate(16, (_) => random.nextInt(9) + 1);
target = random.nextInt(15) + 5;

数字范围是1到9,这个范围既不会太小导致重复太多,也不会太大导致难以计算。目标范围是5到19,这个范围确保至少需要选择2个数字(最小的情况是1+1+1+1+1=5),最多可能需要选择多个数字(比如9+9+1=19)。

这种随机策略确保游戏有合适的难度。如果目标太小,比如2或3,玩家只需要选择一两个数字就能完成,太简单。如果目标太大,比如50,可能需要选择很多数字,甚至可能无解,太难。

可以根据玩家的得分动态调整难度:

void _generateNumbers() {
  final random = Random();
  final difficulty = min(score ~/ 50, 5); // 难度等级0-5
  setState(() {
    numbers = List.generate(16, (_) => random.nextInt(9 + difficulty) + 1);
    target = random.nextInt(15 + difficulty * 3) + 5;
    selected.clear();
  });
}

随着得分增加,数字范围和目标范围都会增大,游戏难度逐渐提高。这种动态难度调整可以让游戏保持挑战性,避免玩家感到无聊。

游戏策略的思考

数字消除游戏不仅考验计算能力,还考验策略思维。有时候可能有多种组合方式达到目标,玩家需要选择最优的方案。

比如目标是10,网格中有[1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7]。可能的组合有:

  • 1+9 = 10
  • 2+8 = 10
  • 3+7 = 10
  • 4+6 = 10
  • 1+2+3+4 = 10
  • 1+2+7 = 10

选择哪种组合呢?从策略角度,应该选择使用数字最少的组合,这样可以保留更多的数字用于下一轮。但从游戏设计角度,我们没有限制玩家的选择,任何正确的组合都可以。

可以添加奖励机制,鼓励玩家使用更少的数字:

void _checkSum() {
  int sum = selected.fold(0, (prev, i) => prev + numbers[i]);
  if (sum == target) {
    setState(() {
      score += 10 + (5 - selected.length) * 2; // 使用数字越少,奖励越多
      _generateNumbers();
    });
  }
}

这种奖励机制可以增加游戏的策略性,让玩家不仅要找到正确的组合,还要找到最优的组合。

用户体验的优化

数字消除游戏的用户体验设计注重了几个方面。首先是实时反馈,选中总和实时显示,玩家可以随时知道当前的状态。其次是自动检查,不需要额外的确认按钮,选对后自动进入下一轮。

颜色的选择也很重要。琥珀色的选中状态与紫色的未选中状态有明显的对比,玩家可以清楚地看到自己的选择。得分使用琥珀色显示,给人一种成就感。

可以添加更多的反馈机制提升体验:

void _checkSum() {
  int sum = selected.fold(0, (prev, i) => prev + numbers[i]);
  if (sum == target) {
    // 播放成功音效
    // AudioPlayer.play('success.mp3');
    
    // 显示成功动画
    // showDialog(context: context, builder: (_) => SuccessDialog());
    
    setState(() {
      score += 10;
      _generateNumbers();
    });
  } else if (sum > target) {
    // 总和超过目标,给出提示
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('总和超过目标了!')),
    );
  }
}

这些反馈机制可以让游戏更加生动有趣,提高玩家的参与度。

性能优化的考虑

数字消除游戏的性能表现很好,因为游戏逻辑简单,UI更新频率不高。每次选择数字时,只有selected列表会改变,Flutter会精确地重建受影响的Widget。

GridView.builder只渲染16个数字,即使频繁更新也不会影响性能。fold方法虽然看起来复杂,但实际上只是简单的循环累加,计算量很小。

需要注意的是,我们在build方法中计算选中总和:

Text('选中总和: ${selected.fold(0, (prev, i) => prev + numbers[i])}')

这意味着每次重建都会计算一次。虽然计算量很小,但如果selected列表很长,可能会影响性能。可以将结果缓存到状态变量中:

int selectedSum = 0;

void _selectNumber(int index) {
  setState(() {
    if (selected.contains(index)) {
      selected.remove(index);
    } else {
      selected.add(index);
    }
    selectedSum = selected.fold(0, (prev, i) => prev + numbers[i]);
  });
  _checkSum();
}

这样只在选择改变时计算一次,build方法中直接使用缓存的值。这是一个经典的空间换时间的优化策略。

扩展功能的思考

数字消除游戏还有很多可以扩展的功能。比如可以添加时间限制,增加游戏的紧张感。可以添加提示功能,帮助玩家找到正确的组合。可以添加难度选择,让玩家可以选择不同的挑战。

还可以添加特殊数字,比如乘法数字(选中后将其他数字乘以2)、减法数字(可以减少总和)等。可以添加连击系统,连续成功消除可以获得额外奖励。可以添加排行榜,让玩家可以与其他玩家比较成绩。

这些扩展功能的实现都不需要大幅修改现有代码。比如添加时间限制,只需要添加一个Timer和一个时间变量。添加特殊数字,只需要在numbers列表中添加特殊标记,在计算总和时特殊处理。良好的代码组织让扩展变得容易。

总结

本文详细介绍了数字消除游戏的实现。我们从游戏玩法设计开始,定义了游戏状态,实现了数字生成、选择和检查等核心逻辑,最后构建了游戏的UI。每个部分都有详细的代码和讲解,帮助你理解实现的原理。

数字消除游戏涉及到数学计算和策略思维,通过实现这个游戏,你学习到了如何使用Random生成随机数,如何使用fold方法进行列表归约,如何实现实时反馈的交互逻辑。我们还深入探讨了随机数生成策略、游戏平衡性设计、性能优化等高级话题。

这些技能不仅适用于数字消除游戏,也适用于其他需要数学计算和策略思维的应用。fold方法是函数式编程的核心概念,掌握它可以让代码更加简洁优雅。随机数生成在很多游戏中都会用到,了解其原理和策略很重要。

接下来的文章中,我们会继续实现其他游戏。每个游戏都有不同的玩法和实现方式,通过学习这些游戏,你将掌握更多的开发技巧,成为一名优秀的Flutter开发者。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐