机器人触摸反馈模块:3x3 随机匹配 + 概率语音播报

机器人"摸一摸"会说话

一、背景:为什么要做这个功能?

我们的机器人是一款会变形的机器人,有双足人形四足小狗两种形态。用户可以通过触摸机器人的外壳来跟机器人互动。

之前的问题是:摸机器人只有表情和动作,不说话。产品希望每次触摸都能听到机器人的反馈语音,让机器人更有人情味。

比如:

  • 摸一下前胸 → 机器人做个律动 + 说:"哎呀,我最喜欢被摸啦~"
  • 有时候还会追加一句:"既然你已经来了,那就别这么快走嘛。"

30% 的概率 触发"撒骄挽留"话术,70% 的概率只是普通的回应。这样每次摸都有新鲜感。


二、整体设计

2.1 触摸分两种形态,各有 3 种随机组合

复制代码
触摸事件(SLIDE 滑动)
  │
  ├─ 双足人形(前胸)   → 3 选 1:律动/掐腰A/掐腰B
  └─ 四足(后背)       → 3 选 1:骄傲/聆听/四处看

2.2 每次触摸都说话

复制代码
100% → 随机说一句(10 句里选 1,不跟上一次重复)
 30% → 再追加拿一句(6 句里选 1,不跟上一次重复)

2.3 执行顺序

复制代码
1. 表情(先笑一个)
2. 动作(开始做动作)
3. 等 0.5 秒
4. 说话(动作开始后 0.5 秒说话,让用户感觉动作和语音同步)
5. 30% 追加(等 5 秒后再来一句)
6. 等待动作完成

三、核心代码详解

3.1 概率模型(30% vs 70%)

这是一个巧妙的"洗牌算法":用一个长度为 10 的数组,放 3 个 true 和 7 个 false,然后随机打乱。每次调用取下一个,取完 10 次再重新洗牌。

保证了每 10 次触摸中刚好 3 次返回 true(30%),7 次返回 false(70%)。

cpp 复制代码
/// 以 10 次有效触摸为一个统计窗口,其中 3 次返回 true(持续互动),7 次返回 false
static bool ShouldBePersistent() {
  static std::array<bool, 10> window = {true, true, true, false, false, false, false, false, false, false};
  static int window_idx              = 10;  // 首次调用时触发重新洗牌
  if (window_idx >= 10) {
    std::shuffle(window.begin(), window.end(), GetTouchRng());
    window_idx = 0;
  }
  return window[window_idx++];
}

为什么不用 rand() % 10 < 3 因为真随机可能会出现连续 5 次都是 true 的情况。用洗牌法保证了 10 次中一定是 3 次,分布均匀。

3.2 随机数生成器

cpp 复制代码
static std::mt19937& GetTouchRng() {
  static std::mt19937 rng(std::random_device{}());
  return rng;
}

这里用了 C++ 的 Mersenne Twister 算法(std::mt19937),比 rand() 质量高得多,用 std::random_device 生成随机种子(真随机)。

3.3 持续互动话术(30% 触发,6 选 1)

cpp 复制代码
/// 30% 追加持续互动话术(随机,不与上次相同)
static const std::string& GetPersistentInteractionTts() {
  static const std::array<std::string, 6> kPhrases = {
    "既然你已经来了,那就别这么快走嘛。",
    "都摸摸我了,不陪我聊聊吗?",
    "再待一小会儿,好不好?",
    "陪我多待一分钟也行呀。",
    "别急着走,我还有点舍不得。",
    "再陪陪我嘛,我会很安静的。",
  };
  static int last_idx = -1;
  static std::uniform_int_distribution<int> dist(0, 5);
  int idx;
  do {
    idx = dist(GetTouchRng());
  } while (idx == last_idx);
  last_idx = idx;
  return kPhrases[idx];
}

关键设计:do-while (idx == last_idx) 保证两次相邻的触摸不会说出相同的话

3.4 预设话术(100% 触发,10 选 1)

