Cara Membuat FPS Dengan Dukungan AI di Unity

First-person shooter (FPS) adalah subgenre game penembak yang pemainnya dikontrol dari sudut pandang orang pertama.

Untuk membuat game FPS di Unity kita memerlukan pengontrol pemain, serangkaian item (dalam hal ini senjata), dan musuh.

Langkah 1: Buat Pengontrol Pemain

Disini kita akan membuat controller yang akan digunakan oleh player kita.

  • Buat Objek Game baru (Objek Game -> Buat Kosong) dan beri nama "Player"
  • Buat Kapsul baru (Objek Game -> Objek 3D -> Kapsul) dan pindahkan ke dalam Objek "Player"
  • Lepaskan komponen Capsule Collider dari Capsule dan ubah posisinya menjadi (0, 1, 0)
  • Pindahkan Kamera Utama ke dalam Objek "Player" dan ubah posisinya menjadi (0, 1.64, 0)
  • Buat skrip baru, beri nama "SC_CharacterController" dan tempel kode di bawah ini di dalamnya:

SC_CharacterController.cs

using UnityEngine;

[RequireComponent(typeof(CharacterController))]

public class SC_CharacterController : MonoBehaviour
{
    public float speed = 7.5f;
    public float jumpSpeed = 8.0f;
    public float gravity = 20.0f;
    public Camera playerCamera;
    public float lookSpeed = 2.0f;
    public float lookXLimit = 45.0f;

    CharacterController characterController;
    Vector3 moveDirection = Vector3.zero;
    Vector2 rotation = Vector2.zero;

    [HideInInspector]
    public bool canMove = true;

    void Start()
    {
        characterController = GetComponent<CharacterController>();
        rotation.y = transform.eulerAngles.y;
    }

    void Update()
    {
        if (characterController.isGrounded)
        {
            // We are grounded, so recalculate move direction based on axes
            Vector3 forward = transform.TransformDirection(Vector3.forward);
            Vector3 right = transform.TransformDirection(Vector3.right);
            float curSpeedX = canMove ? speed * Input.GetAxis("Vertical") : 0;
            float curSpeedY = canMove ? speed * Input.GetAxis("Horizontal") : 0;
            moveDirection = (forward * curSpeedX) + (right * curSpeedY);

            if (Input.GetButton("Jump") && canMove)
            {
                moveDirection.y = jumpSpeed;
            }
        }

        // Apply gravity. Gravity is multiplied by deltaTime twice (once here, and once below
        // when the moveDirection is multiplied by deltaTime). This is because gravity should be applied
        // as an acceleration (ms^-2)
        moveDirection.y -= gravity * Time.deltaTime;

        // Move the controller
        characterController.Move(moveDirection * Time.deltaTime);

        // Player and Camera rotation
        if (canMove)
        {
            rotation.y += Input.GetAxis("Mouse X") * lookSpeed;
            rotation.x += -Input.GetAxis("Mouse Y") * lookSpeed;
            rotation.x = Mathf.Clamp(rotation.x, -lookXLimit, lookXLimit);
            playerCamera.transform.localRotation = Quaternion.Euler(rotation.x, 0, 0);
            transform.eulerAngles = new Vector2(0, rotation.y);
        }
    }
}
  • Lampirkan skrip SC_CharacterController ke Objek "Player" (Anda akan melihat bahwa ia juga menambahkan komponen lain yang disebut Pengontrol Karakter, mengubah nilai tengahnya menjadi (0, 1, 0))
  • Tetapkan Kamera Utama ke variabel Kamera Pemain di SC_CharacterController

Pengontrol Player sekarang siap:

Langkah 2: Buat Sistem Senjata

Sistem senjata pemain akan terdiri dari 3 komponen: Manajer Senjata, skrip Senjata, dan skrip Peluru.

  • Buat skrip baru, beri nama "SC_WeaponManager" dan tempel kode di bawah ini di dalamnya:

SC_WeaponManager.cs

using UnityEngine;

public class SC_WeaponManager : MonoBehaviour
{
    public Camera playerCamera;
    public SC_Weapon primaryWeapon;
    public SC_Weapon secondaryWeapon;

    [HideInInspector]
    public SC_Weapon selectedWeapon;

