Merhaba,


Bu yazı serisinde Android Studio, Socket.io ve Mysql ile online oyun yapımından bahsediyorum.


Videolu anlatım için buraya tıklayınız.
Serinin ilk yazısı için buraya tıklayınız.
Serinin ikinci yazısı için buraya tıklayınız.


Serinin bu üçüncü yazısında anasayfa ekranını tasarlayıp dinamik olarak çalıştırıyoruz. Online kullanıcılara oyun isteği yapıp, isteğin kabul ve red durumlarını inceliyoruz.


Android Studio projesi üzerinde;


activity_main.xml dosyasını açarak şu kodları yazıyoruz ve anasayfa tasarımını yapıyoruz:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.activity.MainActivity">

    <TextView
        android:id="@+id/tvWelcome"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:fontFamily="@font/arbutus_slab"
        android:text="@string/text_welcome"
        android:textSize="20sp"
        android:gravity="center"
        android:textColor="@color/colorPrimary"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/ivLeaderboard"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/leaderboard"
        android:layout_marginTop="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tvWelcome" />

    <TextView
        android:id="@+id/tvOnlineUsers"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/text_online_users"
        android:fontFamily="@font/arbutus"
        android:textSize="20sp"
        android:gravity="center"
        android:layout_margin="10dp"
        android:textColor="@color/colorPrimaryDark"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/ivLeaderboard" />

    <View
        android:id="@+id/view"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/colorPrimaryDark"
        android:layout_margin="10dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tvOnlineUsers" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvOnlineUsers"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_margin="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/view" />

</androidx.constraintlayout.widget.ConstraintLayout>


Anasayfada bulunan RecyclerView içerisinde online kullanıcılar listelenecek. Listede bulunacak elemanların tasarımı için de list_item.xml adında bir layout dosyası oluşturup içeriğini şu şekilde dolduruyoruz:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="10dp">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp">

        <ImageView
            android:id="@+id/ivStatus"
            android:contentDescription="@string/app_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_available"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent" />

        <TextView
            android:id="@+id/tvUserName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/text_player"
            android:textSize="20sp"
            android:fontFamily="@font/arbutus_slab"
            android:layout_marginStart="10dp"
            android:textColor="@color/available"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@id/ivStatus" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

Burada kullanıcıların müsaitlik durumu ve kullanıcı adı görüntülenecek. Müsaitlik durumunu simgeleyen ikonları da drawable klasörü içine ic_available.xml (müsait) ve ic_busy.xml şeklinde iki dosya açarak şu şekilde oluşturuyoruz:


ic_available.xml:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:tint="@color/available"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"/>
</vector>

ic_busy.xml:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:tint="@color/busy"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"/>
</vector>


Online kullanıcılar listesini dinamik şekilde çalıştırmak ve kullanıcılara tıklayıp oyun isteği yapabilmek için model ve adapter sınıflarına ihtiyacımız olacak. Projemizin paket adının bulunduğu klasörde şu şekilde bir paket yapısı oluşturuyoruz:

com.yusufborucu.onlinemathgame

-> model

-> ui

--> activity

--> adapter

App.kt


model klasörü içerisine User.kt adında bir veri sınıfı (data class) oluşturuyoruz:

package com.yusufborucu.onlinemathgame.model

data class User (
    val id: String = "",
    val username: String = "",
    val status: Boolean = true
)


adapter klasörü içerisine de UserAdapter.kt adında bir adapter oluşturuyoruz:

package com.yusufborucu.onlinemathgame.ui.adapter

import android.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.yusufborucu.onlinemathgame.App
import com.yusufborucu.onlinemathgame.R
import com.yusufborucu.onlinemathgame.model.User
import kotlinx.android.synthetic.main.list_item.view.*

class UserAdapter(private val users: MutableList<User>): RecyclerView.Adapter<UserAdapter.UserViewHolder>()
{
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
        return UserViewHolder(view)
    }

    override fun getItemCount(): Int {
        return users.size
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val user = users[position]
        if (user.status) {
            holder.itemView.ivStatus.setImageResource(R.drawable.ic_available)
            holder.itemView.tvUserName.setTextColor(ContextCompat.getColor(holder.itemView.context, R.color.available))
        } else {
            holder.itemView.ivStatus.setImageResource(R.drawable.ic_busy)
            holder.itemView.tvUserName.setTextColor(ContextCompat.getColor(holder.itemView.context, R.color.busy))
        }
        holder.itemView.tvUserName.text = user.username
        holder.itemView.setOnClickListener {
            if (user.status) {
                val builder = AlertDialog.Builder(holder.itemView.context)
                builder.setTitle("Oyun İsteği")
                builder.setMessage(user.username + " kişisine oyun isteği yapmak istediğinize emin misiniz?")
                builder.setPositiveButton("İstek yap") { dialog, which ->
                    App.socket.emit("request", user.id)
                    Toast.makeText(holder.itemView.context, user.username + " kişisine oyun isteği yapıldı.", Toast.LENGTH_SHORT).show()
                }
                builder.setNegativeButton("Vazgeç") { dialog, which ->  }
                val dialog: AlertDialog = builder.create()
                dialog.show()
            } else {
                val builder = AlertDialog.Builder(holder.itemView.context)
                builder.setTitle("Oyun İsteği")
                builder.setMessage(user.username + " kişisi meşgul olduğu için ona oyun isteği gönderemezsiniz.")
                builder.setNegativeButton("Tamam") { dialog, which ->  }
                val dialog: AlertDialog = builder.create()
                dialog.show()
            }
        }
    }

    class UserViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
}