cpp 复制代码
/// 每次抚摸必说的预设话术(随机,不与上次相同)
static const std::string& GetPresetTouchTts() {
  static const std::array<std::string, 10> kPhrases = {
    "哎呀,我最喜欢被摸啦~",
    "暖暖的,好舒服。",
    "再摸一下,我就要开心到冒泡啦。",
    "你摸的时候,我会偷偷变乖。",
    "别呀,别摸我啦......",
    "等一下,我会害羞的",
    "你怎么又突然摸我!",
    "不许趁我不注意偷袭。",
    "轻一点嘛,我很脆弱的。",
    "再摸下去我要融化了。",
  };
  static int last_idx = -1;
  static std::uniform_int_distribution<int> dist(0, 9);
  int idx;
  do {
    idx = dist(GetTouchRng());
  } while (idx == last_idx);
  last_idx = idx;
  return kPhrases[idx];
}

同样用了 do-while 保证不与上次重复。

3.5 入口函数:根据形态和触摸类型分派

cpp 复制代码
std::shared_ptr<SkillParamList> T1InteractionTaskDescription::GetTouchSkillParamList(int32_t event_type) {
  auto motion_state = StateManager::GetInstance()->GetMotionState();
  switch (event_type) {
    case TOUCH_EVENT_DOUBLE_CLICK:   // 点击类:只有四足支持
    case TOUCH_EVENT_TRIPLE_CLICK:
      if (curr_robot_form != kQuadruped) return nullptr;    // 双足不支持点击
      return GetClickQuadrupedSkillParamList();

    case TOUCH_EVENT_SLIDE_LEFT:     // 抚摸类:双足和四足都支持
    case TOUCH_EVENT_SLIDE_RIGHT:
      if (curr_robot_form == kQuadruped) return GetSlideQuadrupedSkillParamList();  // 四足后背
      if (curr_robot_form == kBiped)    return GetSlideBipedSkillParamList();        // 人形前胸
      break;                           // 其他形态不支持

    default:
      return nullptr;
  }
  return nullptr;
}

3.6 双足人形前胸:3 变体 + 语音

cpp 复制代码
std::shared_ptr<SkillParamList> T1InteractionTaskDescription::GetSlideBipedSkillParamList() {
  auto skill_param_list = std::make_shared<SkillParamList>();
  auto curr_action_id   = StateManager::GetInstance()->GetMotionState().curr_action_id;

  // 3 种组合:(animation_id, emotion_id, wait_ms)
  static const std::array<TouchVariant, 3> kVariants = {{
    {43, 102, 5500},  // 律动,emotion=102(开心),时长 5.5s
    {66, 16, 4000},   // 掐腰,emotion=16(轻微喜悦2),时长 4s
    {66, 18, 4000},   // 掐腰,emotion=18(中等喜悦),时长 4s
  }};

  // 随机选取,不与上次相同
  static int last_variant = -1;
  static std::uniform_int_distribution<int> dist_var(0, 2);
  int variant;
  do {
    variant = dist_var(GetTouchRng());
  } while (variant == last_variant);
  last_variant = variant;
  const auto& chosen = kVariants[variant];

  // 1. 表情(先放)
  auto emotion_list = GetSkillParamList(chosen.emotion_id);
  if (emotion_list) {
    for (const auto& p : emotion_list->GetSkillsParam()) {
      skill_param_list->PushSkillParam(p);
    }
  }

  // 2. 如果不在 WBC 模式,先切换到 WBC(预设动作需要 WBC)
  if (curr_action_id != BIPED_LOCOMOTION_WBC) {
    if (curr_action_id == BIPED_LOCOMOTION_DEFAULT
        || curr_action_id == BIPED_LOCOMOTION_TERRAIN
        || curr_action_id == BIPED_LOCOMOTION_RUN
        || curr_action_id == QUADRUPED_TO_BIPED
        || curr_action_id == QUADRUPED_TO_BIPED_ROTATE) {
      // 先切 WBC
      PushSkillParam(ActionParams(BIPED_LOCOMOTION_WBC));
      PushSkillParam(TimeParams(150));  // 等 150ms
    }
  }

  // 3. 动作(先执行)
  auto motion_p = std::make_shared<MotionParams>();
  if (chosen.animation_id == 43) {
    motion_p->SetMotionId(2031);  // 律动
  } else {
    motion_p->SetMotionId(2037);  // 掐腰
  }
  skill_param_list->PushSkillParam(motion_p);

  // 4. 等 0.5 秒后再说话
  PushSkillParam(TimeParams(500));

  // 5. 自然语言:100% 触发
  PushSkillParam(AudioParams(GetPresetTouchTts(), TTS));

  // 6. 30% 追加持续互动话术
  if (ShouldBePersistent()) {
    PushSkillParam(TimeParams(5000));                           // 等 5 秒
    PushSkillParam(AudioParams(GetPersistentInteractionTts(), TTS));
  }

  // 7. 等待动作完成
  PushSkillParam(TimeParams(chosen.wait_ms));

  return skill_param_list;
}

