آموزش ساخت بازی دو بعدی در اندروید استودیو با کاتلین

آموزش ساخت بازی دو بعدی در اندروید استودیو با کاتلین
در این پست می‌خوانید:

به نام خدا، در این مقاله قصد دارم آموزش ساخت یک بازی دو بعدی در اندروید استودیو (سطل و توپ آجرشکن) با استفاده از زبان شیرین کاتلین رو بهت یاد بدم. برای ساخت بازی دو بعدی در اندروید استودیو لازمه که اول با مفهوم هایی مانند SurfaceView و Thread و یک سری کلاس هایی مثل RectF آشنا بشیم.

ویدیوی زیر تقریبا شبیه بازی دو بعدی در اندروید استودیو ساخته شدۀ ما هست که می خوایم آموزشش بدیم(در انتهای مقاله می تونید بازی ساخته شدۀ خودمون رو ببینی):

آموزش کلاس SurfaceView برای ساخت بازی دو بعدی در اندروید استودیو

خب چه بهتر که اندکی دربارۀ گرافیک کامپیوتری اطلاعات داشته باشیم تا بدونیم فلسفۀ Surface و SurfaceView و View و SurfaceFlinger و BufferQueue و … چیه و به چه نحوی با هم تعامل می کنن؛ البته با توجه به آنکه مقالۀ ما قرار است یک مقالۀ کابردی در خصوص ساخت بازی دو بعدی در اندروید استودیو باشه وارد بحث های آکادمی نمی شیم.

ما با ارث بری از کلاس های View و SurfaceView می تونیم یک عنصر رابط کاربری جدید بسازیم؛ درست مثل دکمه و چک باکس که عناصر رابط کاربری هستند و در xml پیاده سازی می شن؛ و بعدش می تونیم کلاس هایی که از این دو کلاس ارث بری کرده اند رو حتی توی فایل xml استفادشون کنیم(البته برای این کار لازمه که برای این کار تمام constructor های کلاس والد رو در کلاس فرزند مورد ارجاع قرار بدیم). و همچنین حتی با ارث بری از این دو کلاس می تونیم بازی دو بعدی در اندروید استودیو بسازیم!

خب حالا یک سوال؟ از کلاس SurfaceView چه زمانی ارث بری کنیم و نیز چه زمانی از کلاس View ارث بری کنیم؟ برای ساخت بازی دو بعدی در اندروید استودیو بهتره از کلاس SurfaceView ارث بری بکنیم چون Main Thread ما رو کمتر درگیر می کنه. ما می خوایم یک بازی دو بعدی در اندروید استودیو داشته باشیم که در آن مدام توپ و سطل در حال حرکت هستند، پس منطقی تره که از SurfaceView استفاده کنیم تا از نظر پردازش بهینه تر باشه.

چون نمی خوایم وارد بحث های آکادمی بشیم و می خوایم به صورت کاربردی ساخت بازی رو یاد بگیریم پس همین قدر پیش گفتار بس می کنه و وارد فضای کد می شیم:

package ir.maziar.ballandpaddle.myGame

import android.content.Context
import android.util.AttributeSet
import android.view.SurfaceHolder
import android.view.SurfaceView

class MyGameView(context: Context,attrs: AttributeSet) : SurfaceView(context,attrs) , SurfaceHolder.Callback {

