通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip

前言

2048是一个数字合并游戏,滑动方块,相同数字合并翻倍。这个游戏有个特点:不同数字的方块颜色不一样。

2是浅色,4稍微深一点,8、16、32越来越深,到2048就是金黄色。这种颜色渐变让玩家一眼就能看出方块的大小。

这篇来聊聊2048的方块颜色怎么实现。
请添加图片描述

方块的基本结构

Widget _buildTile(int value) {
  return Container(
    decoration: BoxDecoration(color: _getTileColor(value), borderRadius: BorderRadius.circular(4)),
    child: Center(
      child: Text(value == 0 ? '' : '$value',
          style: TextStyle(fontSize: value > 512 ? 20 : 28, fontWeight: FontWeight.bold, color: value <= 4 ? Colors.grey[700] : Colors.white)),
    ),
  );
}

每个方块是一个Container,里面放数字。

圆角

borderRadius: BorderRadius.circular(4),

4像素的小圆角,让方块看起来柔和一些。

数字显示

Text(value == 0 ? '' : '$value',

值为0时不显示(空格子),否则显示数字。

字号自适应

fontSize: value > 512 ? 20 : 28,

数字大了位数多,字号要小一点才能放得下。

512以下用28号字,512以上用20号字。1024、2048这些四位数用小字号。

💡 512这个分界点是试出来的。一开始用1000,但1024显示不全。改成512刚好。

文字颜色

color: value <= 4 ? Colors.grey[700] : Colors.white,

2和4的方块颜色很浅,用深灰色文字。8以上的方块颜色深,用白色文字。

这样保证文字在任何背景上都清晰可读。

_getTileColor方法

颜色映射的核心:

Color _getTileColor(int value) {
  return {0: Colors.brown[200], 2: Colors.orange[50], 4: Colors.orange[100], 8: Colors.orange[300],
    16: Colors.orange[400], 32: Colors.orange[500], 64: Colors.orange[600], 128: Colors.yellow[300],
    256: Colors.yellow[400], 512: Colors.yellow[500], 1024: Colors.yellow[600], 2048: Colors.yellow[700],
  }[value] ?? Colors.yellow[800]!;
}

用Map把数字映射到颜色。

空格子

0: Colors.brown[200],

空格子用浅棕色,和背景接近但能区分。

2和4

2: Colors.orange[50], 
4: Colors.orange[100],

最浅的橙色,几乎是白色。这是游戏开始时最常见的方块。

8到64

8: Colors.orange[300],
16: Colors.orange[400], 
32: Colors.orange[500], 
64: Colors.orange[600],

橙色逐渐加深。这个阶段是游戏中期,方块开始有明显颜色了。

128到2048

128: Colors.yellow[300],
256: Colors.yellow[400], 
512: Colors.yellow[500], 
1024: Colors.yellow[600], 
2048: Colors.yellow[700],

切换到黄色系,代表高分方块。2048是金黄色,很有成就感。

超过2048

}[value] ?? Colors.yellow[800]!;

如果数字不在Map里(比如4096、8192),用最深的黄色。

??是空值合并运算符,Map里没找到就用后面的默认值。

颜色渐变的设计思路

为什么用这套颜色?

  1. 从浅到深:数字越大颜色越深,直观
  2. 橙色到黄色:暖色调,有活力
  3. 和原版一致:2048原版游戏就是这个配色,玩家熟悉

颜色不是随便选的,是参考原版游戏调的。

棋盘背景

Container(
  margin: const EdgeInsets.all(16),
  padding: const EdgeInsets.all(8),
  decoration: BoxDecoration(color: Colors.brown[300], borderRadius: BorderRadius.circular(8)),

棋盘用棕色背景,和方块的橙黄色形成对比。

margin和padding

margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(8),

margin让棋盘不贴着屏幕边缘,padding让方块不贴着棋盘边缘。

圆角

borderRadius: BorderRadius.circular(8),

棋盘圆角比方块大一点(8 vs 4),层次分明。

GridView布局

GridView.builder(
  physics: const NeverScrollableScrollPhysics(),
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4, mainAxisSpacing: 8, crossAxisSpacing: 8),
  itemCount: 16,
  itemBuilder: (_, i) => _buildTile(grid[i ~/ 4][i % 4]),
),

