<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Minas Aslanyan</title>
    <description>The latest articles on DEV Community by Minas Aslanyan (@likwifi).</description>
    <link>https://dev.to/likwifi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3917789%2Ff2e3365d-27e5-4ba0-a1d6-dee898b5f929.png</url>
      <title>DEV Community: Minas Aslanyan</title>
      <link>https://dev.to/likwifi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/likwifi"/>
    <language>en</language>
    <item>
      <title>Building Coach Ivy: Embedding a Unity Avatar Inside a Flutter App</title>
      <dc:creator>Minas Aslanyan</dc:creator>
      <pubDate>Fri, 08 May 2026 10:16:30 +0000</pubDate>
      <link>https://dev.to/likwifi/building-coach-ivy-embedding-a-unity-avatar-inside-a-flutter-app-4mpm</link>
      <guid>https://dev.to/likwifi/building-coach-ivy-embedding-a-unity-avatar-inside-a-flutter-app-4mpm</guid>
      <description>&lt;p&gt;Most nutrition apps feel like spreadsheets with a barcode scanner.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbapxrw8fl0gah8xq53z.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbapxrw8fl0gah8xq53z.PNG" alt=" " width="800" height="1734"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://coachivy.app/" rel="noopener noreferrer"&gt;Coach Ivy&lt;/a&gt; started with a different idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if calorie tracking felt less like filling out a form and more like talking to a tiny 3D anime coach who actually reacts to your meals?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single product decision changed the whole technical architecture.&lt;/p&gt;

&lt;p&gt;A normal Flutter UI was not enough. We needed a real-time 3D character, facial animation, avatar customization, mood reactions, and smooth communication between the app layer and the avatar layer.&lt;/p&gt;

&lt;p&gt;So we built Coach Ivy as a hybrid mobile app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flutter&lt;/strong&gt; for the app UI, onboarding, food logging, subscriptions, analytics, and navigation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unity&lt;/strong&gt; embedded inside the Flutter layer for the 3D avatar experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready Player Me / Animaze-style avatar generation&lt;/strong&gt; for creating the character base&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SALSA LipSync&lt;/strong&gt; for 3D BlendShape-based mouth animation&lt;/li&gt;
&lt;li&gt;A custom message bridge between Flutter and Unity so the AI coach could react to app events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is an &lt;a href="https://coachivy.app/ai-calorie-tracker" rel="noopener noreferrer"&gt;AI calorie tracker&lt;/a&gt; that feels less like a database form and more like a playful character experience.&lt;/p&gt;

&lt;p&gt;This post is a breakdown of the dev side.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Flutter + Unity?
&lt;/h2&gt;

&lt;p&gt;Flutter is great for building polished mobile apps quickly.&lt;/p&gt;

&lt;p&gt;It gives you fast UI iteration, cross-platform structure, and a strong ecosystem for things like Firebase, RevenueCat, analytics, onboarding, and app flows.&lt;/p&gt;

&lt;p&gt;But Flutter is not the best tool when your core product experience depends on a 3D animated character.&lt;/p&gt;

&lt;p&gt;For Coach Ivy, the avatar was not just decoration. It was part of the product.&lt;/p&gt;

&lt;p&gt;The user logs food, gets feedback, sees progress, and Ivy reacts. That means the character needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Idle animations&lt;/li&gt;
&lt;li&gt;Facial expressions&lt;/li&gt;
&lt;li&gt;Mood states&lt;/li&gt;
&lt;li&gt;Mouth movement&lt;/li&gt;
&lt;li&gt;BlendShape control&lt;/li&gt;
&lt;li&gt;A proper 3D scene&lt;/li&gt;
&lt;li&gt;Lighting, camera, and model control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity is much better for that part.&lt;/p&gt;

&lt;p&gt;So instead of forcing everything into Flutter, we split the app into two layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flutter app layer
  - onboarding
  - food logging
  - meal photo flow
  - nutrition results
  - subscriptions
  - analytics
  - app navigation

Unity avatar layer
  - 3D character
  - animations
  - facial expressions
  - BlendShapes
  - avatar scene
  - camera / lighting
  - mood reactions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flutter owns the product.&lt;/p&gt;

&lt;p&gt;Unity owns the character.&lt;/p&gt;

&lt;p&gt;That separation made the app much easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Architecture
&lt;/h2&gt;

&lt;p&gt;At a high level, Coach Ivy works like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User action in Flutter
        ↓
Flutter sends event to Unity
        ↓
Unity updates avatar state
        ↓
Avatar reacts visually
        ↓
