در مورد رندر در حلقه های بازی بیاموزید

یک روش بسیار محبوب برای اجرای حلقه بازی به این صورت است:

while (playing) {
    advance state by one frame
    render the new frame
    sleep until it’s time to do the next frame
}

چند مشکل در این مورد وجود دارد، اساسی ترین آنها این ایده است که بازی می تواند تعریف کند که "قاب" چیست. نمایشگرهای مختلف با نرخ‌های متفاوتی تازه می‌شوند و این نرخ ممکن است در طول زمان متفاوت باشد. اگر فریم‌هایی را سریع‌تر از آنچه نمایشگر می‌تواند نشان دهد تولید می‌کنید، باید گهگاه یکی را رها کنید. اگر آنها را خیلی آهسته تولید کنید، SurfaceFlinger به طور دوره‌ای نمی‌تواند بافر جدیدی برای به دست آوردن پیدا کند و فریم قبلی را دوباره نشان می‌دهد. هر دوی این موقعیت ها می توانند باعث اشکالات قابل مشاهده شوند.

کاری که باید انجام دهید این است که نرخ فریم نمایشگر را مطابقت دهید و وضعیت بازی را با توجه به مدت زمانی که از فریم قبلی گذشته است، پیش ببرید. چندین راه برای انجام این کار وجود دارد:

  • استفاده از کتابخانه Android Frame Pacing (توصیه می شود)
  • BufferQueue را پر کنید و به فشار برگشتی "swap buffers" تکیه کنید
  • استفاده از Choreographer (API 16+)

کتابخانه Android Frame Pacing

برای اطلاعات در مورد استفاده از این کتابخانه به دستیابی به سرعت قاب مناسب مراجعه کنید.

پر کردن صف

اجرای این کار بسیار آسان است: فقط بافرها را تا جایی که می توانید سریع عوض کنید. در نسخه‌های اولیه اندروید، این در واقع می‌تواند منجر به جریمه‌ای شود که در آن SurfaceView#lockCanvas() شما را به مدت 100 میلی‌ثانیه بخواباند. اکنون با BufferQueue سرعت می‌گیرد و BufferQueue با سرعتی که SurfaceFlinger می‌تواند خالی می‌شود.

یک نمونه از این رویکرد را می توان در Android Breakout مشاهده کرد. از GLSurfaceView استفاده می‌کند که در حلقه‌ای اجرا می‌شود که برنامه ()onDrawFrame را فراخوانی می‌کند و سپس بافر را تعویض می‌کند. اگر BufferQueue پر باشد، فراخوانی eglSwapBuffers() منتظر می ماند تا یک بافر در دسترس باشد. زمانی که SurfaceFlinger آن‌ها را منتشر می‌کند، بافرها در دسترس قرار می‌گیرند، که پس از خرید بافر جدید برای نمایش، این کار را انجام می‌دهد. از آنجا که این اتفاق در VSYNC رخ می دهد، زمان بندی حلقه قرعه کشی شما با نرخ تازه سازی مطابقت دارد. بیشتر.

چند مشکل در این رویکرد وجود دارد. اول، این برنامه به فعالیت SurfaceFlinger گره خورده است، که بسته به میزان کاری که باید انجام شود و اینکه آیا برای زمان CPU با سایر فرآیندها مبارزه می کند، زمان متفاوتی را می گیرد. از آنجایی که وضعیت بازی شما با توجه به زمان بین تعویض بافر پیشرفت می کند، انیمیشن شما با سرعت ثابت به روز نمی شود. هنگامی که با سرعت 60 فریم در ثانیه اجرا می‌کنید و ناهماهنگی‌ها به طور متوسط ​​در طول زمان مشخص می‌شوند، احتمالاً متوجه این ضربه‌ها نخواهید شد.

دوم، اولین دو تعویض بافر خیلی سریع انجام می شود زیرا BufferQueue هنوز پر نشده است. زمان محاسبه شده بین فریم ها نزدیک به صفر خواهد بود، بنابراین بازی چند فریم ایجاد می کند که در آن هیچ اتفاقی نمی افتد. در بازی‌هایی مانند Breakout که در هر بار تازه‌سازی صفحه نمایش را به‌روزرسانی می‌کند، به جز زمانی که بازی برای اولین بار شروع می‌شود (یا متوقف نشده است)، صف همیشه پر است، بنابراین تأثیر آن قابل توجه نیست. بازی‌ای که گهگاه انیمیشن را متوقف می‌کند و سپس به حالت سریع برمی‌گردد، ممکن است سکسکه‌های عجیبی ببیند.

طراح رقص

Choreographer به شما امکان می دهد تا یک تماس برگشتی تنظیم کنید که در VSYNC بعدی فعال شود. زمان واقعی VSYNC به عنوان یک آرگومان ارسال می شود. بنابراین حتی اگر برنامه شما فوراً بیدار نشود، همچنان تصویر دقیقی از زمان شروع دوره به‌روزرسانی نمایشگر دارید. استفاده از این مقدار، به جای زمان فعلی، یک منبع زمانی ثابت برای منطق به‌روزرسانی وضعیت بازی شما ایجاد می‌کند.