    init {
        holder.addCallback(this)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {

    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {

    }
}

خب همونجور که گفتیم از کلاس SurfaceView ارث بری می کنیم. کلاس SurfaceView یک پراپرتی holder داره که می تونیم با استفاده از تابع addCallback بهش یک callback بدیم تا از سه رخداد زیر مطلع بشیم و در دمی که این سه رخداد، روی دادند؛ بتونیم اقدامات لازم جهت ساخت بازی دو بعدی در اندروید استودیو انجام بدیم:

  1. surfaceCreated  یعنی زمانی که surface (به معنای سطح) ساخته شده و ما می تونیم عناصر و جریان بازی رو عملیاتی کنیم.
  2. surfaceChanged برای زمانی که surface عوض شد، با این تابع توی این آموزش کاری نداریم.
  3. surfaceDestroyed برای زمانی که surface نابود شد ؛ در این حالت دیگه امکان ادامه دادن جریان عملیات بازی وجود نداره و باید منابع و روند جنبش بازی رو متوقف کنیم.

در واقع کلاسی که از کلاس surfaceView مشتق شده (ارث بری کرده) برای ساخت بازی دو بعدی در اندروید استودیو نقش board یا هستۀ بازی رو ایفا می کنه. در منطق بازی سازی ما باید یک board داشته باشیم که فرایند جریان بازی رو مدیریت می کنه.

ما با استفاده از surfaceView می تونیم اشکال هندسی مثل مستطیل و مربع رسم کنیم؛ یا می تونیم عکس و متن و خط و چیز هایی این چنینی رو ترسیم کنیم. سپس با استفاده از منطق برنامه نویسی می تونیم این اشکال هندسی یا عکس ها رو با استفاده از یک Thread جداگانه به حرکت در بیاریم(بجای Thread از کوروتین یا RxJava می شه استفاده  کرد؛ در دورۀ نخبان اندروید استاد نوری کوروتین و RxJava همراه با معماری های خفن اندروید جهت ایجاد اپلیکیشن های تجاری آموزش داده شدن).

فعلا نمی خوایم دربارۀ حرکت اشکال هندسی که با استفاده از surfaceView رسم می کنیم صحبت کنیم. اکنون تنها قصد دارم به موضوع رسم شکل های مستظیل و دایره و استفاده از رنگ ها بپردازم. در واقع surfaceView می توه مثل یک کاغذ برای نقاشی کردن قلمداد بشه، بعدا با استفاده از Thread می تونیم موجودیت های این نقاشی رو به حرکت در بیاریم و در آخر با منطق برنامه نویسی این پویش ها رو قانون مند کنیم تا یک بازی دو بعدی در اندروید استودیو آفریده بشه.

رسم اشکال هندسی با کمک surfaceView

بازی دو بعدی در اندروید استودیو می تونه صرفا بر اساس اشکال هندسی ترسیمی (بدون استفاده از عکس) شکل بگیره. در بازی توپ و سطل فقط کافیه یک دایره در نقش توپ و یک مستطیل سیاه در نقش سطل و یک ماتریسی (آرایۀ دو بعدی ای) از مربع های آبی رسم کنیم

آموزش ساخت بازی دو بعدی در اندروید استودیو

عناصر بازی صرفا اشکال هندسی ای هستند که در surfaceView ترسیم کریم(فضای آبی رنگ یک ماترس سه در هشت از مستطیل های آبی رنگ هست)

اگر توجه بکنی پس زمینۀ بازی من سفید است. به طور پیش فرض پس زمینه سیاه هست و برای سفید شدن پس زمینه باید با استفاده از کلاس Canvas به رنگ آمیزی پس زمینه بپردازیم. کد زیر رو به onSurfaceCreated بیفزونید:

override fun surfaceCreated(holder: SurfaceHolder) {
        val canvas = holder.lockCanvas()
        canvas.drawColor(Color.WHITE)
        holder.unlockCanvasAndPost(canvas)
    }

ما یک شئ از کلاس surfaceHolder داریم. همانطور که از معنای نام این کلاس مشخص است این کلاس نگهدارندۀ سطح می باشه. سطح یک مفهوم در علم گرافیک موبایله (شاید توی علم گرافیک کامپیوتری هم باشه، من دربارۀ بحث کامپیوتر اطلاعی ندارم) که کلاسی با نام Canvas رو درون خودش داره، می تونیم روی Canvas نقاشی کنیم درست مثل کاغذ نقاشی(خود Canvas به معنای بوم نقاشی هست).

اول باید Canvas رو قفل کنی و بعد مقداری که تابع lockCanvas بر می گردونه که اشاره گری به یک شئ از کلاس Canvas هست رو درون یک متغییر قرار بدی. وقتی canvas قفل باشه می تونی شروع به نقاشی کردن بکنی و پس از پایان نقاشی باید قفل Canvas رو بازگشایی بکنی و ارسالش کنی.

الان در کد بالا ما canvas رو قفل کردیم و بعد قبل از بازگشایی قفل canvas اقدام به رنگ آمیزی پس زمینه به رنگ سفید کردیم. حالا پس زمینه بازی دو بعدی در اندروید استودیو ساخته شده سفیده و دیگه سیاه نیست.

یادمون نره برای آنکه board بازیمون نمایش داده بشه یا باید به View Root اکتیویتی اضافش کنیم یا باید کلا به عنوان root به تابع setContentView یک شئ ازش رو پاس بدیم، کد زیر رو نگاه کن:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(MyGame(this,null))
    }
}

حالا به محض اجرای بازی دو بعدی در اندروید استودیو ساخته شده، به یک پس زمینۀ سفید بر می خوریم.

رسم مستطیل سیاه به عنوان سطل در بازی دو بعدی در اندروید استودیو

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

برای رسم مستطیل از کلاس RectF استفاده می کنیم که ویژۀ رسم مستطیل هست. از یکی از سازنده های چهار پارامتری این کلاس استفاده می کنیم.

public RectF(     
    float left,
    float top,
    float right,
    float bottom
)

برای رسم کردن اشکال هندسی یا هر چیز دیگری با استفاده از کلاس Canvas (در اینجا مستطیل برای بازی دو بعدی در اندروید استودیو) باید بدونیم که صفحۀ SurfaceView از ربع چهارم محور مختصات x و y پیروی می کنه. نقطۀ صفر و صفر مختصات بالا سمت چپ صفحه میوفته.

آموزش ساخت بازی دو بعدی در اندروید استودیو

برای رسم مستطیل سیاه در راستای ساخت بازی دو بعدی در اندروید استودیو نقطۀ p1 گوشۀ بالا سمت چپ مستطیل و نقطۀ p2 گوشۀ پایین سمت راست مستطیل می شه. نسبت چهار پارامتر های constructor کلاس RectF با دو نقطۀ p1 و p2 نیز، در تصویر بالا به نمایش گذاشته شده اند. مثلا top و let به ترتیب مقدار x و y نقطۀ p1 هستند، بنابرین داریم:

p1(left,top) , p2(right, bottom)