禁止滚动

physics: const NeverScrollableScrollPhysics(),

2048的棋盘是固定的,不需要滚动。

4x4网格

crossAxisCount: 4,

每行4个方块。

间距

mainAxisSpacing: 8, 
crossAxisSpacing: 8,

方块之间8像素间距,露出棕色背景,形成网格线效果。

索引转换

grid[i ~/ 4][i % 4]

GridView用线性索引(0-15),要转成二维坐标访问grid数组。

i ~/ 4是行号,i % 4是列号。

AspectRatio保持正方形

AspectRatio(
  aspectRatio: 1,
  child: Container(

棋盘必须是正方形,用AspectRatio强制1:1比例。

分数显示

Padding(
  padding: const EdgeInsets.all(16),
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceAround,
    children: [
      Text('分数: $score', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
      if (gameOver) const Text('游戏结束!', style: TextStyle(fontSize: 20, color: Colors.red)),
    ],
  ),
),

分数在棋盘上方,大字号加粗。

游戏结束时显示红色提示。

数据结构

static const int gridSize = 4;
late List<List<int>> grid;

4x4的二维数组,存储每个格子的数字。0表示空。

初始化

void _initGame() {
  grid = List.generate(gridSize, (_) => List.filled(gridSize, 0));
  score = 0;
  gameOver = false;
  _addRandomTile();
  _addRandomTile();
}

全部填0,然后随机放两个方块。

添加随机方块

void _addRandomTile() {
  List<Point<int>> empty = [];
  for (int i = 0; i < gridSize; i++) {
    for (int j = 0; j < gridSize; j++) {
      if (grid[i][j] == 0) empty.add(Point(i, j));
    }
  }
  if (empty.isEmpty) return;
  Point<int> pos = empty[random.nextInt(empty.length)];
  grid[pos.x][pos.y] = random.nextDouble() < 0.9 ? 2 : 4;
}

找出所有空格子,随机选一个,90%概率放2,10%概率放4。

这是2048的标准规则。

颜色的可扩展性

如果想支持更大的数字,可以用计算的方式:

Color _getTileColor(int value) {
  if (value == 0) return Colors.brown[200]!;
  int log = (log2(value)).floor(); // 2->1, 4->2, 8->3, ...
  if (log <= 6) {
    return Colors.orange[(log * 100).clamp(50, 600)]!;
  } else {
    return Colors.yellow[((log - 6) * 100 + 300).clamp(300, 900)]!;
  }
}

但Map的方式更直观,而且2048游戏很少能玩到4096以上。

颜色的心理学

为什么2048用橙黄色系?这不是随便选的。

暖色调的活力

橙色和黄色都是暖色调,给人温暖、活力、积极的感觉。

玩2048是一个不断挑战、不断进步的过程,暖色调能激发玩家的斗志。

如果用冷色调(蓝色、绿色),游戏会显得冷静、理性,少了一些激情。

金色的成就感

2048方块是金黄色,这是有意为之的。

金色在人类文化中代表"珍贵"、“成功”、“胜利”。当玩家终于合成2048时,看到金黄色的方块,成就感油然而生。

这是游戏设计中的正向反馈,用视觉奖励玩家的努力。

渐变的进度感

从浅橙到深黄的渐变,让玩家有一种"进度"的感觉。

颜色越深,说明数字越大,离目标越近。这种视觉反馈帮助玩家判断当前局势。

颜色与可访问性

设计颜色时要考虑色盲用户。

红绿色盲

最常见的色盲是红绿色盲,患者难以区分红色和绿色。

2048用的是橙黄色系,不涉及红绿对比,对红绿色盲用户友好。

对比度

文字和背景的对比度要足够高,否则看不清。

color: value <= 4 ? Colors.grey[700] : Colors.white,

浅背景用深色字(grey[700]),深背景用白色字,保证对比度。

WCAG(Web内容无障碍指南)建议正文对比度至少4.5:1。虽然这是Web标准,但移动端也可以参考。

不只依赖颜色

好的设计不只依赖颜色传递信息。2048的方块上有数字,即使看不清颜色,也能通过数字知道方块的值。

这是冗余设计,用多种方式传递同一信息,提高可访问性。

动画效果

当前实现没有动画,方块是瞬间出现的。如果想加动画,可以用AnimatedContainer:

AnimatedContainer(
  duration: const Duration(milliseconds: 200),
  decoration: BoxDecoration(
    color: _getTileColor(value),
    borderRadius: BorderRadius.circular(4),
  ),
  child: Center(child: Text(...)),
)

AnimatedContainer会自动对属性变化做动画。当颜色变化时(比如2变成4),会有平滑过渡。

缩放动画

新方块出现时可以加缩放动画:

TweenAnimationBuilder<double>(
  tween: Tween(begin: 0.0, end: 1.0),
  duration: const Duration(milliseconds: 200),
  builder: (context, scale, child) {
    return Transform.scale(scale: scale, child: child);
  },
  child: _buildTile(value),
)

方块从0缩放到1,有一个"弹出"的效果。

但动画会增加复杂度,当前实现简化了。

主题切换

如果想支持多种配色主题,可以把颜色映射抽成配置:

class TileTheme {
  final Map<int, Color> colors;
  final Color textLightColor;
  final Color textDarkColor;
  
  const TileTheme({
    required this.colors,
    required this.textLightColor,
    required this.textDarkColor,
  });
}

final classicTheme = TileTheme(
  colors: {0: Colors.brown[200]!, 2: Colors.orange[50]!, ...},
  textLightColor: Colors.grey[700]!,
  textDarkColor: Colors.white,
);

final darkTheme = TileTheme(
  colors: {0: Colors.grey[800]!, 2: Colors.blueGrey[700]!, ...},
  textLightColor: Colors.white70,
  textDarkColor: Colors.white,
);

然后根据用户选择的主题使用不同的TileTheme。

这样可以支持经典主题、暗黑主题、护眼主题等多种配色。

性能优化

const优化

borderRadius: BorderRadius.circular(4),

这行代码每次build都会创建新的BorderRadius对象。可以优化成:

static final _borderRadius = BorderRadius.circular(4);
...
borderRadius: _borderRadius,

用static final缓存,避免重复创建。

颜色缓存

Color _getTileColor(int value) {
  return {...}[value] ?? Colors.yellow[800]!;
}

每次调用都创建一个新的Map。可以优化成:

static final _tileColors = {
  0: Colors.brown[200]!,
  2: Colors.orange[50]!,
  // ...
};

Color _getTileColor(int value) {
  return _tileColors[value] ?? Colors.yellow[800]!;
}

用static final缓存Map,只创建一次。

对于2048这种简单游戏,这些优化不是必须的,但养成好习惯有益无害。

小结

这篇讲了2048的方块颜色,核心知识点:

  • Map颜色映射:数字到颜色的对应关系,简洁直观
  • 颜色渐变:从浅橙到深黄,数字越大越深,有进度感
  • 文字颜色适配:浅背景用深色字,深背景用白色字,保证对比度
  • 字号自适应:大数字用小字号,确保显示完整
  • GridView布局:4x4网格,8像素间距,形成网格线效果
  • AspectRatio:保持棋盘正方形,不会因屏幕比例变形
  • 空值合并运算符:处理Map中不存在的key,提供默认值
  • 颜色心理学:暖色调激发斗志,金色带来成就感
  • 可访问性:考虑色盲用户,保证对比度,不只依赖颜色

颜色是2048的重要视觉元素,好的配色让游戏更有层次感,也能给玩家带来更好的体验。


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

Logo

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

更多推荐