Burada yaptığımız işlemler;

  • list_item.xml adındaki layout dosyamızı adapter e view olarak tanımladık.
  • Adapter e parametre olarak gelen users dizisindeki her bir elemanı kontrol ettik. Müsaitlik durumuna (status) göre ekrandaki ivStatus'ün resmini ve tvUserName'in rengini değiştirdik.
  • tvUserName'in text'ine kullanıcının adını yazdırdık.
  • Listede bulunan her bir elemana tıklandığında kullanıcı müsaitse "aliveli kişisine oyun isteği yapmak istediğinize emin misiniz?" gibi bir AlertDialog açtırdık. İstek yap butonuna basılırsa server tarafına, istek yapılan kullanıcının id'si request emit'i ile gönderdik. Kullanıcı müsait değilse de "aliveli kişisi meşgul olduğu için ona oyun isteği gönderemezsiniz." gibi bir AlertDialog açtırdık.


Node.js projesi üzerinde;


server.js dosyamızı açıyoruz;

...

let games = [];

io.on('connection', socket => {

...

    socket.on('online_users', () => {
        io.emit('online_users', users);
    });

    socket.on('disconnect', () => {
        users = users.filter((user) => {
            return user.id !== socket.id;
        });
        games = games.filter((game) => {
            return game.home.id !== socket.id && game.away.id !== socket.id;
        });
        io.emit('online_users', users);
    });
 
    socket.on('request', (id) => {
        let user = users.find(user => user.id === socket.id);
        io.to(id).emit('request', user);        
    });

    socket.on('accept', (id) => {
        let homePlayer = users.find(user => user.id === id);
        let awayPlayer = users.find(user => user.id === socket.id);
        let joinPlayers = { home: homePlayer.username, away: awayPlayer.username };
        io.to(socket.id).emit('accept', joinPlayers);
        io.to(id).emit('accept', joinPlayers);
        homePlayer.status = false;
        awayPlayer.status = false;
        io.emit('online_users', users);       
         
        let game = {
            home: homePlayer,
            away: awayPlayer,
            time: 30,
            homeScore: 0,
            awayScore: 0,
            emit: false
        };
        games.push(game);
    });

    socket.on('reject', (id) => {
        let user = users.find(user => user.id === socket.id);
        io.to(id).emit('reject', user.username);
    });
...

Burada yaptığımız işlemler;

  • Client tarafından gelen online_users emit'ini (birazdan MainActivity.kt üzerinde yapacağız) karşılıyoruz. Aynı şekilde client tarafına online_users emit'i yaparak users dizisini gönderiyoruz.
  • Bir kullanıcı uygulamayı kapattığında socket bağlantısı da kapanmış oluyor. Bunu da disconnect kısmında karşılayarak bağlantısı kapanan kullanıcıyı users dizisinden siliyoruz. Tekrardan online_users emit'i yaparak online kullanıcılar listesini güncelliyoruz. Ayrıca kullanıcılar arasındaki oyunları tutacağımız games dizisini üst kısımda oluşturuyoruz. Bağlantısı kapanan kullanıcıya ait bir oyun varsa onu da games dizisinden siliyoruz.
  • Client tarafından gelen request emit'ini karşılıyoruz. İstek yapan kullanıcıyı bulup, istek yapılan kullanıcıya request emit'i gerçekleştiriyoruz.
  • Client tarafından gelen accept emit'ini karşılıyoruz. İstek yapan kullanıcıyı homePlayer, isteği kabul eden kullanıcıyı awayPlayer olarak tanımlıyoruz. Bu kullanıcıları joinPlayers objesinde birleştirip iki tarafa da accept emit'i yapıyoruz. İki tarafında müsaitlik durumunu false yapıp online_users emit'i ile online kullanıcılar listesini güncelliyoruz. Son olarak da game objesi oluşturup games dizisine ekliyoruz.
  • Client tarafından gelen reject emit'ini karşılıyoruz. İsteği reddeden kullanıcıyı bulup, istek yapan kullanıcıya reject emit'i yapıyoruz.


Android Studio projesi üzerinde;


MainActivity.kt dosyasını açarak şu kodları yazıyoruz:

package com.yusufborucu.onlinemathgame.ui.activity

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.nkzawa.socketio.client.Socket
import com.yusufborucu.onlinemathgame.App
import com.yusufborucu.onlinemathgame.R
import com.yusufborucu.onlinemathgame.model.User
import com.yusufborucu.onlinemathgame.ui.adapter.UserAdapter
import kotlinx.android.synthetic.main.activity_main.*
import org.json.JSONArray
import org.json.JSONObject

class MainActivity : AppCompatActivity() {

    lateinit var socket: Socket

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val preferences = getSharedPreferences("app", Context.MODE_PRIVATE)
        val username = preferences.getString("username", "")
        tvWelcome.text = "Hoşgeldin $username"

        socket = App.socket

        socket.emit("online_users")

        socket
            .on("online_users") {
                runOnUiThread {
                    run {
                        val onlineUsers = mutableListOf<User>()
                        val users = JSONArray(it[0].toString())
                        for (i in 0 until users.length()) {
                            val item = users.getJSONObject(i)
                            if (item.getString("id") != socket.id())
                                onlineUsers.add(User(item.getString("id"), item.getString("username"), item.getBoolean("status")))
                        }
                        rvOnlineUsers.layoutManager = LinearLayoutManager(applicationContext)
                        rvOnlineUsers.adapter = UserAdapter(onlineUsers)
                    }
                }
            }
            .on("request") {
                runOnUiThread {
                    run {
                        val user = JSONObject(it[0].toString())
                        val username = user.getString("username")
                        val id = user.getString("id")
                        val builder = AlertDialog.Builder(this)
                        builder.setTitle("Oyun İsteği")
                        builder.setMessage("$username seninle oynamak istiyor.")
                        builder.setPositiveButton("Kabul Et") { dialog, which ->
                            socket.emit("accept", id)
                            Toast.makeText(applicationContext, "$username kişisinden gelen oyun isteği kabul edildi.", Toast.LENGTH_SHORT).show()
                        }
                        builder.setNegativeButton("Reddet") { dialog, which ->
                            socket.emit("reject", id)
                            Toast.makeText(applicationContext, "$username kişisinden gelen oyun isteği reddedildi.", Toast.LENGTH_SHORT).show()
                        }
                        val dialog: AlertDialog = builder.create()
                        dialog.show()
                    }
                }
            }
            .on("accept") {
                runOnUiThread {
                    run {
                        val users = JSONObject(it[0].toString())
                        val home = users.getString("home")
                        val away = users.getString("away")
                        val intent = Intent(applicationContext, GameActivity::class.java)
                        intent.putExtra("home", home)
                        intent.putExtra("away", away)
                        startActivity(intent)
                        finish()
                    }
                }
            }
            .on("reject") {
                runOnUiThread {
                    run {
                        val username = it[0].toString()
                        val builder = AlertDialog.Builder(this)
                        builder.setTitle("Oyun İsteği Cevabı")
                        builder.setMessage("$username kişisine yaptığınız oyun isteği reddedildi.")
                        builder.setNegativeButton("Tamam") { _, _ -> }
                        val dialog: AlertDialog = builder.create()
                        dialog.show()
                    }
                }
            }
    }
}

Burada yaptığımız işlemler;

  • Login ekranında sharedPreferences üzerine kaydettiğimiz kullanıcı adı (username) değerini ekranda yazdırıyoruz.
  • Server tarafına online_users emit'i yapıyoruz.
  • Server tarafından gelen online_users emit'ini karşılıyoruz. onlineUsers adında bir liste oluşturup elimize ulaşan veriyi bu listeye aktarıyoruz. Socket bağlantısını yapan kullanıcıyı bu listeye dahil etmiyoruz ki kullanıcı kendisini online kullanıcılar listesinde görüp de kendi kendine oyun isteği yollamasın :)
  • Server tarafından gelen request emit'ini karşılıyoruz. Elimize ulaşan id ve kullanıcı adı verisini kullanarak hangi kullanıcıdan oyun isteği geldiğini AlertDialog ile gösteriyoruz. Kabul ve red durumlarına göre de accept ve reject emit'i yapıyoruz.
  • Server tarafından gelen accept emit'ini karşılıyoruz. Elimize ulaşan home ve away verilerini GameActivity'e yolluyoruz.
  • Server tarafından gelen reject emit'ini karşılıyoruz. Elimize ulaşan kullanıcı adı verisini AlertDialog üzerinde gösteriyoruz.


Bir sonraki yazıda görüşmek üzere...