【M5Stack物联网开发】第四章 用户界面

1 交互设计

交互设计(Interaction Design,简称IxD)是一种专注于创造有意义的关系,介于人与人、人与产品或服务之间的设计领域。这种设计形式主要关注于如何使产品、系统或服务与用户之间的交互更加有效、效率高、直观和愉悦。

交互设计的核心目标是提高用户体验和满意度,通过改善产品的可用性和可访问性来实现。它涉及到多个方面,包括:

  • 可用性:设计易于使用和理解的产品。
  • 功能性:确保产品功能满足用户的需求。
  • 情感化:设计能引发用户情感共鸣的产品。
  • 美学:产品的视觉和触觉设计。
  • 交互逻辑:用户与产品交互的流程和逻辑。

交互设计师常常需要考虑到用户的行为模式,并通过设计来引导和改善这些行为。常用的方法和工具包括用户研究、原型设计、用户测试和迭代设计等。交互设计不仅仅应用于数字产品,如网站和应用程序,也广泛应用于物理产品和服务的设计中。

1.1 UI设计

UI设计,即用户界面设计(User Interface Design),是交互设计的一个重要分支,专注于用户与产品界面之间的交互方式。UI设计的核心目标是通过创建直观、美观且功能性强的界面来提升用户体验。这涉及到视觉元素的设计,如布局、颜色、图标、按钮等,以及它们如何组织和工作以支持用户操作。

UI设计的主要组成部分包括:

  1. 布局:界面元素的组织和结构,决定了用户如何导航和使用产品。
  2. 视觉元素:
    • 颜色:使用合适的颜色方案可以影响用户的情绪和注意力,同时强化品牌认知。
    • 字体:选择易读且美观的字体来提升信息的可读性和界面的美观度。
    • 图标和按钮:设计直观的图标和按钮,使用户能快速理解并执行操作。
  3. 交互:设计元素的交互方式,如点击、滑动、拖拽等,这些都是用户与界面互动的方式。
  4. 动画和过渡:合理的动画和过渡效果可以提升用户体验,使界面感觉更加流畅和自然。

示例:

1. 移动应用的UI设计:

考虑一个常见的社交媒体应用,如Instagram。其UI设计包括:

  • 布局:清晰的导航栏位于屏幕底部,方便用户使用单手操作。
  • 颜色方案:使用温和的色调,以视觉上不过分刺激用户,同时保持品牌识别。
  • 图标设计:图标直观,如放大镜代表搜索,心形代表喜欢,这些都是用户容易理解和记忆的。
  • 动画:用户点赞时的心形图标会有弹跳效果,增加了互动的趣味性。

2. 网站UI设计:

以Amazon的在线购物网站为例。其UI设计特点包括:

  • 布局:清晰的分类导航,搜索栏位于页面顶部,方便用户快速找到所需商品。
  • 颜色和字体:使用品牌色和清晰的字体,确保用户能够轻松阅读和识别不同类别和功能。
  • 图标和按钮:购物车图标明显,结账按钮颜色鲜明,引导用户进行购买

1.2 UX设计

UX设计,即用户体验设计(User Experience Design),是交互设计中关注用户在使用产品或服务过程中的体验的一个领域。UX设计不仅仅涉及到界面的美观,更重要的是整体体验的流畅性、效率、易用性以及满足用户需求的程度。

UX设计的主要组成部分包括:

  1. 用户研究:了解目标用户的需求、行为和心理特点。
  2. 信息架构:组织和结构化信息,使用户能够容易地找到所需内容。
  3. 用户流程(User Flows):设计用户在应用中的行动路径,优化任务完成的效率。
  4. 交互设计:确保用户与产品的交互简单直观。
  5. 原型和线框图(Wireframing and Prototyping):创建初步设计模型,用于测试和改进设计。
  6. 可用性测试:通过用户测试来检验设计的有效性,确保用户能够顺利使用产品。

示例:

1. 网站的UX设计:

以一个在线图书馆网站为例。其UX设计特点可能包括:

  • 用户研究:调查用户最常查找的图书类型和使用设备,从而确定重点优化的功能。
  • 信息架构:将图书分类清晰地展示,如文学、科技、历史等,以及一个明显的搜索功能。
  • 用户流程:简化借书流程,用户可以在几步内完成借书,包括搜索、选择、借阅和确认。
  • 原型和线框图:设计初步的页面布局,通过迭代改进以提升用户体验。
  • 可用性测试:邀请真实用户测试网站,收集反馈并优化设计。

2. 移动应用的UX设计:

考虑一个健康追踪应用,如Fitbit。其UX设计特点包括:

  • 用户研究:了解用户在使用健康追踪应用时的主要需求,如记录运动、监测睡眠和饮食。
  • 信息架构:清晰地展示健康数据,如步数、心率、卡路里消耗等。
  • 用户流程:优化添加新的健康记录的流程,确保用户可以轻松输入和查看数据。
  • 交互设计:设计简单直观的交互元素,如滑动切换不同的健康指标。
  • 可用性测试:定期进行用户测试,确保新功能的可用性和解决用户反馈的问题。

2 M5Stack Core2中的交互技术

2.1 使用按钮

M5Stack Core2 v1.1 的虚拟按钮是通过其电容触控屏实现的,这为用户提供了更多的交互可能性。虚拟按钮位于屏幕的底部,从左侧开始分别为BtnA、BtnB和BtnC,三个按钮可以在软件中自定义其功能,包括点击(Press)、释放(Release)、长按(Hold)等操作,非常适合需要用户界面交互的应用。

M5Unified中的M5.BtnA、M5.BtnB、M5.BtnC三个模块分别对应按钮的各个操作。

复制代码
#include <M5Unified.h>

void setup()
{
    auto cfg = M5.config();
    M5.begin(cfg);
}

void loop()
{
    // 只要按压(Press)即可,与点击(Click)不同
    // 点击(Click)是按压后快速释放(Release)
    if(M5.BtnA.isPressed())
        M5.Display.println("BtnA is Pressed");

    // 按压(Press)2000毫秒(2秒)后触发
    // 同样,不需要点击(Click)的释放(Release)操作
    if(M5.BtnB.pressedFor(2000))
        M5.Display.println("BtnB pressed for 2000 ms");

    // 点击(Click),包含两个动作
    // 按压(Press)与快速释放(Release)
    if(M5.BtnC.getState() == M5.BtnC.state_clicked)
        M5.Display.println("BtnC has clicked state");

    delay(100);
    // 更新所有硬件状态
    M5.update();
}
复制代码

2.2 使用触摸屏

M5Stack Core2 具有一块集成的触摸屏。这块触摸屏是一个3.5英寸的彩色TFT LCD显示屏,分辨率为320x240像素。当前版本中,它支持最多2个触控点的多点触控,这对于开发交互式应用程序非常有用。

M5.Touch 是M5Unified API中用于处理触摸屏输入的一个模块。该模块提供了一系列函数,使开发者能够轻松地获取和处理来自Core2触摸屏的输入数据。

复制代码
#include <M5Unified.h>

void setup()
{
    auto cfg = M5.config();
    M5.begin(cfg);

    M5.Display.println("Tounch anywhere");
}