3.7 四足后背:3 变体 + 语音

cpp 复制代码
std::shared_ptr<SkillParamList> T1InteractionTaskDescription::GetSlideQuadrupedSkillParamList() {
  auto skill_param_list = std::make_shared<SkillParamList>();
  auto curr_action_id   = StateManager::GetInstance()->GetMotionState().curr_action_id;

  // 3 种组合
  static const std::array<TouchVariant, 3> kVariants = {{
    {25, 102},  // 骄傲动作,emotion=102(开心)
    {49, 16},   // 聆听动作,emotion=16(轻微喜悦2)
    {54, 18},   // 四处看,emotion=18(中等喜悦)
  }};

  // 随机选取(同双足逻辑)
  // ... (省略,见上方双足版本)

  // 1. 表情
  // ... (省略)

  // 2. 动作(先执行)
  if (chosen.animation_id == 25) {
    // 骄傲:需要先切 STAND,然后设 PRIDE 动作
    if (curr_action_id == GET_DOWN || curr_action_id == STORE || curr_action_id == SIT_DOWN) {
      PushSkillParam(ActionParams(STAND_DEFAULT, check_set=true));
    }
    PushSkillParam(ActionParams(PRIDE));
  } else {
    // 聆听/四处看:全身动作
    const std::string motion_name = (chosen.animation_id == 49)
        ? "QUAD_MIMIC_HEED" : "QUAD_MIMIC_GLANCE";

    if (curr_action_id != STAND_DEFAULT) {
      PushSkillParam(ActionParams(STAND_DEFAULT));
      PushSkillParam(TimeParams(100));
    }
    PushSkillParam(MotionParams(motion_name, kWholeBodyMotion));
  }

  // 3. 等 0.5 秒后再说话
  PushSkillParam(TimeParams(500));

  // 4. 自然语言:100% 触发
  PushSkillParam(AudioParams(GetPresetTouchTts(), TTS));

  // 5. 30% 追加
  if (ShouldBePersistent()) {
    PushSkillParam(TimeParams(5000));
    PushSkillParam(AudioParams(GetPersistentInteractionTts(), TTS));
  }

  // 6. 骄傲动作需要等待 6.5 秒后恢复
  if (chosen.animation_id == 25) {
    PushSkillParam(TimeParams(6500));
    PushSkillParam(ActionParams(LOCOMOTION_DEFAULT));  // 恢复到行走模式
  }

  return skill_param_list;
}

四、技术细节

4.1 SkillParamList:任务执行队列

SkillParamList 是一个技能参数列表,任务按顺序逐个执行其中的每个 Skill:

复制代码
SkillParamList = [表情, 切WBC, 动作, 等500ms, 说话, (等5s+说话), 等动作完成]
                    ↓
              Task::Run() 按顺序执行

每个 Skill 的类型不同:

Skill 作用 说明
EmoticonParams 播放表情 异步,发 RPC 后即返回
ActionParams 切换 MC 模式 同步等待确认
MotionParams 播放预设动作 异步 RPC
TimeParams 等待 阻塞线程指定毫秒
AudioParams 播放语音 异步 RPC