Flutter continues app flow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User scans a meal
        ↓
Flutter receives calories/macros from AI
        ↓
Flutter sends a "meal_logged" event to Unity
        ↓
Unity plays Ivy's reaction animation
        ↓
Ivy changes facial expression / mouth / pose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially important for a &lt;a href="https://coachivy.app/photo-calorie-tracker" rel="noopener noreferrer"&gt;photo calorie tracker&lt;/a&gt;, because the user experience should not end at “here are your numbers.”&lt;/p&gt;

&lt;p&gt;The meal scan gives the data.&lt;/p&gt;

&lt;p&gt;The avatar gives the reaction.&lt;/p&gt;

&lt;p&gt;That is what makes the app feel alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Embedding Unity Inside Flutter
&lt;/h2&gt;

&lt;p&gt;The tricky part is not making a Unity scene.&lt;/p&gt;

&lt;p&gt;The tricky part is making Unity behave like part of a mobile app.&lt;/p&gt;

&lt;p&gt;A standalone Unity app usually owns the full screen. But Coach Ivy needed Unity to live inside a Flutter experience.&lt;/p&gt;

&lt;p&gt;Flutter still handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Navigation&lt;/li&gt;
&lt;li&gt;Screens&lt;/li&gt;
&lt;li&gt;Buttons&lt;/li&gt;
&lt;li&gt;Paywalls&lt;/li&gt;
&lt;li&gt;Forms&lt;/li&gt;
&lt;li&gt;Photo upload&lt;/li&gt;
&lt;li&gt;App state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unity is embedded as a visual and interactive layer.&lt;/p&gt;

&lt;p&gt;In Flutter, the Unity view becomes part of the widget tree.&lt;/p&gt;

&lt;p&gt;A simplified version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;UnityWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;onUnityCreated:&lt;/span&gt; &lt;span class="n"&gt;onUnityCreated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;onUnityMessage:&lt;/span&gt; &lt;span class="n"&gt;onUnityMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="n"&gt;Positioned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;bottom:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;left:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;right:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;MealResultCard&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets us combine a real 3D character with Flutter-native UI.&lt;/p&gt;

&lt;p&gt;That is powerful because the app still feels like a normal mobile app, not like a game menu wrapped around forms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Communication Between Flutter and Unity
&lt;/h2&gt;

&lt;p&gt;The bridge between Flutter and Unity is where the product starts to feel alive.&lt;/p&gt;

&lt;p&gt;Flutter knows what the user is doing.&lt;/p&gt;

&lt;p&gt;Unity knows how Ivy should react.&lt;/p&gt;

&lt;p&gt;So Flutter passes events to Unity.&lt;/p&gt;

&lt;p&gt;A simplified Flutter-side example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendMealLoggedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MealResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"meal_logged"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"calories"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"protein"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;protein&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"tone"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"sassy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"emotion"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"surprised"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="n"&gt;unityController&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"IvyAvatarController"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"OnFlutterEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;jsonEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then Unity receives it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnFlutterEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonUtility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromJson&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IvyEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"meal_logged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;PlayMealReaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"goal_completed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;PlayCelebration&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"water_reminder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;PlayReminderExpression&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not send random commands everywhere. Create a small event protocol.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of calling Unity methods like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OpenMouth()
Blink()
LookAngry()
MoveArm()
SayText()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is cleaner to send product-level events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;meal_logged
goal_completed
coach_message_started
coach_message_finished
streak_lost
streak_saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unity translates those into animations.&lt;/p&gt;

&lt;p&gt;That keeps the Flutter side clean and avoids turning the app into a mess of random animation calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Avatar Generation Pipeline
&lt;/h2&gt;

&lt;p&gt;For the avatar base, we used a Ready Player Me / Animaze-style workflow.&lt;/p&gt;

&lt;p&gt;The rough pipeline looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate avatar
        ↓
Export avatar model
        ↓
Import into Unity
        ↓
Configure materials and rig
        ↓
Set up BlendShapes
        ↓
Connect animation controllers
        ↓
Render avatar inside Flutter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part was not just getting a model into Unity.&lt;/p&gt;

&lt;p&gt;The important part was making sure the model had the right facial controls.&lt;/p&gt;

&lt;p&gt;For a talking and expressive coach, the avatar needs useful facial BlendShapes.&lt;/p&gt;