    // Start is called before the first frame update
    void Start()
    {
        //At the start we enable the primary weapon and disable the secondary
        primaryWeapon.ActivateWeapon(true);
        secondaryWeapon.ActivateWeapon(false);
        selectedWeapon = primaryWeapon;
        primaryWeapon.manager = this;
        secondaryWeapon.manager = this;
    }

    // Update is called once per frame
    void Update()
    {
        //Select secondary weapon when pressing 1
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            primaryWeapon.ActivateWeapon(false);
            secondaryWeapon.ActivateWeapon(true);
            selectedWeapon = secondaryWeapon;
        }

        //Select primary weapon when pressing 2
        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            primaryWeapon.ActivateWeapon(true);
            secondaryWeapon.ActivateWeapon(false);
            selectedWeapon = primaryWeapon;
        }
    }
}
  • Buat skrip baru, beri nama "SC_Weapon" dan tempel kode di bawah ini di dalamnya:

SC_Weapon.cs

using System.Collections;
using UnityEngine;

[RequireComponent(typeof(AudioSource))]

public class SC_Weapon : MonoBehaviour
{
    public bool singleFire = false;
    public float fireRate = 0.1f;
    public GameObject bulletPrefab;
    public Transform firePoint;
    public int bulletsPerMagazine = 30;
    public float timeToReload = 1.5f;
    public float weaponDamage = 15; //How much damage should this weapon deal
    public AudioClip fireAudio;
    public AudioClip reloadAudio;

    [HideInInspector]
    public SC_WeaponManager manager;

    float nextFireTime = 0;
    bool canFire = true;
    int bulletsPerMagazineDefault = 0;
    AudioSource audioSource;

    // Start is called before the first frame update
    void Start()
    {
        bulletsPerMagazineDefault = bulletsPerMagazine;
        audioSource = GetComponent<AudioSource>();
        audioSource.playOnAwake = false;
        //Make sound 3D
        audioSource.spatialBlend = 1f;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButtonDown(0) && singleFire)
        {
            Fire();
        }
        if (Input.GetMouseButton(0) && !singleFire)
        {
            Fire();
        }
        if (Input.GetKeyDown(KeyCode.R) && canFire)
        {
            StartCoroutine(Reload());
        }
    }

    void Fire()
    {
        if (canFire)
        {
            if (Time.time > nextFireTime)
            {
                nextFireTime = Time.time + fireRate;

                if (bulletsPerMagazine > 0)
                {
                    //Point fire point at the current center of Camera
                    Vector3 firePointPointerPosition = manager.playerCamera.transform.position + manager.playerCamera.transform.forward * 100;
                    RaycastHit hit;
                    if (Physics.Raycast(manager.playerCamera.transform.position, manager.playerCamera.transform.forward, out hit, 100))
                    {
                        firePointPointerPosition = hit.point;
                    }
                    firePoint.LookAt(firePointPointerPosition);
                    //Fire
                    GameObject bulletObject = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
                    SC_Bullet bullet = bulletObject.GetComponent<SC_Bullet>();
                    //Set bullet damage according to weapon damage value
                    bullet.SetDamage(weaponDamage);

                    bulletsPerMagazine--;
                    audioSource.clip = fireAudio;
                    audioSource.Play();
                }
                else
                {
                    StartCoroutine(Reload());
                }
            }
        }
    }

    IEnumerator Reload()
    {
        canFire = false;

        audioSource.clip = reloadAudio;
        audioSource.Play();

        yield return new WaitForSeconds(timeToReload);

        bulletsPerMagazine = bulletsPerMagazineDefault;

        canFire = true;
    }

    //Called from SC_WeaponManager
    public void ActivateWeapon(bool activate)
    {
        StopAllCoroutines();
        canFire = true;
        gameObject.SetActive(activate);
    }
}
  • Buat skrip baru, beri nama "SC_Bullet" dan tempel kode di bawah ini di dalamnya:

SC_Bullet.cs

using System.Collections;
using UnityEngine;

public class SC_Bullet : MonoBehaviour
{
    public float bulletSpeed = 345;
    public float hitForce = 50f;
    public float destroyAfter = 3.5f;