void loop()
{
    // 获取产生了几个触控点,Core2最多支持两点触控
    auto count = M5.Touch.getCount();

    if (count != 0)
        M5.Display.printf("%d ", count);

    for (int i = 0; i < count; i++)
    {
        // 获取触控点信息的方式有两种
        // 简单的获取触控位置:getTouchPointRaw
        // 获取更丰富的交互信息,比如Press、Click、Drag等:getDetail
        // 参数是获取第几个触控点的信息,触控点的索引从0开始
        auto detail = M5.Touch.getDetail(i);
        auto rowXY = M5.Touch.getTouchPointRaw(i);

        // getDetail中的x、y与getTouchPointRaw中的x、y是同一个数据
        M5.Display.printf("T%d (%d,%d | %d) ", i, rowXY.x, rowXY.y, rowXY.size);
        M5.Display.printf("detail (%d,%d | %d, %d) ", detail.x, detail.y, detail.deltaX(), detail.deltaY());
        
        // 拖动(Drag)是由两个行为构成的:保持(Hold)与拖动(Drag)
        if (detail.isDragging())
            M5.Display.print("dragging ");

        // 点击(Click)也是有两个行为构成的:按压(Press)与快速释放(Release)
        if (detail.wasClicked())
            M5.Display.print("clicked ");
            
        M5.Display.printf("\n");
    }

    delay(100);
    // 更新所有硬件状态
    M5.update();
}
复制代码

2.3 制作一个按钮(Button)

M5Unified API中的M5GFX模块已经包含了一个简单的按钮,本小节我们将从头自己做一个Button。

  • 绘制一个Button的外框
  • 在Button的正中间显示Button的文字(Label)
  • 检测用户是否点击了Button
复制代码
#include <M5Unified.h>

// core2 screen 320x240
#define BtnX 100
#define BtnY 90
#define BtnW 120
#define BtnH 60

void setup()
{
    auto cfg = M5.config();
    M5.begin(cfg);

    // 在屏幕上填充一个矩形,表示按钮的边框
    M5.Display.fillRect(BtnX, BtnY, BtnW, BtnH, WHITE);

    // drawCenterString是在将文字的中文位置放置在(X,Y)
    // 然后在屏幕上绘制文字
    M5.Display.setTextColor(RED);
    M5.Display.drawCenterString("Click Me", BtnX + BtnW / 2, BtnY + BtnH / 2);
}

void loop()
{
    if (M5.Touch.getCount() == 1)
    {
        auto rawXY = M5.Touch.getTouchPointRaw(0);

        // 只有当触摸的位置,在按钮范围内
        // 且触控的交互行为是点击(Click)时
        // 才进行对应操作
        if (rawXY.x > BtnX && rawXY.x < BtnX + BtnW 
        && rawXY.y > BtnY && rawXY.y < BtnY + BtnH 
        && M5.Touch.getDetail(0).wasClicked())
            M5.Display.print("Clicked ");
    }

    delay(100);
    // 更新所有硬件状态
    M5.update();
}
复制代码

 

3 游戏开发概论

3.1 游戏主循环

游戏主循环(Game Loop)是游戏运行中最核心的部分,它负责控制游戏的整体流程和时间管理。游戏主循环确保游戏的状态持续更新,并且使得游戏能够响应用户输入,同时还要渲染图形输出到屏幕上。这个循环在游戏启动后会不断重复执行,直到游戏关闭。

游戏主循环的主要组成部分包括:

  1. 处理输入(Input Handling):

    • 游戏需要检测并响应玩家的输入,如键盘、鼠标或游戏手柄的操作。这一步骤确保玩家的操作能够被游戏捕捉并作出相应的反应。
  2. 更新游戏状态(Updating Game State):

    • 游戏逻辑需要更新游戏世界的状态,包括角色的移动、得分变化、AI决策等。这一步骤是游戏动态变化的核心,涉及物理计算、碰撞检测、游戏规则的执行等。
  3. 渲染输出(Rendering):

    • 一旦游戏状态更新完成,游戏引擎需要将这些信息转化为图形输出,显示到屏幕上。这包括3D模型的渲染、场景的绘制、UI元素的显示等。
  4. 维护时间管理(Time Management):

    • 确保游戏的运行速度与真实时间同步。这通常通过控制每次循环的时间(即帧率)来实现,确保游戏播放流畅且不会因为硬件性能差异而有所不同。

3.2 游戏引擎