&lt;p&gt;At minimum, we wanted control over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mouth open / closed&lt;/li&gt;
&lt;li&gt;Smile&lt;/li&gt;
&lt;li&gt;Frown&lt;/li&gt;
&lt;li&gt;Blink&lt;/li&gt;
&lt;li&gt;Brows&lt;/li&gt;
&lt;li&gt;Surprised face&lt;/li&gt;
&lt;li&gt;Angry / strict expression&lt;/li&gt;
&lt;li&gt;Viseme-like mouth shapes for talking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those, the character looks like a 3D statue.&lt;/p&gt;

&lt;p&gt;And a nutrition coach that judges your snack choices should not look like a statue.&lt;/p&gt;

&lt;p&gt;That personality is also why we positioned Coach Ivy as a &lt;a href="https://coachivy.app/kawaii-calorie-tracker" rel="noopener noreferrer"&gt;kawaii calorie tracker&lt;/a&gt;, not just another food logging utility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using BlendShapes for Facial Animation
&lt;/h2&gt;

&lt;p&gt;BlendShapes are named facial shape targets on a mesh.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mouthSmile
mouthOpen
eyeBlinkLeft
eyeBlinkRight
browDown
jawOpen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Unity, they can be controlled from code through the &lt;code&gt;SkinnedMeshRenderer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Simplified example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IvyFaceController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MonoBehaviour&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;SkinnedMeshRenderer&lt;/span&gt; &lt;span class="n"&gt;faceRenderer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;smileIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;mouthOpenIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Awake&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;smileIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faceRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedMesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBlendShapeIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mouthSmile"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;mouthOpenIndex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;faceRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sharedMesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBlendShapeIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mouthOpen"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetSmile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;faceRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBlendShapeWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smileIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetMouthOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;faceRenderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBlendShapeWeight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mouthOpenIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a reaction can blend several facial states together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PlaySassyReaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;SetSmile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;65f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;SetBrowDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;25f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;SetMouthOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the character starts feeling less like a model and more like a personality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using SALSA for 3D Mouth Animation
&lt;/h2&gt;

&lt;p&gt;For mouth movement, we used SALSA-style 3D BlendShape animation.&lt;/p&gt;

&lt;p&gt;Instead of manually animating every mouth frame, the system can drive mouth shapes from audio or speech intensity.&lt;/p&gt;

&lt;p&gt;The practical setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Audio / speech event
        ↓
SALSA analyzes signal
        ↓
SALSA drives mouth BlendShapes
        ↓
Avatar appears to speak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Coach Ivy, this matters because the avatar is not just standing there.&lt;/p&gt;

&lt;p&gt;She reacts, talks, complains, celebrates, and gives feedback.&lt;/p&gt;

&lt;p&gt;The personality only works if the face supports it.&lt;/p&gt;

&lt;p&gt;A basic text response is useful.&lt;/p&gt;

&lt;p&gt;A 3D coach reacting with facial animation is much more memorable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Biggest Challenge: Lifecycle
&lt;/h2&gt;

&lt;p&gt;Embedding Unity into Flutter is not just a rendering problem.&lt;/p&gt;

&lt;p&gt;It is a lifecycle problem.&lt;/p&gt;

&lt;p&gt;Mobile apps pause, resume, background, foreground, rotate, rebuild widgets, and sometimes kill views aggressively.&lt;/p&gt;

&lt;p&gt;Unity does not behave like a normal Flutter widget.&lt;/p&gt;

&lt;p&gt;So we had to think carefully about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When Unity loads&lt;/li&gt;
&lt;li&gt;Whether Unity should stay alive between screens&lt;/li&gt;
&lt;li&gt;How to avoid reloading the scene too often&lt;/li&gt;
&lt;li&gt;How to pause animations when not visible&lt;/li&gt;
&lt;li&gt;How to avoid memory spikes&lt;/li&gt;
&lt;li&gt;How to handle hot restarts during development&lt;/li&gt;
&lt;li&gt;How to recover if the Unity view is destroyed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule that helped:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat Unity like a heavy runtime, not like a normal UI component.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Do not rebuild it casually.&lt;/p&gt;

&lt;p&gt;Do not mount and unmount it every time the user changes a tab.&lt;/p&gt;

&lt;p&gt;Keep the Unity scene stable and send it events.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Notes
&lt;/h2&gt;

&lt;p&gt;A 3D avatar inside a mobile app can get expensive quickly.&lt;/p&gt;

&lt;p&gt;Here are a few things that helped.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Keep the Unity scene small
&lt;/h3&gt;

&lt;p&gt;We did not need a full game world.&lt;/p&gt;