    float currentTime = 0;
    Vector3 newPos;
    Vector3 oldPos;
    bool hasHit = false;

    float damagePoints;

    // Start is called before the first frame update
    IEnumerator Start()
    {
        newPos = transform.position;
        oldPos = newPos;

        while (currentTime < destroyAfter && !hasHit)
        {
            Vector3 velocity = transform.forward * bulletSpeed;
            newPos += velocity * Time.deltaTime;
            Vector3 direction = newPos - oldPos;
            float distance = direction.magnitude;
            RaycastHit hit;

            // Check if we hit anything on the way
            if (Physics.Raycast(oldPos, direction, out hit, distance))
            {
                if (hit.rigidbody != null)
                {
                    hit.rigidbody.AddForce(direction * hitForce);

                    IEntity npc = hit.transform.GetComponent<IEntity>();
                    if (npc != null)
                    {
                        //Apply damage to NPC
                        npc.ApplyDamage(damagePoints);
                    }
                }

                newPos = hit.point; //Adjust new position
                StartCoroutine(DestroyBullet());
            }

            currentTime += Time.deltaTime;
            yield return new WaitForFixedUpdate();

            transform.position = newPos;
            oldPos = newPos;
        }

        if (!hasHit)
        {
            StartCoroutine(DestroyBullet());
        }
    }

    IEnumerator DestroyBullet()
    {
        hasHit = true;
        yield return new WaitForSeconds(0.5f);
        Destroy(gameObject);
    }

    //Set how much damage this bullet will deal
    public void SetDamage(float points)
    {
        damagePoints = points;
    }
}

Sekarang, Anda akan melihat bahwa skrip SC_Bullet memiliki beberapa kesalahan. Itu karena ada satu hal terakhir yang harus kita lakukan, yaitu mendefinisikan antarmuka IEntity.

Antarmuka di C# berguna ketika Anda perlu memastikan bahwa skrip yang menggunakannya, telah menerapkan metode tertentu.

Antarmuka IEntity akan memiliki satu metode yaitu ApplyDamage, yang nantinya akan digunakan untuk memberikan kerusakan pada musuh dan pemain kita.

  • Buat skrip baru, beri nama "SC_InterfaceManager" dan tempel kode di bawah ini di dalamnya:

SC_InterfaceManager.cs

//Entity interafce
interface IEntity
{ 
    void ApplyDamage(float points);
}

Menyiapkan Manajer Senjata

Manajer senjata adalah sebuah Objek yang akan berada di bawah Objek Kamera Utama dan akan berisi semua senjata.

  • Buat GameObject baru dan beri nama "WeaponManager"
  • Pindahkan WeaponManager ke dalam Kamera Utama Pemain dan ubah posisinya menjadi (0, 0, 0)
  • Lampirkan skrip SC_WeaponManager ke "WeaponManager"
  • Tetapkan Kamera Utama ke variabel Kamera Pemain di SC_WeaponManager

Menyiapkan Senapan

  • Seret dan lepas model senjata Anda ke dalam adegan (atau cukup buat Kubus dan rentangkan jika Anda belum memiliki modelnya).
  • Skalakan model sehingga ukurannya relatif terhadap Kapsul Pemain

Dalam kasus saya, saya akan menggunakan model Senapan yang dibuat khusus (BERGARA BA13):

BERGARA BA13

  • Buat GameObject baru dan beri nama "Rifle" lalu pindahkan model senapan ke dalamnya
  • Pindahkan Objek "Rifle" ke dalam Objek "WeaponManager" dan letakkan di depan Kamera seperti ini:

Perbaiki Masalah Kliping Kamera di Unity.

Untuk memperbaiki objek yang terpotong, cukup ubah bidang dekat kamera menjadi lebih kecil (dalam kasus saya, saya menyetelnya ke 0,15):

BERGARA BA13

Jauh lebih baik.

  • Lampirkan skrip SC_Weapon ke Objek Senapan (Anda akan melihat bahwa skrip ini juga menambahkan komponen Sumber Audio, ini diperlukan untuk menembakkan api dan memuat ulang audio).

Seperti yang Anda lihat, SC_Weapon memiliki 4 variabel untuk ditetapkan. Anda dapat langsung menetapkan variabel audio Api dan Muat Ulang audio jika Anda memiliki Klip Audio yang sesuai di proyek Anda.

