Amal از Rive (قبلاً Flare) برای تمام انیمیشنهای شخصیتها استفاده میکند — شامل هماهنگی لبها با گفتار، سفارشیسازی آواتار، واکنشهای بازخوردی و شخصیتهای بازی. ما Rive را به جای Lottie یا شیپتهای اسپریتی انتخاب کردیم زیرا از ماشین حالت زمان اجرا، دستکاری برنامهای و رندر شتابیافته با GPU با نرخ ۶۰ فریم بر ثانیه به صورت یک فایل فشرده به ازای هر شخصیت پشتیبانی میکند.
کتابخانه داراییهای انیمیشن
شخصیتهای اصلی
lip-sync-amal-01.riv- شخصیت اصلی Amal (نسخههای تمامقد و فقط صورت)
- چند تخته هنری به ازای هر حالت دهان (برای نگاشت فونم)
- حالتها: آرام، صحبت کردن، خطا، جشن، خواب
- حجم فایل: ۱.۲ مگابایت (در مقابل بیش از ۵۰ مگابایت برای شیپتهای اسپریتی)
avatar.riv- آواتار قابل سفارشیسازی کاربر (۳ تخته هنری)
- تمامقد: سر، تنه، اندامها با لباس
- فقط سر: برای داشبورد و اپ والدین
- همراه پروانه: انیمیشن پاداش
- معماری مبتنی بر اجزا: شکل سر، مو، چشمها، لباسها، لوازم، رنگها
- حجم فایل: ۲.۴ مگابایت
- آواتار قابل سفارشیسازی کاربر (۳ تخته هنری)
coin-01.rivوcoins-01.riv- انیمیشنهای پاداش (شناور و جمعآوری کردن سکهها)
- سکه تکی: ۱۵۰ کیلوبایت
- چند سکه: ۳۰۰ کیلوبایت
cute-monster-final.riv- شخصیت بازخورد با چند حالت احساسی
- حالتها: خوشحال (جواب درست)، سردرگم (اشتباه)، تفکر (در حال پردازش)، جشن گرفتن (سلسله پیروزیها)
- حجم فایل: ۱.۸ مگابایت
بهینهسازی مخصوص اندروید
- ساخت NDK سفارشی (نسخه Rive NDK-r28) برای تطابق با ترازبندی صفحه ۱۶ کیلوبایتی
- کاهش حجم باینری ۸٪ در مقابل ساخت استاندارد
- تضمین سازگاری با مدیریت حافظه تهاجمی در اندروید ۱۲ و بالاتر
روند هماهنگی لبها (توضیح فنی)
مرحله ۱: تولید صوت TTS و استخراج نشانههای گفتار
# src/services/tts_client.py
from google.cloud import texttospeech
def generate_speech_with_marks(text: str, language: str = 'ar-SA'):
client = texttospeech.TextToSpeechClient()
synthesis_input = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code=language,
name='ar-SA-Neural2-A' # صدای WaveNet
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
effects_profile_id=['small-bluetooth-speaker-class-device'] # بلندگوی کودک
)
# درخواست نشانههای گفتار (زمانبندی فونمها)
request = texttospeech.SynthesizeSpeechRequest(
input=synthesis_input,
voice=voice,
audio_config=audio_config,
enable_text_to_speech_as_cloud_service=True
)
response = client.synthesize_speech(request=request)
# پاسخ شامل:
# - audio_content: بایتهای MP3
# - timepoints: [{character, byte_pos, time_ms}, ...]
return {
'audio': response.audio_content,
'speech_marks': response.timepoints # زمانهای دقیق فونمها
}
مرحله ۲: نگاشت فونمها به حالتهای دهان در Rive
فایل lip_sync_avatar.json فونمهای عربی را به موقعیتهای دهان نگاشت میکند:
{
"phoneme_map": {
"ا": { "rive_state": "mouth_a_open", "duration_ms": 200 },
"ب": { "rive_state": "mouth_lips_closed", "duration_ms": 150 },
"ع": { "rive_state": "mouth_pharyngeal", "duration_ms": 250 },
"ق": { "rive_state": "mouth_uvular", "duration_ms": 180 },
...
},
"mouth_positions": [
{ "id": "mouth_a_open", "blend_values": { "jaw_open": 0.8, "lips_rounded": 0.2 } },
{ "id": "mouth_lips_closed", "blend_values": { "jaw_open": 0.1, "lips_rounded": 0.9 } },
...
]
}
مرحله ۳: کنترلکننده LipSync برای هماهنگی پخش
// lib/src/modules/animations/controllers/lip_sync_controller.dart
class LipSyncController extends GetxController {
late Rive riveCharacter;
late AudioPlayer audioPlayer;
late LipSyncMapper mapper;
void playWithLipSync(String text, String audioPath) {
// مرحله ۱: بارگذاری شخصیت Rive
riveCharacter.loadRiveFile('lip-sync-amal-01.riv');
// مرحله ۲: بارگذاری نشانههای گفتار از خروجی TTS
mapper = LipSyncMapper.fromJson(loadJsonAsset('lip_sync_avatar.json'));
// مرحله ۳: پخش صوت و کنترل انیمیشن دهان
audioPlayer.play(AudioSource.file(audioPath));
// مرحله ۴: بهروزرسانی موقعیت دهان در هر فریم صوتی
audioPlayer.onPositionChanged.listen((Duration position) {
String phoneme = mapper.phonemeAtTime(position.inMilliseconds);
String riveState = mapper.riveStateForPhoneme(phoneme);
riveCharacter.setStateInput('mouth_state', riveState);
});
}
}
مرحله ۴: کنترلکننده شخصیت Rive مدیریت چرخه عمر
// مدیریت وضعیت کامل انیمیشن شخصیت (نه فقط دهان)
class RiveCharacterController extends GetxController {
States: idle → prepare → speaking → idle → error/celebration
void startExercise() {
// انتقال به حالت آماده به شنیدن
character.setStateInput('state_machine', 'prepare');
}
void childSpeaks(String recognizedText, double accuracy) {
character.setStateInput('state_machine', 'speaking');
lipSyncController.playFeedback(recognizedText);
}
void onFeedbackComplete(bool wasCorrect) {
if (wasCorrect) {
character.setStateInput('state_machine', 'celebrate');
playRewardAnimation();
} else {
character.setStateInput('state_machine', 'error');
playEncouragingPhrase();
}
}
}
سیستم سفارشیسازی آواتار
معماری مبتنی بر اجزا
کودکان میتوانند آواتار خود را از قطعات مختلف سفارشی کنند:
{
"avatar_customization": {
"head_shapes": [
{ "id": "round", "rive_element": "head_round" },
{ "id": "oval", "rive_element": "head_oval" },
{ "id": "square", "rive_element": "head_square" }
],
"hair_styles": [
{ "id": "ponytail", "rive_element": "hair_ponytail" },
{ "id": "braids", "rive_element": "hair_braids" },
{ "id": "straight", "rive_element": "hair_straight" }
],
"colors": {
"skin_tone": ["light", "medium", "dark"],
"hair_color": ["black", "brown", "blonde", "red"],
"shirt_color": ["blue", "pink", "green", "yellow", "purple"],
"accent_color": ["red", "orange", "green", "blue"]
}
}
}
نگاشت عناصر نامگذاری شده Rive (avatar_customization_rive_names.dart)
const avatarRiveNames = {
'head_round': 'Head_Round',
'head_oval': 'Head_Oval',
'hair_ponytail': 'Hair_Ponytail',
'shirt_blue': 'Shirt_Blue',
'shirt_pink': 'Shirt_Pink',
// ... بیش از ۵۰ نگاشت عناصر دیگر
};
وقتی کودک "سر گرد + پیراهن آبی" را انتخاب میکند، اپلیکیشن:
- عنصر Rive
Head_Roundرا فعال میکند - عنصر Rive
Shirt_Blueرا فعال میکند - تمام شکلهای دیگر سر و رنگهای پیراهن را غیرفعال میکند
- آواتار شخصیسازی شده کودک در سراسر اپ ظاهر میشود
چرا Rive را به جای گزینههای دیگر انتخاب کردیم
| ویژگی | Rive | Lottie | شیپتهای اسپریتی | ویدئو |
|---|---|---|---|---|
| ماشینهای حالت | ✓ | ✗ | ✗ | ✗ |
| کنترل زمان اجرا | ✓ (کامل) | جزئی | دستی | ✗ (غیرفعال) |
| حجم فایل | ۱-۲ مگابایت | ۲-۳ مگابایت | ۵۰+ مگابایت | ۱۰۰+ مگابایت |
| کارایی | ۶۰fps با GPU | ۳۰fps با CPU | ۶۰fps با GPU | متغیر |
| تعامل | ✓ کامل | ✓ جزئی | ✓ کامل | ✗ ندارد |
| چالش یادگیری | متوسط | آسان | آسان | آسان |
| نگهداری | یک فایل .riv | یک JSON | صدها تصویر | یک ویدئو |
با این مقایسه Rive برنده است چون ما نیاز به کنترل برنامهای، ماشینهای حالت و فشردگی برای اپ موبایل داریم.
بهینهسازی عملکرد
- بارگذاری پیشبار: فایلهای .riv هنگام شروع اپ بارگذاری میشوند، نه برای هر تمرین
- رندر GPU: Rive به صورت خودکار از GPU استفاده میکند و روی دستگاههای قدیمیتر با CPU ادامه میدهد
- حوضچه حافظه: استفاده مجدد از کنترلرهای Rive بین صفحات برای جلوگیری از توقفهای جمعآوری زباله
- فشردهسازی: فایلهای Rive به طور ذاتی فشرده هستند و نیاز به بهینهسازی اضافه ندارند
نتیجه: انیمیشن ۶۰ فریم بر ثانیه روی پردازنده Snapdragon 662 و بالاتر (گوشیهای میانرده ۲۰۱۹).
سؤالات متداول
سؤال: آیا میتوان انیمیشنها را از Adobe Animate به Rive صادر کرد؟
خیر، مستقیم خیر. ما از ویرایشگر بومی Rive (rive.app) استفاده میکنیم. انیماتورها در Rive طراحی میکنند و خروجی .riv را در اپلیکیشن Flutter استفاده میکنیم.
سؤال: چه طور انواع بدن یا معلولیتها را مدیریت میکنید؟
سفارشیسازی آواتار شامل گزینههای انواع بدن (لاغر، ورزشی، گرد) و لوازم جانبی (عینک، سمعک، کمکهای حرکتی) است تا همه کودکان احساس نمایندگی داشته باشند.
سؤال: اگر کودکی از آواتار خود خوشش نیاید چه؟
آنها هر زمان میتوانند آواتار خود را تغییر دهند. اپ هیچ نمای خاصی را تحمیل نمیکند — کنترل خلاقیت کامل با کودکان است.