مثلا توی نرم افزار paint وقتی می خوای مربع رسم کنی اول کلیک می کنی و بعد اشاره گر موس رو درگ می کنی و بعد در یک نقطۀ دیگه رها می کنی. در دمی که کلیک می کنی نقطۀ p1 بوجود میاد و در دمی که کلید موس رو رها می کنی نقطۀ p2 بوجود میاد و بر اساس این دو نقطه مربع توی paint رسم می شه. اینجا هم همینطوره فقط بجای آنکه نقطه های p1 و p2 رو به RectF بدیم، پارامتر های x و y این دو نقطه رو تحت اسامی left, top, right, bottom به سازندۀ کلاس می دیم تا سطل بازی دو بعدی در اندروید استودیو ساخته شده ایجاد شه.

override fun surfaceCreated(holder: SurfaceHolder) {
        val canvas = holder.lockCanvas()
        canvas.drawColor(Color.WHITE)
        val rectWidth = width / 5f
        val rectHeight = height / 18f
        val rectX = (width / 2) - (rectWidth / 2)
        val rectY = height - (height / 6f)
        val paddle = RectF(rectX,rectY,rectX+rectWidth,rectY+rectHeight)
        val rectPaint = Paint().apply {
            color = Color.BLACK
            style = Paint.Style.FILL
        }
        canvas.drawRect(paddle,rectPaint)
        holder.unlockCanvasAndPost(canvas)
    }

 

خب بعد از آنکه اشاره گر شئ canvas رو از holder دریافت کردیم و صفحه بازی دو بعدی در اندروید استودیو توسعه یافته شده رو سفید کردیم می خوایم بگیم عرض و ارتفاع مستطیل سیاه چقدر باشه. اندازۀ عرض کل surfaceView درون یک پراپرتی با نام width ذخیره شده و اندازۀ ارتفاع surfaceView هم درون یک پراپرتی دیگه با نام height ذخیره شده. در واقع این width و height ها با عرض و ارتفاع صفحۀ اکتیویتی برابر هستند چون در کد های بالاتر ما مستقیما یک شئ از کلاس surfaceView رو به تابع setContentView که داخل اکتیویتیه پاس دادیم.

خب اگر width رو تقسیم بر 5 کنیم، اندازۀ یک پنجم عرض صفحۀ رو بدست میاریم و چون می خوایم عرض سطل ما یک پنجم اندازۀ عرض صفحه باشه پس کد

val rectWidth = width / 5f رو برای نگهداری اندازۀ عرض مستطیل در نظر می گیریم. برای نگهداری ارتفاع مستطیل هم چون می خوایم ارتفاع مستطیل برابر با یک هجدهم ارتفاع صفجه باشه پس از کد :val rectHeight = height / 18f استفاده می کنیم. حالا عرض و ارتفاع مستطیلمون رو داریم و می خوایم به نقطۀ مختصات x و y زاویۀ بالا سمت چپ مستطیل، دسترسی داشته باشیم(که ازش با نام نقطۀ p1 در بالاتر یاد شد):

val rectWidth = width / 5f
val rectHeight = height / 18f
val rectX = (width / 2) - (rectWidth / 2)

می گیم موقعیت x ما بیوفته وسط صفحه (یعنی فاصلش از چپ و از راست صفحۀ نمایش یکسان باشه)؛ اما بعد از آنکه x وسط صفحه افتاد، به اندازۀ نصف عرض مستطیل‌مون ازش کم کن. هدف از این کار آنه که مستطیل دقیقا وسط صفحه بیوفته چون ما داریم دربارۀ نقطۀ p1 صحبت می کنیم و این x در واقع سمت چپ ترینِ مستطیل ماست! ، اگر نصف عرض مستطیل رو ازش کم نکنیم و بعدا مستطیل رسم بشه، مستطیل ما مایل به سمت راست صفحه میوفته و دقیقا وسط صفحۀ بازی دو بعدی در اندروید استودیو ساخته شده نخواهد بود.

درواقع x دقیقا همان پارامتر left ما هست که در تصویر بالایین دیدی. باید ازش نصف عرض مستطیل رو کم کنیم تا مستطیل دقیقا در وسط عرض صفحه قرار بگیره.

val rectWidth = width / 5f
val rectHeight = height / 18f
val rectX = (width / 2) - (rectWidth / 2)
val rectY = height - (height / 6f)

حالا می خوایم دربارۀ ارتفاع مستطیل صحبت بکنیم که بهش y می گیم و y چیزی جز همان top که در تصویر بالا بهش پرداختیم نیست. درواقع بعدا به top میایم rectHeight رو می افزونیم تا bottom بدست بیاد و همچنین به left هم rectWidth رو می افزونیم تا right بدست بیاد و مستطیل ما برای بازی دو بعدی در اندروید استودیو ساخته شدۀ ما، آمادۀ ایفای نقش به عنوان سطل بشه.

فراموش نکن که نقطۀ (0,0) زاویۀ بالا سمت چپ صفحه هست، این یعنی برای ترسیم ما همیشه توی ربع چهارم ناحیۀ مختصاتی هستیم. برای همین اگر فقط ارتفاع صفحه رو تقسیم بر 6 بکنیم مسطتیل بالای صفحه میوفته؛ برای همین لازمه ارتفاع کل صفحه رو از یک ششم صفحه کم بکنیم تا سطل بازی دو بعدی در اندروید استودیو ساخته شده پایین صفحه بیوفته.