Variabel Bullet Prefab akan dijelaskan nanti di tutorial ini.

Untuk saat ini, kami hanya akan menetapkan variabel Titik api:

  • Buat GameObject baru, ganti namanya menjadi "FirePoint" dan pindahkan ke dalam Rifle Object. Tempatkan tepat di depan laras atau sedikit ke dalam, seperti ini:

  • Tetapkan FirePoint Transform ke variabel Fire point di SC_Weapon
  • Tetapkan Senapan ke variabel Senjata Sekunder dalam skrip SC_WeaponManager

Menyiapkan Senapan Mesin Ringan

  • Gandakan Objek Senapan dan ganti namanya menjadi Submachinegun
  • Ganti model senjata di dalamnya dengan model lain (Dalam kasus saya, saya akan menggunakan model TAVOR X95 yang dibuat khusus)

TAVOR X95

  • Pindahkan transformasi Fire Point hingga sesuai dengan model baru

Pengaturan objek Weapon Fire Point di Unity.

  • Tetapkan Submachinegun ke variabel Senjata Utama dalam skrip SC_WeaponManager

Menyiapkan Prefab Peluru

Cetakan peluru akan muncul sesuai dengan laju tembakan Senjata dan akan menggunakan Raycast untuk mendeteksi apakah peluru tersebut mengenai sesuatu dan menimbulkan kerusakan.

  • Buat GameObject baru dan beri nama "Bullet"
  • Tambahkan komponen Trail Renderer ke dalamnya dan ubah variabel Time menjadi 0.1.
  • Atur kurva Lebar ke nilai yang lebih rendah (mis. Mulai 0,1 berakhir 0), untuk menambahkan jejak yang terlihat lancip
  • Buat Material baru dan beri nama bullet_trail_material dan ubah Shadernya menjadi Particles/Additive
  • Tetapkan materi yang baru dibuat ke Trail Renderer
  • Ubah Warna Trail Renderer menjadi sesuatu yang berbeda (misal: Mulai: Oranye Terang Akhir: Oranye Gelap)

  • Simpan Objek Bullet ke Prefab dan hapus dari Scene.
  • Tetapkan Prefab yang baru dibuat (drag & drop dari tampilan Proyek) ke variabel Prefab Senapan dan Peluru Submachinegun

Senapan mesin ringan:

Senapan:

Senjatanya sekarang sudah siap.

Langkah 3: Buat AI Musuh

Musuhnya berupa Kubus sederhana yang mengikuti Pemain dan menyerang setelah mereka cukup dekat. Mereka akan menyerang dalam gelombang, dan setiap gelombang mempunyai lebih banyak musuh yang harus dilenyapkan.

Menyiapkan AI Musuh

Di bawah ini saya telah membuat 2 variasi Kubus (Yang Kiri untuk instance hidup dan yang Kanan akan muncul setelah musuh terbunuh):

  • Tambahkan komponen Rigidbody ke instance mati dan hidup
  • Simpan Instance Mati ke Prefab dan hapus dari Scene.

Sekarang, instance hidup memerlukan beberapa komponen lagi untuk dapat menavigasi level permainan dan menimbulkan kerusakan pada Pemain.

  • Buat skrip baru dan beri nama "SC_NPCEnemy" lalu tempel kode di bawah ini di dalamnya:

SC_NPCEnemy.cs

using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]

public class SC_NPCEnemy : MonoBehaviour, IEntity
{
    public float attackDistance = 3f;
    public float movementSpeed = 4f;
    public float npcHP = 100;
    //How much damage will npc deal to the player
    public float npcDamage = 5;
    public float attackRate = 0.5f;
    public Transform firePoint;
    public GameObject npcDeadPrefab;

    [HideInInspector]
    public Transform playerTransform;
    [HideInInspector]
    public SC_EnemySpawner es;
    NavMeshAgent agent;
    float nextAttackTime = 0;

    // Start is called before the first frame update
    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        agent.stoppingDistance = attackDistance;
        agent.speed = movementSpeed;