游戏引擎是一种为开发视频游戏提供核心功能的软件开发环境,它帮助开发者通过提供编写游戏所需的多种功能和工具来简化游戏开发过程。常见的游戏引擎包括Unity、Unreal Engine等,它们各有特色但都提供了上述的一些或全部功能,使得游戏开发更加高效和灵活。这些引擎不仅适用于开发电脑和游戏机游戏,也广泛应用于移动游戏和网页游戏的开发。

游戏主循环是游戏引擎中的中心枢纽,它与游戏引擎的其他部分紧密相连,共同支持游戏的运行和功能实现。下面详细介绍游戏主循环与游戏引擎其他部分的关系:

  1. 图形渲染引擎:

    • 游戏主循环的一个重要任务是调用图形渲染引擎来绘制游戏场景。在每个循环周期中,更新后的游戏状态需要被渲染引擎转化为视觉输出,这包括场景、角色、动画和UI元素的绘制。
  2. 物理引擎:

    • 在游戏主循环中,物理引擎负责计算和模拟物理互动,如碰撞检测、物体运动等。主循环确保这些物理计算与游戏状态的更新同步进行,以保持游戏世界的一致性和实时反应。
  3. 声音处理:

    • 游戏主循环也需要与声音系统协作,确保音效和背景音乐与游戏事件同步播放。例如,当发生特定事件(如爆炸或得分)时,相应的声音效果需要在正确的时刻被触发。
  4. 脚本系统:

    • 游戏逻辑和事件通常通过脚本来控制。游戏主循环会执行这些脚本,以更新游戏状态和响应玩家的行为。脚本系统使得开发者可以灵活地编写和修改游戏逻辑,而无需修改底层代码。
  5. 动画系统:

    • 动画系统负责处理角色和物体的动画。游戏主循环在每个周期更新动画状态,确保动画的流畅播放与游戏事件的同步。
  6. 人工智能(AI):

    • AI系统在游戏主循环中更新非玩家角色的行为。这包括路径寻找、策略决策等。主循环确保AI的决策过程与游戏状态实时更新相匹配。
  7. 网络功能:

    • 对于多玩家游戏,游戏主循环必须处理网络数据的发送和接收,确保所有玩家的游戏状态同步。这通常涉及处理延迟和网络抖动的策略。
  8. 用户界面(UI)系统:

    • UI系统与游戏主循环协作,更新和响应用户界面元素。例如,当玩家得分变化时,得分板需要立即更新。
  9. 资产管理:

    • 资产管理系统负责加载和管理游戏资源,如纹理、模型和声音文件。游戏主循环需要确保这些资源在需要时可用,而不会引起性能下降。

游戏主循环通过协调这些组件的活动,确保游戏的流畅运行和良好的玩家体验。它是游戏引擎中不同系统之间交互的桥梁,是整个游戏运行机制的核心。

4 小程序3:猜数字小游戏

4.1 功能分析

猜数字小游戏是一个简单有趣的游戏,通常是这样玩的:

  • 游戏开始时,玩家会在一定范围内(比如1到100)随机选择一个数字。
  • Core2尝试猜这个数字,每猜一次,玩家会给出提示,告诉Core2猜测的数字是太高了还是太低了。
  • 游戏继续进行,直到Core2猜中正确的数字。
  • 猜中后,可以选择重新开始游戏。

4.2 伪代码

我们将使用状态机,来完成猜数字游戏的主循环。游戏有三个主要状态:GAME_STARTGAME_LOOPGAME_OVER。以下是每个部分的功能和流程:

  • GAME_START:
    • 如果“开始游戏按钮”被点击,则会执行以下操作:
      • 生成一个0到100之间的随机数,表示Core2初次猜测的值。
      • 绘制游戏的主界面。
      • 将游戏状态切换到GAME_LOOP。
  • GAME_LOOP:
    • 按钮B:如果按钮B被按下,表示Core2猜对了数字,显示游戏结束屏幕,并切换到GAME_OVER状态。
    • 按钮A:如果按钮A被按下,表示Core2猜的数字太大了,减小数字,然后重新绘制游戏界面,并保持在GAME_LOOP状态。
    • 按钮C:如果按钮C被按下,表示Core2猜的数字太小了,增大数字,,然后重新绘制游戏界面,并保持在GAME_LOOP状态。
  • GAME_OVER:
    • 如果“重新开始游戏按钮”被点击,则会执行以下操作:
      • 生成一个0到100之间的随机数,表示Core2初次猜测的值。
      • 绘制游戏的主界面。
      • 将游戏状态切换到GAME_LOOP。

