چگونه بازی خود را در میان چند دستگاه همگام‌سازی کنیم؟

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 15 خرداد 1398
دسته بندی ها : اندروید

اگر در همگام‌سازی بازی مشکل دارید، به جای درستی آمده‌اید!

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

مدل ما

در این مقاله، ما یک بازی تخته ساده دارای دو بازیکن را بر می‌داریم. قبل انجام هر کاری، ما به دو بازیکن نیاز داریم؛ درست است؟

برای راه‌آندازی آن، شما باید یک ویژگی به نام matchmaking را پیاده‌سازی کنید که در آن یک گره معمولی در FirebaseDatabase خود دارید، و هر بازیکن می‌تواند چالش‌های خود را در آن پست کند. چالش پست شده شامل UID رقیب، و یک ارجاع دیگر به یک گره حرکات (moves node)، که حرکات در آن منتشر خواهند شد می‌باشد.

پس از این که هر دو بازیکن گره حرکات را به دست می‌آورند، یکی از آن‌ها باید اولین حرکت خود را پست کند. سپس بازیکن دوم، سپس اول و... ما از ChildEventListener در Firease برای دریافت حرکات پست شده توسط رقیب استفاده خواهیم کرد.

عمیق‌تر به کد وارد شوید

اساسا، ما دو کار برای انجام دادن داریم: ارسال یک حرکت و دریافت یک حرکت. کامپوننت FirebaseGameSynchronizer همین کار را انجام خواهد داد، اما تفسیر حرکت مورد نظر، توسط Modulator که شما پیاده‌سازی می‌کنید، انجام خواهد شد.

public class FirebaseGameSynchronizer implements ChildEventListener {

    private int mSelfMoveSoph;// Semaphore that stores the no. of moves we posted

    private boolean mMoveIndex;// no. of moves synced currently

    private DatabaseReference mMovesRecordList;// moves-node

    private Modulator mMessageModulator;

    private FirebaseGameSynchronizer(DatabaseReference movesRecordList,

                                     Modulator messageModulator) {

        mMovesRecordList = movesRecordList;

        mMessageModulator = messageModulator;

        mMoveIndex = 0;

        mSelfMoveSoph = 0;

        mMovesRecordList.addChildEventListener(this);

    }

    public void sendMoveMsg(String moveValue) {

        ++mSelfMoveSoph;

        FirebaseDatabase.getInstance().getReference(mMovesRecordList.getPath() + "/M" + mMoveIndex)

                .setValue(moveValue);

    }

    @Override

    public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

        if (mSelfMoveSoph > 0) {

            --mSelfMoveSoph;

            ++mMoveIndex;

            return;

        }


        Log.d("FirebaseGameSync", "Real-sync");

        mMessageModulator.onReceiveMove(false, dataSnapshot.getValue(String.class));

        ++mMoveIndex;

    }

    public interface Modulator {

        public void onReceiveMove(boolean isSyncingPast, String encodedMsg);

    }
}

mover حرکات را با استفاده از sendMoveMsg ارسال می‌کند. شما می‌توانید حرکت خود را به چند روش انکود کنید. برای مثال اگر یک قطعه از (a,b) به (c,d) منتقل شده است، پس حرکات را تحت عنوان abcd انکود کنید. من به خصوص اگر اندازه نمونه شما (یا اگر بازی شما تخته است، اندازه تخته شما) کمتر از ۱۰ است، این متد را به شدت پیشنهاد می‌کنم.

sendMoveMsg اساسا حرکت مورد نظر را به mMovesRecordList در گره حرکات آپلود می‌کند و انتظار دارد که بازیکن دیگر منتظر آن باشد.

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

پس من همچنین یک ویژگی جالب دیگر اضافه کردم (اگر می‌خواهید که در عوض هر دو بازیکن حرکت را دریافت کنند، فقط تمام ارجاع‌های به mSelfMoveSoph را حذف کنید): مخابره حرکت خود. هر زمان که sendMoveMsg فراخوانی می‌شود، mSeldMoveSoph را افزایش می‌دهد. ما می‌دانیم که حال با این مخابره چند حرکت را آپلود کرده‌ایم.

هر زمان که یک حرکت توسط Firebase اضافه شده است، onChildAdded فراخوانی می‌شود. اگر مخابره مورد نظر یک مقدار داشته باشد، onChildAdded حرکت را نادیده می‌گیرد. در غیر این صورت، mMessageModulator فراخوانی می‌شود تا حرکت را تفسیر کرده، و به کاربر نشان دهد. Modulator یک رابط تابعی است که متمم انکودر move-to-string شما می‌باشد. این رابط رشته آپلود شده به Firebase را می‌گیرد و آن را به حرکت تبدیل می‌کند.

صبر کنید، اگر کاربر یک فراخوانی را دریافت کند، این کار نخواهد کرد

بله، اگر کاربر یک درخواست را دریافت کند و برنامه شما متوقف شود... کاربر چگونه به بازی برگردد؟

باز هم بیایید یک Modulator را به این صورت بسازیم:

public class GenericGameFragment implements FirebaseGameSynchronizer.Modulator {
    public void onMoveReceived(boolean isSyncingPast, String encodedMsg) {
       // حرکت کن... آن را بر روی رابط کاربری نشان بده...

    }
}

حال دو اتفاق بد پیش خواهند آمد:

۱. اگر کاربر بازی را ترک کد، FirebaseGameSynchronizer به گره‌ای که منتظر آن است، متصل خواهند ماند. این یک نشت در مصرف حافظه و CPU است.

۲. FirebaseGameSynchronizer یک ارجاع به قطعه شما خواهد داشت. فقط آن را به این صورت در نظر داشته باشید که Modulator‌ مورد نظر باید رابط کاربری را بروزرسانی کند و ارجاعی به GenericGameFragment دارد.

همگام‌سازی و عدم همگام‌سازی به moves-node

من پیش‌تر از یک راه حل نسبتا ساده برای این مشکل استفاده می‌کردم. این راه حل، ترکیب از دو چیز است:

۱. پرچم همگام‌سازی (Sync flag): وقتی که شما به طور صحیح همگام‌سازی می‌کنید، FirebaseGameSynchronizer بعد از آن Modulator مورد نظر را فراخوانی خواهد کرد. در غیر این صورت، FirebaseGameSynchronizer حرکت را در یک buffer ذخیره خواهد کرد. در هنگام تنظیم مجدد پرچم، FirebaseGameSynchronizer در ابتدا حرکت را در buffer آن رها خواهد کرد.

۲. اتصال (Attachment): Modulator مورد نظر هر زمان که متد onStop مربوط به قطعه فراخوانی شود، حذف شود و هر زمان که متد onStart فراخوانی شود هم مجددا تنظیم می‌شود.

public class FirebaseGameSynchronizer implements ChildEventListener {

    private DatabaseReference mMovesRecordList;

    private Modulator mMessageModulator;

    private int mMoveIndex;

    private int mSelfMoveSoph;

    private boolean mSynced;

    private ArrayDeque<String> mUnsyncBuffer = new ArrayDeque<>();

    private void resyncAll() {

        while (!mUnsyncBuffer.isEmpty())

            mMessageModulator.onReceiveMove(true, mUnsyncBuffer.pop());

    }

    boolean isSynced = false;

    private FirebaseGameSynchronizer(DatabaseReference movesRecordList,

                                     Modulator messageModulator) {

        mMovesRecordList = movesRecordList;

        mMessageModulator = messageModulator;

        mMoveIndex = 0;

        mSelfMoveSoph = 0;

        mMovesRecordList.addChildEventListener(this);

    }

    public int moveCount() {

        return mMoveIndex;

    }

    public String recordPath () {

        return mMovesRecordList.getPath().toString();

    }

    public void attachModulator(Modulator modulator) {

        mMessageModulator = modulator;

    }

    public void detachModulator() {

        mMessageModulator = null;

    }

    public void startSync() {

        if (!isSynced) {

            resyncAll();

            isSynced = true;

            return;

        }

    }

    public void stopSync() {

        isSynced = false;

    }

    public void flush() {

        mMovesRecordList.removeEventListener(this);

    }

    public void sendMoveMsg(String moveValue) {

        ++mSelfMoveSoph;

        FirebaseDatabase.getInstance().getReference(mMovesRecordList.getPath() + "/M" + mMoveIndex)

                .setValue(moveValue);

    }

    @Override

    public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {

        if (mSelfMoveSoph > 0) {

            --mSelfMoveSoph;

            ++mMoveIndex;

            return;

        }

        if (!isSynced && dataSnapshot.getKey().charAt(0) == 'M') {

            mUnsyncBuffer.add(dataSnapshot.getValue(String.class));

            ++mMoveIndex;

            return;

        }
        Log.d("FirebaseGameSync", "Real-sync");

        mMessageModulator.onReceiveMove(false, dataSnapshot.getValue(String.class));

        ++mMoveIndex;
    }

    public static FirebaseGameSynchronizer newInstance(String moveListRecordPath,

                                                       Modulator modulator) {
        return new FirebaseGameSynchronizer(FirebaseDatabase.getInstance()

                .getReference(moveListRecordPath), modulator);

    }

    public interface Modulator {

        public void onReceiveMove(boolean isSyncingPast, String encodedMsg);

    }
}

قبل از استفاده از این همگام‌ساز جدید، به یاد داشته باشید که startSync() را فراخوانی کنید. در هنگامی که متد onStop فراخوانی می‌شود، stopSync را فراخوانی کنید و در هنگامی که متد onResume فراخوانی می‌شود هم آن را مجددا startSync کنید. حال شما باید detachModulator را فراخوانی کرده، و در هنگامی که متد onDestroy فراخوانی می‌شود هم آن را flush کنید.

برای دیدن پیاده‌سازی کامل، به این لینک بروید.

منبع

مقالات پیشنهادی

  • چگونه کد خود را خواناتر کنیم؟

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

    عرفان کاکایی