        //Set Rigidbody to Kinematic to prevent hit register bug
        if (GetComponent<Rigidbody>())
        {
            GetComponent<Rigidbody>().isKinematic = true;
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (agent.remainingDistance - attackDistance < 0.01f)
        {
            if(Time.time > nextAttackTime)
            {
                nextAttackTime = Time.time + attackRate;

                //Attack
                RaycastHit hit;
                if(Physics.Raycast(firePoint.position, firePoint.forward, out hit, attackDistance))
                {
                    if (hit.transform.CompareTag("Player"))
                    {
                        Debug.DrawLine(firePoint.position, firePoint.position + firePoint.forward * attackDistance, Color.cyan);

                        IEntity player = hit.transform.GetComponent<IEntity>();
                        player.ApplyDamage(npcDamage);
                    }
                }
            }
        }
        //Move towardst he player
        agent.destination = playerTransform.position;
        //Always look at player
        transform.LookAt(new Vector3(playerTransform.transform.position.x, transform.position.y, playerTransform.position.z));
    }

    public void ApplyDamage(float points)
    {
        npcHP -= points;
        if(npcHP <= 0)
        {
            //Destroy the NPC
            GameObject npcDead = Instantiate(npcDeadPrefab, transform.position, transform.rotation);
            //Slightly bounce the npc dead prefab up
            npcDead.GetComponent<Rigidbody>().velocity = (-(playerTransform.position - transform.position).normalized * 8) + new Vector3(0, 5, 0);
            Destroy(npcDead, 10);
            es.EnemyEliminated(this);
            Destroy(gameObject);
        }
    }
}
  • Buat skrip baru, beri nama "SC_EnemySpawner" lalu tempel kode di bawah ini di dalamnya:

SC_EnemySpawner.cs

using UnityEngine;
using UnityEngine.SceneManagement;

public class SC_EnemySpawner : MonoBehaviour
{
    public GameObject enemyPrefab;
    public SC_DamageReceiver player;
    public Texture crosshairTexture;
    public float spawnInterval = 2; //Spawn new enemy each n seconds
    public int enemiesPerWave = 5; //How many enemies per wave
    public Transform[] spawnPoints;

    float nextSpawnTime = 0;
    int waveNumber = 1;
    bool waitingForWave = true;
    float newWaveTimer = 0;
    int enemiesToEliminate;
    //How many enemies we already eliminated in the current wave
    int enemiesEliminated = 0;
    int totalEnemiesSpawned = 0;

    // Start is called before the first frame update
    void Start()
    {
        //Lock cursor
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        //Wait 10 seconds for new wave to start
        newWaveTimer = 10;
        waitingForWave = true;
    }

    // Update is called once per frame
    void Update()
    {
        if (waitingForWave)
        {
            if(newWaveTimer >= 0)
            {
                newWaveTimer -= Time.deltaTime;
            }
            else
            {
                //Initialize new wave
                enemiesToEliminate = waveNumber * enemiesPerWave;
                enemiesEliminated = 0;
                totalEnemiesSpawned = 0;
                waitingForWave = false;
            }
        }
        else
        {
            if(Time.time > nextSpawnTime)
            {
                nextSpawnTime = Time.time + spawnInterval;

                //Spawn enemy 
                if(totalEnemiesSpawned < enemiesToEliminate)
                {
                    Transform randomPoint = spawnPoints[Random.Range(0, spawnPoints.Length - 1)];

                    GameObject enemy = Instantiate(enemyPrefab, randomPoint.position, Quaternion.identity);
                    SC_NPCEnemy npc = enemy.GetComponent<SC_NPCEnemy>();
                    npc.playerTransform = player.transform;
                    npc.es = this;
                    totalEnemiesSpawned++;
                }
            }
        }

        if (player.playerHP <= 0)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Scene scene = SceneManager.GetActiveScene();
                SceneManager.LoadScene(scene.name);
            }
        }
    }

    void OnGUI()
    {
        GUI.Box(new Rect(10, Screen.height - 35, 100, 25), ((int)player.playerHP).ToString() + " HP");
        GUI.Box(new Rect(Screen.width / 2 - 35, Screen.height - 35, 70, 25), player.weaponManager.selectedWeapon.bulletsPerMagazine.ToString());

        if(player.playerHP <= 0)
        {
            GUI.Box(new Rect(Screen.width / 2 - 85, Screen.height / 2 - 20, 170, 40), "Game Over\n(Press 'Space' to Restart)");
        }
        else
        {
            GUI.DrawTexture(new Rect(Screen.width / 2 - 3, Screen.height / 2 - 3, 6, 6), crosshairTexture);
        }

        GUI.Box(new Rect(Screen.width / 2 - 50, 10, 100, 25), (enemiesToEliminate - enemiesEliminated).ToString());

        if (waitingForWave)
        {
            GUI.Box(new Rect(Screen.width / 2 - 125, Screen.height / 4 - 12, 250, 25), "Waiting for Wave " + waveNumber.ToString() + " (" + ((int)newWaveTimer).ToString() + " seconds left...)");
        }
    }

    public void EnemyEliminated(SC_NPCEnemy enemy)
    {
        enemiesEliminated++;

        if(enemiesToEliminate - enemiesEliminated <= 0)
        {
            //Start next wave
            newWaveTimer = 10;
            waitingForWave = true;
            waveNumber++;
        }
    }
}
  • Buat skrip baru, beri nama "SC_DamageReceiver" lalu tempel kode di bawah ini di dalamnya:

SC_DamageReceiver.cs

using UnityEngine;

public class SC_DamageReceiver : MonoBehaviour, IEntity
{
    //This script will keep track of player HP
    public float playerHP = 100;
    public SC_CharacterController playerController;
    public SC_WeaponManager weaponManager;

    public void ApplyDamage(float points)
    {
        playerHP -= points;

        if(playerHP <= 0)
        {
            //Player is dead
            playerController.canMove = false;
            playerHP = 0;
        }
    }
}
  • Lampirkan skrip SC_NPCEnemy ke instance musuh yang aktif (Anda akan melihatnya menambahkan komponen lain yang disebut Agen NavMesh, yang diperlukan untuk menavigasi NavMesh)
  • Tetapkan prefab instance mati yang baru dibuat ke variabel NPC Dead Prefab
  • Untuk Fire Point, buat GameObject baru, pindahkan ke dalam instance musuh yang masih hidup dan letakkan sedikit di depan instance tersebut, lalu tetapkan ke variabel Fire Point:

  • Terakhir, Simpan instance yang masih hidup ke Prefab dan hapus dari Scene.

Menyiapkan Spawner Musuh

Sekarang mari beralih ke SC_EnemySpawner. Skrip ini akan memunculkan musuh dalam gelombang dan juga akan menampilkan beberapa informasi UI di layar, seperti HP Pemain, Amunisi saat ini, berapa banyak Musuh yang tersisa dalam gelombang saat ini, dll.

  • Buat GameObject baru dan beri nama "_EnemySpawner"
  • Lampirkan skrip SC_EnemySpawner ke sana
  • Tetapkan AI musuh yang baru dibuat ke variabel Prefab Musuh
  • Tetapkan tekstur di bawah ini ke variabel Crosshair Texture

  • Buat beberapa GameObjects baru dan letakkan di sekitar Scene lalu tetapkan mereka ke array Spawn Points

Anda akan melihat bahwa ada satu variabel terakhir yang tersisa untuk ditetapkan yaitu variabel Pemain.

  • Lampirkan skrip SC_DamageReceiver ke instance Player
  • Ubah tag instance Player menjadi "Player"
  • Tetapkan variabel Pengontrol Pemain dan Manajer Senjata di SC_DamageReceiver

  • Tetapkan instance Player ke variabel Player di SC_EnemySpawner

Dan terakhir, kita harus memasang NavMesh di adegan kita sehingga AI musuh dapat bernavigasi.

Juga, jangan lupa untuk menandai setiap Objek statis di Scene sebagai Navigasi Statis sebelum membuat NavMesh:

  • Masuk ke jendela NavMesh (Window -> AI -> Navigation), klik pada tab Bake lalu klik tombol Bake. Setelah NavMesh dipanggang, tampilannya akan seperti ini:

Sekarang saatnya menekan Mainkan dan mengujinya:

Sharp Coder Pemutar video

Semuanya berfungsi seperti yang diharapkan!