val paddle = RectF(rectX,rectY,rectX+rectWidth,rectY+rectHeight)
val rectPaint = Paint().apply {
     color = Color.BLACK
     style = Paint.Style.FILL
}
canvas.drawRect(paddle,rectPaint)

خب بالاخره از کلاس RectF استفاده کردیم و پارامتر هایش رو پر کردیم. حالا باید یک شئ از کلاس Paint بسازیم تا با استفاده از پراپرتی های color و style بگیم که رنگ مستطیل‌مون سیاه و توپُر باشه. شئ Paint همانطور که از نامش پیداست برای سبک دهی و رنگ آمیزی و ظاهر دهی بکار می ره.

در آخر هم با استفاده از تابع drawRect مستطیل‌مون رو رسم می کنیم.

خروجی بازی دو بعدی در اندروید ساخته شده تا اینجا باید اینجور باشه:

سطل در بازی دو بعدی در اندروید استودیو ساخته شده

سطل در بازی دو بعدی در اندروید استودیو ساخته شده

کتاب the art of game design در باب هنر بازی سازی محتوای شناخته شده ای دارد

ایجاد دایره به عنوان توپ در بازی دو بعدی در اندروید استودیو

برای ایجاد توپ دایره ای‌مون در بازی دو بعدی در اندروید استودیو ساخته شده از تابع  canvas.drawCircle() استفاده می کنیم. چهارتا پارامتر x و y و radius و Paint می گیره تا دایره رو برای ما رسم کنه. برخلاف RectF که x و y برابر با زاویۀ سمت چپ و بالای شئ هندسی بود؛ برای رسم دایره x و y صرفا به مرکز دایره اشاره داره. radius هم به معنای شعاع دایره هست. شعاع بیشتر، دایرۀ بزرگتر و شعاع کمتر، دایرۀ کوچکتر.

کد هامون:

val circleX = width / 2f
val circleY = height / 2f
val radius = (width + height) / 100f
val circlePaint = Paint().apply {
       color = Color.RED
       style = Paint.Style.FILL
}
canvas.drawCircle(circleX,circleY,radius,circlePaint)

می گیم شعاع دایره برابر با یک صدمِ جمع عرض و ارتفاع صفحه نمایش باشه و دایره دقیقا مرکز صفحه بیوفته (x و y دقیقا وسط صفحه میوفتن)، همچنین رنگ دایره قرمز و توپُر باشه.

بازی دو بعدی در اندروید استودیو رسم توپ

بازی دو بعدی در اندروید استودیو: توپ رسم شده تا اینجا

ایجاد آجر های بازی دو بعدی در اندروید استودیو ساخته شده

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

//matrix of blocks
        val blockWidth = width / 7f
        val blockHeight = height / 16f
        var blockX: Float
        var blockY: Float
        val blockPaint = Paint()
        for (i: Int in 0 until 7){
            blockY = i * blockHeight
            for (j : Int in 0 until 7){
                blockX = j * blockWidth
                blockPaint.apply {
                    style = Paint.Style.FILL
                    color = Color.BLUE
                }
                val block = RectF(blockX, blockY,blockX+blockWidth,blockY+blockHeight)
                canvas.drawRect(block,blockPaint)
                blockPaint.apply {
                    style = Paint.Style.STROKE
                    color = Color.CYAN
                    strokeWidth = 20f
                }
                canvas.drawRect(block,blockPaint)
            }
        }

عرض هر آجر یک هفتم عرض صفحه هست و ارتفاع هر آجر یک شانزدهم ارتفاع صفحه هست. blockX و blockY ها هم همان left و top های هر آجر می باشه. blockPaint هم برای سبک دهی و ظاهر دهی به آجر ها هست.

حلقه های دو بعدی ما به این صورت هست که حلقۀ بیرونی سطر ها (Vertical) رو می شمره و حلقۀ داخلی ستون ها (Horizontal) رو می شمره؛ پس حلقۀ بیرونی با blockHeight سر و کار داره و حلقۀ درونی با blockWidth. از آنجایی که عرض هر سطر یک هفتم صفحه هست؛ پس هر بار که حلقه پیمایش می شه باید blockX رو برابر با حاصل ضرب blockWidth و i (که شمارندۀ حلقۀ بیرونی هست) باشه.

و هر بار که j (که شمارندۀ حلقۀ داخلی هست و با blockWidth سر و کار داره) پیمایش می شه، باید ضربدر blockHeight بشه و حاصل ضربشان در blockY ریخته بشه. بعدش هم به ازای هر آجر یک شئ از کلاس RectF می سازیم.

حالا می خوایم دوباره از paint استفاده کنیم یک بار به صورت Fill که خروجی به صورت زیر در میاد:

آجر های بازی دو بعدی در اندروید استودیو با خاصیت Fill

آجر های بازی دو بعدی در اندروید استودیو با خاصیت Fill

بعدش فقط سبک paint رو روی stroke تنظیم می کنیم و دوباره RectF رو باهاش ترسیم می کنیم. حالا هر آجر یک قاب برای خودش داره تا از هم قابل تشخیص باشن:

آجر های بازی دو بعدی در اندروید استودیو با خاصیت Stroke روی fill

آجر های بازی دو بعدی در اندروید استودیو با خاصیت Stroke روی fill

خب دوستان حالا رسم کردنی ها رو رسم کردیم و می مونه که فقط بهشون پویایی و منطق ببخشیم:

بخشیدن پویایی و منطق برای آفریدن بازی دو بعدی در اندروید استودیو

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

مثلا می تونیم از دیزاین پترن های Abstract Factory و Builder و Prototype و دیگر دیزاین پترن های Creational برای ساخت شخصیت های بازی با توجه به سناریوی بازی استفاده کنیم تا از مزایای دیزاین پترن ها برخوردار بشیم(من مقالات متعددی دربارۀ دیزاین پترن های Creational نوشته ام و در آکادمی نوری منتشر کردم و در مقالۀ آموزش جامع دیزاین پترن ها در کاتلین به صورت عمومی دربارۀ دیزین پترن ها توضیح دادم).

استفاده از Multi-Threading برای ساخت بازی دو بعدی در اندروید استودیو

در واقع با کمک یک Thread جداگانه در کنار MainThread می تونیم مسئولیت پویایی توپ و سایر جنبش های بازی دو بعدی در اندروید استودیو سخته شدۀمان رو بپذیریم. یک کلاس با نام MyThread می سازیم که از کلاس Thread ارث بری می کنه(می تونیم بجای تعامل مستقیم با کلاس Thread ، از کوروتین ها استفاده کنیم [ کوروتین چیست؟ ] ).

قبل تعریف کلاس‌مون، دو تا تابع جدید در کلاس MyGame تعریف می کنیم:

fun drawing(canvas: Canvas) {
        canvas.drawColor(Color.WHITE)
        
    }

    fun update() {
         
    }

همچنین تمام کد هایی که توی تابع surfaceCreated نوشتیم رو پاک می کنیم، چون می خوایم برای توپ و سطل و آجر ها کلاس های جداگانه بنویسیم.

سپس کلاس MyThread رو برا پویایی بخشی به بازی دو بعدی در اندروید استودیو مان تعریف می کنیم:

package ir.maziar.ballandpaddle.myGame

import android.view.SurfaceHolder

class MyThread(private val surfaceHolder: SurfaceHolder,private val  myGame: MyGame) : Thread() {

    var isRunning: Boolean = true

    override fun run() {
        super.run()
        while (isRunning){
            val canvas = surfaceHolder.lockCanvas()
            synchronized(surfaceHolder){
                myGame.drawing(canvas)
                myGame.update()
            }
            surfaceHolder.unlockCanvasAndPost(canvas)
        }
    }
}

کلاس ما دوتا ورودی می گیره. یکی یک شئ از کلاس MyGame و یکی یک شئ از کلاس SurfaceHolder. یک پراپرتی با نام isRunning تعریف می کنیم تا برای از بین بردن فرآیند بازی ازش کمک بکنیم. بعد درون تابع run یک حلقه بر اساس همان پراپرتی تعریف می کنیم و در یک چهار چوب safe-Thread با surfaceHolder کار می کنیم، (با توجه به احتمال آنکه بعدا از Thread های دیگر برای دسترسی surfaceHolder ممکنه طی گسترش بازی در سناریوی های پیشبینی نشده، استفاده بشه؛ برای آینده نگری از بلاک synchronized استفاده کنیم).

سپس دو تابع drawing و update رو صدا می زنیم. قرار تمام کلاس های توپ و سطل و آجر از طریق این تابع drawing رسم بشوند و در تابع update موقعیت های x و y  آنها تغییر کند(شکل های رسم شده با تغییر x و  y پویا بشوند و بازی دو بعدی در اندروید استودیو به جنبش بیوفتد).

کلاس توپ

package ir.maziar.ballandpaddle.myGame

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF

class MyBall(private var width: Int, private var height: Int) {
    //x of center of circle
    private var circleX = width / 2f

    //y of center of circle
    private var circleY = height / 2f

    //شعاع دایره
    var radius = (width + height) / 100f

    //steps to move / speed
    private var sX = 30f
    private var sY = 30f

    private val circlePaint = Paint().apply {
        color = Color.RED
        style = Paint.Style.FILL
    }

    fun draw(canvas: Canvas) {
        canvas.drawCircle(circleX, circleY, radius, circlePaint)
    }

    fun update(paddle: MyPaddle, isVictory : Boolean) {
        //if ball pass the paddle
        if (circleY + radius + sY > paddle.bounds.bottom) {
            if (!isVictory) {
                //game Over and ball is in jail
                paddle.rectY = height.toFloat() / 2f
                paddle.rectX = 0f
                paddle.rectWidth = width.toFloat()
            }else{
                //victory and big red ball and black bg
                paddle.rectX = 0f
                paddle.rectY = 0f
                paddle.rectWidth = width.toFloat()
                paddle.rectHeight = height.toFloat()
            }
        }
        //reverse x and y by limit space
        if ((circleX + radius + sX > width) || (circleX - radius + sX < 0)) {
            reverseX()
        }
        if ((circleY + radius + sY > height) || (circleY - radius + sY < 0)) {
            reverseY()
        }
        //move
        circleX += sX
        circleY += sY
    }

    //live bounds
    val bounds
        get() = RectF(
            circleX - radius,
            circleY - radius,
            circleX + radius,
            circleY + radius
        )


    //reverse

    private fun reverseX() {
        sX = -sX
    }

    fun reverseY() {
        sY = -sY
    }
}

 