&lt;p&gt;We needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One avatar&lt;/li&gt;
&lt;li&gt;Simple lighting&lt;/li&gt;
&lt;li&gt;One camera&lt;/li&gt;
&lt;li&gt;Controlled animations&lt;/li&gt;
&lt;li&gt;Lightweight background&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The less Unity has to render, the better the app feels.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Avoid unnecessary texture size
&lt;/h3&gt;

&lt;p&gt;Avatar textures can become surprisingly heavy.&lt;/p&gt;

&lt;p&gt;For mobile, it is usually better to compress and resize aggressively instead of shipping desktop-quality textures.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use Flutter for UI, not Unity
&lt;/h3&gt;

&lt;p&gt;It is tempting to build buttons and overlays in Unity.&lt;/p&gt;

&lt;p&gt;But for Coach Ivy, Flutter is better for app UI.&lt;/p&gt;

&lt;p&gt;Unity should render the character.&lt;/p&gt;

&lt;p&gt;Flutter should render the product interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Send events, not constant updates
&lt;/h3&gt;

&lt;p&gt;Flutter should not spam Unity every frame.&lt;/p&gt;

&lt;p&gt;Instead of sending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mouth = 0.1
mouth = 0.2
mouth = 0.4
mouth = 0.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;coach_message_started
coach_message_finished
meal_logged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let Unity handle animation internally.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;If I were starting again, I would define the Flutter to Unity event contract earlier.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;IvyEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;avatar_ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meal_logged&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;calories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;protein&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;mood&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;coach_speaking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;emotion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;goal_completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;goalType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle_state&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if the app is not written in TypeScript, writing the protocol this way helps.&lt;/p&gt;

&lt;p&gt;It prevents random one-off messages from growing into chaos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Was Worth It
&lt;/h2&gt;

&lt;p&gt;This setup is more complex than a normal Flutter app.&lt;/p&gt;

&lt;p&gt;But it also makes Coach Ivy feel different.&lt;/p&gt;

&lt;p&gt;The point was not to build another calorie tracker.&lt;/p&gt;

&lt;p&gt;The point was to make nutrition tracking feel more emotional, playful, and personal.&lt;/p&gt;

&lt;p&gt;A Flutter-only app can track calories.&lt;/p&gt;

&lt;p&gt;A Unity-powered avatar can react to them.&lt;/p&gt;

&lt;p&gt;That difference matters.&lt;/p&gt;

&lt;p&gt;Especially in consumer apps, the experience is part of the product.&lt;/p&gt;

&lt;p&gt;For Coach Ivy, the 3D avatar is not just visual polish.&lt;/p&gt;

&lt;p&gt;It is the interface.&lt;/p&gt;

&lt;p&gt;You can see the live app here: &lt;a href="https://apps.apple.com/us/app/coach-ivy-kawaii-calorie-log/id6757866872" rel="noopener noreferrer"&gt;Coach Ivy on the App Store&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Architecture
&lt;/h2&gt;

&lt;p&gt;The final structure looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Coach Ivy Mobile App

Flutter
  ├── onboarding
  ├── auth
  ├── meal logging
  ├── AI nutrition analysis
  ├── subscriptions
  ├── analytics
  └── Unity bridge

Unity
  ├── avatar scene
  ├── Ready Player Me avatar
  ├── BlendShape controllers
  ├── SALSA mouth animation
  ├── idle animations
  ├── emotion states
  └── Flutter event receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the right engine for the right layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Flutter is excellent for app structure.&lt;/p&gt;

&lt;p&gt;Unity is excellent for real-time character experience.&lt;/p&gt;

&lt;p&gt;Together, they made Coach Ivy possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;Embedding Unity inside Flutter is not the simplest architecture.&lt;/p&gt;

&lt;p&gt;But if your app depends on a living, reactive, animated character, it can be the right one.&lt;/p&gt;

&lt;p&gt;Coach Ivy needed to feel like a cute but slightly bossy AI nutrition coach.&lt;/p&gt;

&lt;p&gt;That meant the avatar had to move, react, talk, and judge food choices with personality.&lt;/p&gt;

&lt;p&gt;Flutter gave us the app.&lt;/p&gt;

&lt;p&gt;Unity gave us the character.&lt;/p&gt;

&lt;p&gt;The bridge between them gave Coach Ivy her soul.&lt;/p&gt;

&lt;p&gt;You can try the app at &lt;a href="https://coachivy.app/" rel="noopener noreferrer"&gt;coachivy.app&lt;/a&gt; or download it from the &lt;a href="https://apps.apple.com/us/app/coach-ivy-kawaii-calorie-log/id6757866872" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mobile</category>
      <category>unity3d</category>
    </item>
  </channel>
</rss>