4.3 功能实现

复制代码
#include <M5Unified.h>

// Core2 Screen 320x240

// 定义游戏标题在屏幕中的位置
#define TitleX 160
#define TitleY 60

// 定义“开始游戏”与“再次游戏”按钮在屏幕中的文字
#define StartButtonX 80
#define StartButtonY 130
#define StartButtonWidth 160
#define StartButtonHeight 60

// 定义游戏状态机
#define GAME_START 0
#define GAME_LOOP 1
#define GAME_OVER 2
int gameStatus = GAME_START;

// Core2当前猜测的数字
int currentNumber = 0;

// 在屏幕上绘制标题,这个函数有两个参数
// title:绘制在屏幕中上位置的标题文字
// button:绘制在屏幕中下方位置的按钮文字
void drawTitleScreen(String title, String button)
{
  M5.Display.clear(BLACK);

  // drawTitle
  M5.Display.setTextSize(2);
  M5.Display.setTextColor(WHITE);
  M5.Display.drawCenterString(title, TitleX, TitleY);

  // drawButton
  M5.Display.fillRect(StartButtonX, StartButtonY, StartButtonWidth, StartButtonHeight, WHITE);

  M5.Display.setTextSize(4);
  M5.Display.setTextColor(BLACK);
  M5.Display.drawCenterString(button, StartButtonX + StartButtonWidth / 2, StartButtonY + StartButtonHeight / 4);
}

// 检测当用户触摸屏幕时,位置是否在Button内
// 返回值true:触摸位置在Button内,相当于Button Click
// 返回值false:触摸位置不在Button内
bool infoButtonClicked()
{
  auto count = M5.Touch.getCount();
  if (!count)
  {
    return false;
  }

  auto detail = M5.Touch.getTouchPointRaw();
  if (detail.x > StartButtonX && detail.x < StartButtonX + StartButtonWidth && detail.y > StartButtonY && detail.y < StartButtonY + StartButtonHeight)
  {
    return true;
  }

  return false;
}

// 绘制猜数字界面
void drawLoopScreen()
{
  M5.Display.clear(BLACK);

  // drawNumber
  M5.Display.setTextSize(4);
  M5.Display.setTextColor(WHITE);
  M5.Display.drawCenterString("is it " + String(currentNumber) + "?", TitleX, TitleY);

  // drawButton
  M5.Display.setTextSize(2);
  // 320是Core2屏幕的宽度
  auto screenWidthIn1o3 = 320 / 3;
  M5.Display.drawCenterString("Lower", screenWidthIn1o3 / 2, 220);
  M5.Display.drawCenterString("Correct", screenWidthIn1o3 + screenWidthIn1o3 / 2, 220);
  M5.Display.drawCenterString("More", screenWidthIn1o3 * 2 + screenWidthIn1o3 / 2, 220);
}

void setup()
{
  auto cfg = M5.config();
  M5.begin(cfg);

  drawTitleScreen("Number Game", "START");
}