4.2 为什么动作在语音前面?

考虑到用户体验:如果先说话后动作,用户会听到语音但看不到动作,感觉很怪。先动作后说话,动作开始 0.5 秒后语音响起,用户感觉动作和语音是同时发生的。

4.3 为什么不与上次重复?

cpp 复制代码
do {
    idx = dist(GetTouchRng());    // 随机选一个
} while (idx == last_idx);        // 如果跟上次一样,重新选
last_idx = idx;                   // 记住这次选的

static 变量记住上次的选择,确保不会"同一个动作连续做两次"。

4.4 WBC 模式守卫

双足的预设动作(PresetMotion)必须在 WBC(Whole Body Control,全身控制) 模式下才能执行。如果当前不在 WBC,需要先发一个 Action 切换到 WBC。这是双足机器人的安全约束。


五、数据流全景

复制代码
用户摸机器人
    │
    ▼
硬件触摸传感器 → ROS2 topic /aima/hal/touch/state
    │
    ▼
Scheduler 收到 TouchState 消息
    │
    ▼
Dispatcher::DispatchTouchStatus(event_type)
    │
    ├─ 检查冷却时间(四足滑动 7 秒内不重复触发)
    ├─ 检查 validity(是否有效触摸类型)
    │
    ▼
TaskFactory::CreateTaskTouch(event_type)
    │
    ▼
T1InteractionTaskDescription::GetTouchSkillParamList(event_type)
    │  根据形态分派
    ├─ kBiped    → GetSlideBipedSkillParamList()
    └─ kQuadruped → GetSlideQuadrupedSkillParamList()
    │
    ▼
返回 SkillParamList
    │
    ▼
WorkerManager → Worker → Task::Run()
    │
    ▼
按顺序执行 Skill:
    1. EmoticonSkill    → PlayEmotion RPC  → 表情渲染模块
    2. ActionSkill      → SetMcAction RPC  → MC(运动控制)
    3. MotionSkill      → SetMcPresetMotion RPC → MC
    4. TimeSkill        → sleep 阻塞
    5. AudioSkill       → PlayTts RPC      → HAL Audio
    6. TimeSkill        → sleep 阻塞

六、总结

这个模块的核心思想很简单:每次触摸都给用户一段新鲜的语音反馈

技术实现上用了几个关键技巧:

  1. 洗牌算法保证概率分布的均匀性
  2. do-while 去重避免连续两次相同
  3. SkillParamList 顺序执行确保动作和语音的时序正确
  4. 异步 RPC 让动作和语音能并发出
相关推荐
工业机器人销售服务1 小时前
遨博产品尺寸偏差智能检测,微米级筛查误差,严控成品装配精度
机器人·自动化
百度智能云技术站2 小时前
训练周期减半:LoongForge 全链路优化 GR00T N1.6 训练,吞吐提升至 2.3 倍
机器人·llm
才兄说3 小时前
机器人二次开发机器狗巡检?高精度自主定位
机器人
小烤箱6 小时前
什么是 ROS2:机器人软件的数据加工工业园区
人工智能·机器人·ros
梦想的旅途27 小时前
企微API:外部群的主动/被动自动回复机器人
机器人·企业微信·rpa
AI猫站长9 小时前
快讯|地瓜机器人旭日S600 560TOPS算力平台适配自变量具身基础模型,蚂蚁灵波与简智联合研发专属数采设备,龙旗工厂智元机器人8小时作业成功率99.5%
大数据·人工智能·机器学习·机器人·具身智能
yu85939589 小时前
基于卡尔曼滤波器的集中式机器人轨迹定位算法
算法·机器人
SLAM必须dunk11 小时前
GMR(General Motion Retargeting)仓库详细解析
机器学习·机器人
刘大猫.18 小时前
智造短剧新引擎:火山引擎上线「火山剧创 1.0」,制作效率提升 80%
人工智能·ai·chatgpt·机器人·大模型·火山引擎·短剧新引擎