متأسفانه، این واقعیت که پس از هر VSYNC یک تماس برگشتی دریافت می کنید، تضمین نمی کند که پاسخ تماس شما به موقع اجرا شود یا بتوانید با سرعت کافی بر اساس آن عمل کنید. برنامه شما باید موقعیت هایی را که در آن عقب مانده است شناسایی کند و فریم ها را به صورت دستی رها کند.

فعالیت "Record GL app" در Grafika نمونه ای از این را ارائه می دهد. در برخی از دستگاه‌ها (مانند Nexus 4 و Nexus 5)، اگر فقط بنشینید و تماشا کنید، این فعالیت شروع به کاهش فریم می‌کند. رندر GL بی‌اهمیت است، اما گاهی اوقات عناصر View دوباره ترسیم می‌شوند و اگر دستگاه در حالت کم مصرف قرار گرفته باشد، پاس اندازه‌گیری/طراحی می‌تواند زمان بسیار زیادی طول بکشد. (طبق گفته systrace، پس از کند شدن ساعت در اندروید 4.4، به جای 6 میلی‌ثانیه، 28 میلی‌ثانیه طول می‌کشد. اگر انگشت خود را در اطراف صفحه بکشید، فکر می‌کند در حال تعامل با فعالیت هستید، بنابراین سرعت ساعت بالا می‌ماند و هرگز پایین نمی‌آیید. یک قاب.)

راه حل ساده این بود که اگر زمان کنونی بیش از N میلی ثانیه پس از زمان VSYNC باشد، یک فریم را در فراخوانی کرئوگراف رها کنید. در حالت ایده آل، مقدار N بر اساس فواصل VSYNC مشاهده شده قبلی تعیین می شود. برای مثال، اگر دوره به‌روزرسانی 16.7 میلی‌ثانیه (60 فریم در ثانیه) باشد، اگر بیش از 15 میلی‌ثانیه دیر اجرا می‌کنید، ممکن است فریم را رها کنید.

اگر اجرای "Record GL app" را تماشا کنید، می‌بینید که شمارنده فریم کاهش یافته افزایش می‌یابد، و حتی وقتی فریم‌ها پایین می‌آیند، یک چشمک قرمز در حاشیه می‌بینید. با این حال، مگر اینکه چشمان شما خیلی خوب باشد، لکنت انیمیشن را نخواهید دید. با سرعت 60 فریم در ثانیه، تا زمانی که انیمیشن با سرعت ثابتی به پیشرفت خود ادامه دهد، برنامه می‌تواند گاه به گاه فریم را بدون اینکه کسی متوجه شود رها کند. اینکه چقدر می‌توانید از آن دور شوید تا حدی به چیزی که می‌کشید، ویژگی‌های نمایشگر، و میزان توانایی فردی که از برنامه استفاده می‌کند در تشخیص jank بستگی دارد.

مدیریت موضوع

به طور کلی، اگر روی SurfaceView، GLSurfaceView یا TextureView رندر می‌کنید، می‌خواهید این رندر را در یک رشته اختصاصی انجام دهید. هرگز هیچ «بالا بردن سنگین» یا هر کاری که زمان نامشخصی را در رشته رابط کاربری نیاز دارد انجام ندهید. در عوض، دو رشته برای بازی ایجاد کنید: یک رشته بازی و یک رشته رندر. برای اطلاعات بیشتر به بهبود عملکرد بازی خود مراجعه کنید.

Breakout و "Record GL app" از رشته های رندر اختصاصی استفاده می کنند، و همچنین وضعیت انیمیشن را در آن رشته به روز می کنند. این یک رویکرد معقول است تا زمانی که وضعیت بازی به سرعت به روز شود.

بازی های دیگر منطق و رندر بازی را به طور کامل جدا می کنند. اگر یک بازی ساده داشتید که کاری جز جابجایی یک بلوک در هر 100 میلی ثانیه انجام نمی داد، می توانید یک رشته اختصاصی داشته باشید که این کار را انجام می داد:

run() {
    Thread.sleep(100);
    synchronized (mLock) {
        moveBlock();
    }
}

(شما ممکن است بخواهید زمان خاموشی یک ساعت ثابت را برای جلوگیری از جابجایی تنظیم کنید - sleep() کاملاً سازگار نیست و moveBlock() زمان غیر صفر می برد -- اما شما این ایده را دریافت می کنید.)

وقتی کد قرعه کشی بیدار می شود، فقط قفل را می گیرد، موقعیت فعلی بلوک را می گیرد، قفل را آزاد می کند و می کشد. به جای انجام حرکت کسری بر اساس زمان‌های دلتای درون فریم، فقط یک رشته دارید که اشیا را به امتداد حرکت می‌دهد و رشته‌ای دیگر که هنگام شروع طراحی، چیزها را به هر کجا که باشند می‌کشد.

برای صحنه‌ای با هر پیچیدگی، می‌خواهید فهرستی از رویدادهای آینده ایجاد کنید که بر اساس زمان بیداری مرتب شده‌اند و تا زمان موعد رویداد بعدی بخوابید، اما این ایده همان است.