// 游戏主循环
void loop()
{
  delay(100);
  M5.update();

  switch (gameStatus)
  {
  case GAME_START:
    if (infoButtonClicked())
    {
      // random函数可以随机生成一个正整数
      // 该函数具有一个参数,生成的随机数小于该参数
      // random(101)就是随机生成0~100的随机数
      currentNumber = random(101);
      drawLoopScreen();
      gameStatus = GAME_LOOP;
    }
    break;
  case GAME_LOOP:
    if (M5.BtnB.isPressed())
    {
      drawTitleScreen("Game Over", "AGAIN");
      gameStatus = GAME_OVER;
    }
    else if (M5.BtnA.isPressed())
    {
      currentNumber = currentNumber - random(currentNumber);
      drawLoopScreen();
      gameStatus = GAME_LOOP;
    }
    else if (M5.BtnC.isPressed())
    {
      currentNumber = currentNumber + random(101 - currentNumber);
      drawLoopScreen();
      gameStatus = GAME_LOOP;
    }
    break;
  case GAME_OVER:
    if (infoButtonClicked())
    {
      currentNumber = random(101);
      drawLoopScreen();
      gameStatus = GAME_LOOP;
    }
    break;
  default:
    break;
  }
}
复制代码

 

4.4 C++ 函数

4.4.1 函数基础

在C++中,函数是一组一起执行一个任务的语句。函数允许你将代码分割成可重用的模块,可以在程序中多次调用。使用函数可以使代码更加模块化、易于理解和维护。与变量的使用相同,函数在使用前也需要声明和定义。

  • 函数声明(Function Declaration): 告诉编译器函数的名称、返回类型和参数(如果有的话)。此时的函数不占用内存,也就无法被调用。
  • 函数定义(Function Definition): 包含函数声明和函数体(即实际代码块)。

示例

下面是一个简单的C++函数示例,该函数计算两个整数的和并返回结果:

 
复制代码
// int 表示返回值的类型,是整数
int add(int x, int y)
{
    // return后面的就是返回值  
    return x+y;
}


// void 表示这个函数执行完,没有返回值
void printName()
{
    M5.Display.println("Name");

    // return 后面什么都没有
    return
}
复制代码

函数的类型

  1. 有返回值的函数: 如上面的add函数,返回一个整数。
  2. 无返回值的函数: 使用void作为返回类型,不返回任何值。
  3. 带参数的函数: 如上面的add函数,接受两个整数作为参数。
  4. 不带参数的函数: 不接受任何参数。

4.4.2 变量的作用域

在C++中,变量的作用域定义了变量在哪里可以被访问。根据变量声明的位置,可以将C++中的变量作用域分为以下几种类型(更多类型在后续介绍):

  • 局部作用域(Local Scope)

局部变量是在函数内部或一个代码块内部定义的变量。它们只在定义它们的函数或代码块内部可见和可用。当控制流离开该函数或代码块时,这些变量的生命周期结束,它们占用的内存会被释放。

复制代码
void function() {
    int localVariable = 10;  // 局部变量
    M5.Display.print(localVariable);
}

void printNumber() {
    function();
    // M5.Display.print(localVariable); // 错误:localVariable 在这里不可访问
}
复制代码
  • 全局作用域(Global Scope)

全局变量是在所有函数外部定义的变量。它们在程序的整个执行期间都是可用的,从定义它们的位置开始,直到程序结束。全局变量可以被程序中任何函数访问。

复制代码
int globalVariable = 10;  // 全局变量

void function() {
    M5.Display.print(globalVariable);
}

void printNumber() {
    M5.Display.print(globalVariable); 
}
复制代码
  • 块作用域(Block Scope)

块作用域通常与局部作用域相关,特指在某个特定的代码块(如if语句、for循环等)内定义的变量。这些变量只在其定义的那个特定块中有效。

void function() {
    if (true) {
        int blockVariable = 30;  // 块级变量
        M5.Display.print(blockVariable);
    }
    // M5.Display.print(blockVariable); // 错误:blockVariable 在这里不可访问
    return 0;
}

4.5 练习

在进行猜数字时,可以看到,Core2在增加和减少时,会一下变得过多,比如第一次猜20,然后玩家按下More,于是第二次猜50,此时玩家按下Lower,第三次猜10。

实际上,第三次猜测的值应该在20-50之间才合理。请完成这个功能。

 

5 理论与术语总结

posted @   庞兴庆  阅读(73)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示