ورودی های این کلاس width و height هستن یعنی عرض و ارتفاع صفحه. circleX و circleY همان موقعیت های x و y مرکز توپ ما در بازی دو بعدی در اندروید استودیو ساخته شده هستن. radius هم شعاع دایره هست. sX و sY هم سرعت حرکت دایره هستند؛ برای کاهش و افزایش سرعت توپ باید این دو پراپرتی رو کم و زیاد کنی.

در تابع draw توپ را رسم می کنیم. با توابع reverseX و reverseY وقتی توپ به دیوار های صفحه نمایش (یا آجر و سطل) برخورد کرد، کاری می کنیم تا حرکت توپ با توجه به قوانین فیزیک معکوس شه، تنها قانون فیزیکی که در این بازی دو بعدی در اندروید استودیو ساخته شدۀ ما رعایت نمی شه؛ کاهش یافتن شتاب سرعت حرکت توپ است(توپ هرگز سرعتش کم نمی شود و در ادامه متوقف نخواهد شد).

در تابع update دو پارامتر ورودی داریم می گوییم اگر توپ از سطل رد شد و کاربر هنوز با نابودی تمامی آجر ها به پیروزی نهایی در بازی دو بعدی دست نیافته بود؛ سطل بیاد وسط بازی و تبدیل به حائل بین آجر ها و توپ بشه! بدترین نوع شکست اسارت هست، ما می گوییم اگر بازی دو بعدی در اندروید استودیو رو باختی توپت باید زندانی بشه! اما اگر تمامی آجر ها رو ترکونده بودی و به پیروزی رسیدی ، سطل سیاه تبدیل به پس زمینۀ بازی دو بعدی در اندروید استودیو ساخته شده بشه!

بازی دو بعدی در اندروید استودیو

وقتی که هنوز آجر ها نابود نشده باشند و به کف بازی دو بعدی در اندروید استودیو ساخته شده برخورد بکنی، توپت زندانی می شه!

بازی دو بعدی در اندروید استودیو

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

همچنین در تابع update می گوییم اگر دایره با عرض گوشی برخورد کرد، x ها معکوس شن و اگر به بالا و پایین صفحه برخورد کرد y ها معکوس بشن؛ تا توپ درون چهار دیواری صفحۀ گوشی گرفتار شه. و در پایین شرط ها circleX و circleY رو به ترتیب با sX و sY جمع می کنیم تا موقعیت دایره جابجا بشه.

ما دایره رو با استفاده از RectF بیان کردیم، زیرا این کلاس یک تابعی با نام intersects داره که بهمون کمک می کنه برخورد توپ با سایر عناصر بازی مثل سطل و آجر ها رو با هم تشخیص بدیم و بتونیم حرکت توپ رو reverse کنیم یا آجر ها رو پاک کنیم چون نابود شدند!

کد های surfaceHolder.lockCanvas()surfaceHolder.unlockCanvasAndPost(canvas) باعث می شن تمام نقاشی های قبلی ما بر روی surface پاک بشن و آخرین ترسیم های صورت گرفته جایگزین  ، برای همین ما فکر می کنیم توپ داره روی صفحه حرکت میکنه، چون توپی که در فریم قبلی رسم کرده بودیم پاک شده و توپ جدیدی که رسم کردیم جایگزینش شده.

سپس به کلاس MyGame می رویم و کد های زیر رو می افزونیم:

private val ball:MyBall by lazy { MyBall(width,height) }
private val thread : MyThread by lazy {  MyThread(holder,this) }

override fun surfaceCreated(holder: SurfaceHolder) {
        thread.isRunning = true
        thread.start()
}

override fun surfaceDestroyed(holder: SurfaceHolder) {
        var retry = true
        thread.isRunning = false
        while (retry) {
            try {
                thread.join()
                retry = false
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
        }
    }

    fun drawing(canvas: Canvas) {
        canvas.drawColor(Color.WHITE)
        ball.draw(canvas)
    }

    fun update() {
        ball.update()
    }

ما عناصر موجود در کلاس های MyThread و MyBall رو همانطور که باید، درون کلاس MyGame که هستۀ بازی ما یا Board بازی ما هست پیاده سازی کردیم.

در ادامۀ ساخت بازی دو بعدی در اندروید استودیو به سراغ آجر ها می ریم، در پایان مقاله برای تکمیل MyGame بر می گردیم:

کلاس MyBlocks

package ir.maziar.ballandpaddle.myGame

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF

class MyBlocks(
    left: Float,
    top: Float,
    right: Float,
    bottom: Float
) {
    var isVisible = true
    private val fillPaint = Paint().apply {
        style = Paint.Style.FILL
        color = Color.BLUE
    }
    private val strokePaint = Paint().apply {
        style = Paint.Style.STROKE
        color = Color.CYAN
        strokeWidth = 20f
    }

    val bounds = RectF(left, top, right, bottom)

    fun draw(canvas: Canvas): Boolean {
        if (isVisible) {
            canvas.drawRect(bounds, fillPaint)
            canvas.drawRect(bounds, strokePaint)
        }
        return isVisible
    }

    companion object {
        fun createBlocks(widthScreen: Int, heightScreen: Int): MutableList<MyBlocks> {
            val blockWidth = widthScreen / 2f
            val blockHeight = heightScreen / 23f
            val myBlocks: MutableList<MyBlocks> = mutableListOf()
            var x: Float
            var y: Float
            for (i: Int in 0 until 10) {
                y = i * blockHeight
                for (j: Int in 0 until 2) {
                    x = j * blockWidth
                    val myBlock = MyBlocks(x, y, x + blockWidth, y + blockHeight)
                    myBlocks.add(myBlock)
                }
            }
            return myBlocks
        }
    }


}

یک شئ در این کلاس معادل یک آجر در صفحه هست؛ در واقع ما با استفاده از تابع ایستای createBlocks اقدام به ایجاد ماتریسی از آجر ها در صفحه بازی دو بعدی در اندروید استودیو ساخته شده می کنیم. ما یک لیستی از آجر ها رو توسط این تابع return می کنیم.

این کلاس یک پراپرتی ای داره با نام isVisible که اگر true باشه آجرمون رسم می شه اگر نه آجر رسم نمی شه و دلیلش شرطی هست که توی تابع draw تعریف کردیم.

توپ در حال نابودی آجر های در دو ستون و ده سطر چیده شده

توپ در حال نابودی آجر های در دو ستون و ده سطر چیده شده در بازی دو بعدی در اندروید استودیو ساخته شده‌مون

کلاس سطل

package ir.maziar.ballandpaddle.myGame

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.view.MotionEvent

class MyPaddle(private val screenWidth  : Int,private val screenHeight : Int) {

    var rectWidth = screenWidth / 5f
    var rectHeight = screenHeight / 27f
    var rectX = (screenWidth / 2) - (rectWidth / 2)
    var rectY = screenHeight - (screenHeight / 6f)
    val bounds get() = RectF(rectX,rectY,rectX+rectWidth,rectY+rectHeight)

    private val rectPaint = Paint().apply {
        color = Color.BLACK
        style = Paint.Style.FILL
    }

    fun drawPaddle(canvas: Canvas){
        canvas.drawRect(bounds,rectPaint)
    }


    fun handleTouchEvent(event: MotionEvent) {
        when(event.action){
            MotionEvent.ACTION_MOVE -> {
                rectX = event.x - (rectWidth / 2)
                if (rectX < 0){
                    rectX = 0f
                }
                if (rectX + rectWidth > screenWidth){
                    rectX = screenWidth - rectWidth
                }
            }
        }
    }
}

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

اندازۀ عرض سطل یک پنجم صفحه هست و ارتفاعش هم یک بیست و هفتم صفحۀ بازی دو بعدی در اندروید استودیو ساخته شده‌مون.

پراپرتی bounds یک نکتۀ جالبی داره، ما می دونیم RectX و RectY و حتی RectWidth و RectHeight قراره بعدا تغییر بکنن، برای همین از get() که همون Getterمون هست استفاده می کنیم تا همیشه RectF ای که می خوایم در اختیار بگیریم با آخرین مقدار های پراپرتی هامون (یعنی RectX و RectWidth و…) سازگار باشه.

آموزش ساخت بازی مار و پله در اندروید رو از آکادمی نوری تهیه کنید!

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

ورودی این تابع یک MotionEvent هست که از MyGame بهش پاس می دیم، بعد بررسی می کنیم که آیا Action این MotionEvent از نوع move هست یا نه؟ به طور کلی در بازی های دو بعدی در در اندروید استودیو ساخته شده، از کلاس MotionEvent برای مدیریت رفتار های لمسی کاربر بر روی صفحۀ گوشی  استفاده شده.

تکمیل کلاس MyGame

private val ball: MyBall by lazy { MyBall(width, height) }
    private val blocks: MutableList<MyBlocks> by lazy { MyBlocks.createBlocks(width, height) }
    private val thread: MyThread by lazy { MyThread(holder, this) }
    private val paddle: MyPaddle by lazy { MyPaddle(width, height) }

    init {
        holder.addCallback(this)

        setOnTouchListener { _, event ->
            this.paddle.handleTouchEvent(event)
            return@setOnTouchListener true
        }
    }

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

fun drawing(canvas: Canvas) {
        canvas.drawColor(Color.WHITE)
        paddle.drawPaddle(canvas)
        ball.draw(canvas)
        val removed = mutableListOf<MyBlocks>()
        blocks.forEach {
            if (!it.draw(canvas)) {
                removed.add(it)
            }
        }
        blocks.removeAll(removed)
        removed.clear()
        if (blocks.isEmpty() && ball.radius < height / 2f){
            ball.radius += 3f
        }else if(blocks.isEmpty() && ball.radius >= height / 2f){
            thread.isRunning = false
        }
    }

تابع draw ای که داخل کلاس MyGame هست تمام تابع هایی که اسم هاشون draw هست و توی کلاس های دیگه هستند رو صدا می زنه و خودش هم داخل کلاس MyThread به صورت پیاپی تا زمانی که Thread جان در تن داره صدا زده می شه و برای همینه که بازی ما جان داره!

یک آجر اگر inVisible باشه، موقع صدا زدن تابع draw اش، مقدار false رو return می کنه. ما لیست آجر هامونو پیمایش می کنیم اگر تابع draw ی آجری، مقدار false برگردودند به یک لیست جدیدی با نام removed اضافش می کنیم؛ در آخر هم تمام عناصر removed رو از دل لیست  blocks پاک می کنیم. حالا اگر آجری دارای مقدار false برای پراپرتی isVisible اش باشه، برای همیشه پاک می شه.

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

تمام آجر ها در بازی دو بعدی در اندروید استودیو ساخته شده نابودش شده اند و دایره شروع به بزرگ شدن می کنه

تمام آجر ها در بازی دو بعدی در اندروید استودیو ساخته شده نابودش شده اند و دایره شروع به بزرگ شدن می کنه

چقدر این رفتار دایره من رو یاد خورشید خودمون می ندازه که پس از آنکه سوختش تمام شد شروع به بزرگ شدن می کنه تا جایی که دنیای اهل زمین(اگر تا آن موقع نابود نشده باشن) نابود بشه!

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

اینم بگم که وقتی توپ بازی دو بعدی در اندروید استودیو ساخته شده‌مون شروع به بزرگ شدن می کنه، دیگه با سطل سیاه رنگ برخورد نمی کنه و تنش های سطل سیاه رنگ و توپ قرمزمون برای همیشه تمام می شن! اینجا توپ داره رشد می کنه و سطل نمی تونه در برابر رشد توپ کار به جز تماشا کردن بکنه!

بد نیست دربارۀ فلسفۀ این بازی صحبت بکنیم:

یک بازی دو بعدی در اندروید استودیو ساخته شده داریم؛ توپ سعی می کنه آجر ها رو نابود بکنه اما حینی که آجر ها رو نابود می کنه اندازش کوچکتر می شه، تا جایی که نگرانش می شیم!

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

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

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

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

هدفم از بازگویی کردن این آرمان آن بود که بگم می تونیم با استفاده از معیار هایی به بازی هامون هدف هایی فراتر از سرگرم شدن ببخشیم! بیش از این سعی نمی کنم از این بازی مفهوم استنتاج کنم و به ادامۀ آموزش می پردازیم:

fun update() {
        ball.update(paddle,blocks.isEmpty())
        checkCollision()
    }

    private fun checkCollision() {
        if(RectF.intersects(paddle.bounds, ball.bounds)){
            if (blocks.isNotEmpty()) ball.reverseY()
        }
        blocks.forEach {
            if (RectF.intersects(it.bounds, ball.bounds)) {
                ball.reverseY()
                ball.radius--
                it.isVisible = false
            }
        }
    }

در کد بالا دوتا تابع داریم، تابع update که درون کلاس MyThread مانند تابع draw صدا زده می شده و تا وقتی Thread جان داشته باشه این تابع صدا زده می شه؛ و تابع checkCollision که بررسی می کنه آیا توپ با آجر ها برخورد کرده یا نه؟ آیا توپ با سطل برخورد کرده یا نه؟

برای بررسی رویداد بخورد دو تا چیز در بازی دو بعدی در اندروید استودیو توسعه داده شده، باید از تابع intersects که یک تابع ایستا در کلاس RectF هست استفاده کنیم. حالا آن bounds هایی که توی کلاس ها توپ و سطل و آجر تعریف کردیم به کارمون میاد! چون ما با استفاده از آنها می تونیم برخورد رو تشخیص بدیم و بگیم آجر پاک بشه بعد آنکه با توپ برخورد کرد، یا توپ Reverse بشه بعد آنکه با سطل برخورد کرد(و به سمت آجر ها دوباره تازش ببره).

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

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

کاربر که هرچه در بازی دو بعدی در اندروید استودیو ساخته شده بیشتر پیشروی می کن و آجر های بیشتری رو نابود می کنه و به هدف بیشتر نزدیکتر می شه، مجبور شاهد ضعف تدریجی توپ باشه و نگران آیندۀ توپ بشه، (سطل می ترسه تلاش هایی که می کنه، حیات توپ رو به خطر بندازه یا نتونه مانع برخوردش با پایین صفحۀ بازی بشه این یعنی حس تردید)

پایان

دوستان این بازی دو بعدی در اندروید استودیو ساخته شده به محض اجرا شدن برنامه‌مون شروع می شه و باید سریع سبد رو بکشید سمت راست تا توپ رو بگیره، سبد رو با انگشتتون لمس کنید و بکشید سمت راست تا نزاره توپ بره پایین و زندانی بشه.

همچنین با توجه به چرخه حیات Thread و SurfaceView ، اگر بازی رو باختی کلا اپلیکیشن رو از TaskManager گوشی پاک کن و دوباره برنامه رو از اول اجرا کن. بهتره از کوروتین ها بجای خود Thread استفاده کنی. اگر حس کردی بازی لگ می زنه بخاطر Thread هست؛ بهتره از موارد بهینه تر بجای استفاده مستقیم از Thread مثل Rx و کوروتین استفاده کنی که چون این مقاله آموزشی بود من پایه ترین حالات رو بررسی کردم و بر اهداف این آموزش متمرکز بودم.

ویدیوی بازی دو بعدی در اندروید استودیو ساخته شده ای که در این مقاله آموشش دادیم  :

(سورس بازی رو در انتهای مقاله قرار دادیم)

به زودی آموزش ویدیویی این بازی در وبسایت قرار خواهد گرفت

جعبه دانلود فایل۱ فایل
دانلود سورس کد
گزارش خرابی لینک دانلود
فرم گزارش خرابی لینک دانلود
دیدگاه‌ها ۰
ارسال دیدگاه جدید