Chủ Nhật, 28 tháng 2, 2021

Tích vô hướng (Dot Product) là gì? Làm sao để ứng dụng trong lập trình game?

 Hello mụi người, cũng hơn 40 ngày nghỉ tết thì cũng bắt đầu trở lại trường rồi, tranh thủ rảnh rỗi mấy ngày cuối giới thiệu cho mấy bạn thấy được sức mạnh của Tích Vô Hướng chứ thấy mấy bạn if else hơi cực, chưa kể if else mà sai logic thì lại hư các game.

 Tích vô hướng cũng chả xa lạ gì đối với các bạn vì nó đã được dạy trong chương trình phổ thông, cụ thể là sách toán lớp 10. Cũng nhớ hồi đó lười học mà tơi chương Vector chả hiểu sao ham học nên giờ cũng còn nhớ sơ sơ :))

Đây là một bài rất nặng lý thuyết, các bạn nhớ trang bị kĩ kiến thức toán khi đọc nhé

Tích vô hướng là cái vẹo gì?

Tích vô hướng (tên tiếng Anh: dot product hoặc scalar product) là một phép toán đại số lấy hai chuỗi số có độ dài bằng nhau (thường là các vectơ tọa độ) và cho kết quả là một số. Trong hình học Euclid, tích vô hướng với tọa độ Descartes của hai vectơ thường được sử dụng. Tích vô hướng cũng thường được gọi là tích trong Euclid dù nó không phải là loại tích trong duy nhất có thể được định nghĩa trong không gian Euclid (xem thêm tại Không gian tích trong).  (Theo wikipedia)

Đọc xong cái định nghĩa tắt mẹ cái blog đi ngủ cho khỏe, hù chút thôi chứ mình cũng có hiểu mẹ gì đâu, tuy nhiên phát biểu đại số của nó sẽ dễ hiểu hơn thế này

ab=i=1naibi=a1b1+a2b2+...+anbn

Ví dụ:

Đây là phát biểu kiểu công thức xưa mình hay dùng. Tuy nhiên đối với mình tích vô hướng là độ lớn của vector VÀ độ lớn của phép chiếu của vector còn lại lên lên nó

được phát biểu công thức dưới dạng

(bản chất của Tích Vô Hướng)


Ứng dụng trong Đường Tròn Lượng Giác

Và đặc biệt hơn Chúng ta thường áp dụng Dot Product trong đường tròn lượng giác hay Unit Circle (Có bán kính = 1) hơn thay vì các trường hợp tổng quát



Khi áp dụng trong Unit Circle, ta sẽ có hình chiếu của vector lên vector còn lại cũng chính là tích vô hướng của 2 vector (bạn có thể chứng minh từ công thức trên)

Trong gif trên, đường màu tím chiếu vuông góc từ vector quay chính là tích vô hướng của 2 vector đó

Ứng dụng trong lập trình game

Trong unity đã implement công thức tích vô hướng, chúng ta chỉ việc lấy ra xài thôi. Đó chính là Vector2.Dot or Vector3.Dot

Sau đây mình sẽ liệt kê 2 cách mình hay dùng khi ứng dụng Dot, đây chỉ là ứng dụng rất rất nhỏ của Dot Product

1.Xác định vị trí vật thể

Các bạn thử suy nghĩ, có 2 object như hình sau, với player là khối chữ nhất màu xanh và enemy là hình tròn màu đỏ. Vậy làm sao để kiểm tra khi nào enemy nằm ở: trước, sau, trái, phải của Player.



Mình sẽ chắc chắn trong đầu các bạn sẽ là 1 loạt các dòng if else dài ngoằn chỉ để check. Player nằm vậy còn đơn giản nhé, chứ nó mà xoay ngang, xoay dọc thì bạn if else hồi loạn xì ngầu lên á :))

Dot Product hỗ trợ ta làm việc này cực tốt. Quay lại gif trên, để ý vector hướng lên trên là vector hướng của player, và vector quay là vector từ player tới enemy. Khi đó ta có Dot > 0 => nó nằm ở phía trước của player và ngược lại Dot < 0 ta có nó nằm phía sau player.

Tiếp tục check trái phải. Để ý khi chiếu vector xoay lên trục hoành (trục Cos) thì khi Dot >0 thì enemy nằm bên phải và khi Dot < 0 thì enemy nằm bên trái. Very easy.

[SerializeFieldTransform enemy = default;
 
private void Update()
{
    //DOT PRODUCT
    Vector2 playerToEnemy = enemy.position - this.transform.position;
    float dotProduct;
 
    //FRONT , BEHIND
    dotProduct = Vector2.Dot(transform.up, playerToEnemy);
    if (dotProduct > 0Debug.Log("Front");
    else if (dotProduct < 0Debug.Log("Behind");
    else Debug.Log("Middle");
 
    //LEFT , RIGHT
    dotProduct = Vector2.Dot(transform.right, playerToEnemy);
    if (dotProduct > 0Debug.Log("Right");
    else if (dotProduct < 0Debug.Log("Left");
    else Debug.Log("Middle");
}

2.Điều chỉnh âm thanh khi va chạm

Vấn đề là khi player va chạm với một vật thể nào đó, để chân thật nhất chúng ta phải tính volume sao cho to nhỏ hợp lý với lực tác động.





Trong hình trên, hình tròn là player, di chuyển với v = 10m/s hướng xuống đất.


Nhiều bạn nhanh lẹ sẽ suy nghĩ rằng. Đặt volume = độ lớn vận tốc. Ok tạm chấp nhận được, nhưng hay xét đến trường hợp dưới đây







Trong TH2 ta thấy quả bóng gần như là chạm mặt đất nhưng vẫn di chuyển với v = 10 m/s. => Khi chạm đất volume nó sẽ ngang quả bóng thả thẳng từ trên cao xuống => sai logic

Dot Product sẽ hỗ trợ ta giải quyết chuyện này nhanh gọn, xem hình dưới đây (Lưu ý: vector hướng mình vẽ là vector vận tốc (v) đấy nhé)








Chỉ cần lấy Dot Product giữa Vector vận tốc và vector hướng lên (Vector2.Up) là xong => Dot Product = khoảng cách màu đỏ như hình vẽ (do Vector2.Up có độ lớn = 1)


Đây là cách mà mình Implement

[SerializeFieldAudioSource _audio;
[SerializeFieldRigidbody2D _rig;
 
private void OnValidate()
{
    _audio = GetComponent<AudioSource>();
    _rig = GetComponent<Rigidbody2D>();
}
 
 
private void OnCollisionEnter2D(Collision2D collision)
{
    _audio.volume = Mathf.Abs(Vector2.Dot(transform.up, _rig.velocity))/ 10f;
    _audio.Play();
}


Tổng kết

Sức mạnh của Dot Product vẫn còn rất nhiều, đó là mình chỉ demo sơ sơ cho bạn xem thôi đó. Đại đa số các game developer tận dụng sức mạnh của Dot Product rất triệt để. Oke, bài này hơi nặng lý thuyết nhưng mong các bạn cố hiểu và nếu có gì thắc mắc nhớ để lại comment để mình giải đáp nhé.


Share:

Thứ Bảy, 20 tháng 2, 2021

ScriptableObject không chỉ để lưu data (P1)

Intro  

ScriptableObject là một class kế thừa trực tiếp từ class Object, là nơi chứa dữ liệu của game chúng ta. Trong phạm vi bài này mình sẽ không đi sâu về lý thuyết cơ bản của ScriptableObject, mà là cách vận dụng nó sao cho hiệu quả và dùng hết khả năng của ScriptableObject vào game architecture

Để đọc được bài này mọi người nên tìm hiểu ScriptableObject trong Unity và cách dùng nó cơ bản.Học tại đây

 Chúng ta có thể hiểu trách nhiệm của ScriptableObject là lưu data ,nhưng theo mình thì cách dùng ScriptableObject không chỉ đơn giản là như vậy mình thấy mọi người thường đặt nghi vấn: "Tại sao không khai báo biến thẳng vào class mà chúng ta nên xài ScritptableObject?". Như mọi người đều biết trong team chúng ta luôn có Game Designer và làm sao để GD có thể tham gia vào quá trình config game của chúng ta như chỉnh sửa thông số máu của nhân vật qua mỗi level, damage của boss, exp lên level,.. Và chúng ta có ScripTableObject để thực hiện điều này. 

Làm việc hiệu quả hơn với Game Designer

Tiếp tục vấn đề trên, bạn sẽ tiếp tục đặt ra câu hỏi: "Thằng GD có thể config trực tiếp trong prefab mà tại sao là dùng ScriptableObject?". Khi một team làm việc với designer, designer muốn chỉnh sửa thông số của enemy, player hay map thường sẽ phải vào file prefab và chỉnh => mỗi object lại phải tìm 1 file trong prefab. Trong khi đó 1 prefab có thể đặt " chằn chịt " các con số public dẫn đến rổi rắm và hiệu suất công việc giảm mạnh.

Đó là lý do mình khi làm việc với Game Deisnger nên dùng ScriptableObject nhé. Thằng nào còn thắc mắc đấm vỡ mồm. 

Reduce Project's memory

Trong docs của Unity đã rất rõ ràng điều này, nhưng mình sẽ lấy ví dụ cho mấy bạn hiểu. Nôm na là một Enemy Object có 10 thông số float trong nó và trong game sẽ có rất nhiều loại Enemy giống nó, giả sử đó là 5 enemy => game sẽ cần bộ nhớ để lưu trữ 5*10 float field đó. Thay vào đó ta sẽ dùng ScriptTableObject là 1 file config duy nhất, định nghĩa các field có thể có và tất cả enemy sẽ tham chiều tới file Config đó và lấy ra các thuộc tinh.

Điều này rất hữu dụng khi dùng một số dữ liệu không thay đổi trong game.

Giảm dependency

Theo dõi đoạn code sau:

Ta có Player với chỉ số HP = 100 khi bắt đầu game

public class Player : MonoBehaviour
{
    public float HP;
    private void Start()
    {
        HP = 100;
    }
}

PlayerUI sẽ display chỉ số HP đó lên màn hình cho người chơi dưới dạng thanh cuộn ngang



Để thực hiện điều đó ta implement PlayerUI như sau:
public class PlayerUI : MonoBehaviour
{
    public Player player;
 
    private void Update()
    {
        transform.localScale = new Vector3((player.HP / 100),transform.localScale.y,transform.localScale.z);
    }
}

Lưu ý: đây là cách làm không tốt, mình chỉ demo nhanh cho các bạn xem và cách làm tốt hơn là dụng Event ở một bài của mình Tại đây , đó cũng chính là giải pháp tối ưu  cho bài toán này, tuy nhiên trong khuôn khổ bài này mình sẽ chỉ nói về ScriptableObject

Ví dụ: Khi một player bị tấn công bơi enemy khiến player bị giảm HP -> HP UI sẽ tham chiếu đến HP Player và display nó khi có sự thay đổi.Tiếp tục, khi máu của player dưới 20% thì Audio sẽ thay đổ nhịp độ -> tiếp tục khiến Audio phải tham chiều đến Player tạo ra nhiều sự phụ thuộc cứng.Tồi tệ hơn khi có nhiều EnemyAI tham chiều tới Player để xác định và tấn công 

Ở ví dụ trên ta thấy ở mỗi Component ta đều tham chiếu đến Player dẫn đến 1 só trường hợp sau

  • Disable Player, hư cả game
  • Thay đổi nhẹ cấu hình trong Player, hư cả game
  • Chúng ta tham chiếu đến Player nhưng không dùng hết sức mạnh của Player Component
Những điều này khiến cho mã nguồn bị kết dính nghiệm trọng

Vậy giải pháp là gì?

Như những gì chúng ta đã phân tích thì ScriptableObject là một object lưu trữ dữ liệu độc lập với code base. Vậy thì chỉ cần các object khác như UI , Audio, EnemyAI khác tham chiếu đến ScriptableObject này là được chả việc gì chúng ta phải reference cứng Player cả, ngoài ra ScriptableObject một khi đã khởi tạo thì luôn tồn tại => không sợ break game.

Vậy chúng ta sẽ sửa code bên trên 1 tí lại như sau

[CreateAssetMenu(fileName ="PlayerConfig" , menuName = "GameConfiguration/PlayerConfig",order =1)]
public class PlayerConfig : ScriptableObject
{
    public float HP;
}


Ta sẽ ném thuộc tính HP vào ScriptableObject và tạo một file Config riêng cho nó

Tại những nơi chúng ta cần tham chiếu đến HP, khái báo public PlayerConfig ví dụ như PlayerUI

public class PlayerUI : MonoBehaviour
{
    public PlayerConfig player;
 
    private void Update()
    {
        transform.localScale = new Vector3((player.HP / 100),transform.localScale.y,transform.localScale.z);
    }
}


Như vậy, code của chúng ta đã hết bị tightly coupling và chuyển về loose coupling 



Tổng kết

Trong phần này mình đã giới thiệu một vài sức mạnh của ScriptableObject cho các bạn, phần sau sẽ năng hơn về logic nhưng lại cực kỳ hữu dụng đối với các team muốn tận dụng sức mạnh của Game Designer. Thanks for reading.

Share:

Thứ Hai, 15 tháng 2, 2021

Delegate, Event, là gì? Ứng dụng trong lập trình game như thế nào?

 

Trước khi đi vào các khái niệm về delegate và event. Ta cần phải hiểu một loại design pattern ứng dụng mạnh mẽ delegate và event. Đó là Observer Design Pattern.

Mình sẽ nói chi tiết về Observer Design Pattern về một bài riêng, trong phạm vị bài này mình sẽ nói sơ lược qua nó và cách ứng dụng delegate và event để tạo ra các Observer như thế nào.

Vấn đề đặt ra

Trong 1 game gồm player và các enemy. 
Khi player Kill 1 enemy => phía UI sẽ tăng lên 1 đơn vị, mình thấy đại đa số các bạn sẽ GetComponent<X> (với X là component sẽ render ra UI) trực tiếp trong method Kill():

   void Kill()
    {
       ScoreDisplay score = GameObject.Find("ScoreText").GetComponent<ScoreDisplay>();
       score.Update();
    }


Tiếp tục, Khi player dùng 1 vật phẩm nào đó, sẽ gọi đến UI chứa vật phẩm đó để giảm số lượng nó đi 1 đơn vị


    void UseItem()
    {
       Item item = GameObject.Find("ScoreText").GetComponent<Item>();
       item.Update();
    }

Tượng tượng khi dùng item thì mình sẽ play một audio nào đó => lại tiếp tục Getcomponent đén audio và play.

Sau một hồi đặt ra ví dụ, chúng ta thấy càng scale thì code của chúng ta sẽ phát sinh một số thứ sau đây:

  • Spaghetti code (code rối rắm)
  • Tạo ra các depedency cứng
  • Highly coupled code
  • Mất 1 phần tử nào đó => break cả game
  • 1 thay đổi nhỏ => break cả game

Observer Design Pattern sinh ra để giải quyết vấn đề này

Observer Pattern là gì?

Một object (người thông báo) sẽ gửi broadcast messenger đến một hoặc nhiều object khác được lựa chọn (subscribe) và phải "do something" dựa trên những thứ nó đã đăng kí.

Giải thích ý tưởng trên là thế này: chúng ta sẽ tạo 1 biến static là người thông báo và người thông báo này có trách nhiệm sẽ gửi messenger cho toàn bộ những người lắng nghe nó, và những người lắng nghe nó sẽ phải thực hiện 1 hành động nào đó khi có messenger gửi tới. Chúng ta có delegate để thực hiện điều này.


  

Lợi ích của Observer Pattern

  • Dễ dàng giao tiếp giữa các object
  • Dễ dàng chia sẽ thông tin
  • Phân tách object và code dễ dàng, tránh bị couple
  • Loại bỏ các dependency không mong muốn
Như vậy sơ lược của Observer Pattern là như thế, vậy các thực hiện nó như thế nào, ta phải sẽ bắt đầu tìm hiểu về Delegate và Event.

Delegates

Nếu bạn đã học qua c++ thì chả lạ gì khái niệm con trỏ hàm, delegates chính là khái niệm tương tự trong c#. Các biến tạo ra từ delegate có thể tham chiếu đến các phương thức cùng kiểu với delegate, ngoài ra có thể truyền vào function như một callback

//Khai báo định nghĩa các delegate
public delegate void VoidDelegate();
public delegate int IntDelegate();
public delegate void DelegateParameter(int a, int b);
static void PrintInt() => Console.WriteLine("Int");
 
static int ReturnInt()
{
    return 5;
}
 
static void PrintDoubleInt(int a, int b) => Console.WriteLine("{0} - {1}", a, b);
 
static void Main(string[] args)
{
    //Tạo ra instance của delegate
    VoidDelegate myDelegate = PrintInt;
    DelegateParameter delegateParameter = PrintDoubleInt;
    IntDelegate intDelegate = ReturnInt;
    intDelegate = PrintInt; //Error
 
}

Xem đoạn code trên, để gán bằng một delegate thì method phải cùng kiểu với delegate đó

Và để thực thi delegate ta có method Invoke.

Invoke sẽ thực thi tất cả Subscriber (định nghĩa ở bên dưới) có trong delegate đó

Console.WriteLine(intDelegate?.Invoke()); // Output: 5
myDelegate?.Invoke(); // Ouput: Int

Dấu ? trước delegate là kiểm tra xem nó có phải null không, nếu nó là null thì không invoke. Nếu bạn bỏ qua nó thì nó sẽ throw exception.

Nhiêu đây vẫn chưa thể hiện sức mạnh sủa delegates, sức mạnh của delegate nằm ở chỗ khả năng multiple functions assigned, có thể += nhiều function và invoke cùng lúc.

+= : tăng thêm 1 subscribe cho delegate đó

-= : giảm đi 1 subscribe cho delegate đó

các đối tượng subscribe phải cùng kiểu định nghĩa với delegate

Ta có ví dụ sau:

//Khai báo định nghĩa các delegate
public delegate void VoidDelegate();
static void PrintInt() => Console.WriteLine("Int");
static void PrintString() => Console.WriteLine("String");
static void PrintTanDepTrai() => Console.WriteLine("Tan dep trai vai l*n");
 
static void Main(string[] args)
{
    //Tạo ra instance của delegate
    VoidDelegate myDelegate = null;
    myDelegate += PrintInt; // Add subscriber
    myDelegate += PrintString; 
    myDelegate += PrintTanDepTrai; 
    myDelegate?.Invoke();  //Output là cả 3 function trên
 
    Console.WriteLine("_________");
 
    myDelegate -= PrintInt;
    myDelegate?.Invoke();  // Output mất đi PrintInt
}




Event

Sau khi đã biết delegate, event là một khái niệm khá dễ hiểu, mình sẽ tóm lược như sau:
  • Về bản chất Event là một delegate
  • Khi khởi tạo 1 event, thực chất chỉ là khởi tạo một instance delegate
  • Mọi thao tác đều giống delegate
  • Ngăn chặn overwriten (gán bằng) một function
  • Ngăn chặn public Invoking, tức là bên ngoài class không thể gọi ?.Invoke
public class Testing
{
    public delegate void DelegateTemplate();
    public static event DelegateTemplate example;
 
    void Test()
    {
        example?.Invoke();  
    }
 
}

Ta có class Testing chứa event example (là 1 instance của delegate) và có thể gọi invoke trong nội bộ của class đó

Ta có đoạn code mẫu duối đây
class Program
{
    static void PrintInt() => Console.WriteLine("Int");
    static void PrintString() => Console.WriteLine("String");
    static void PrintTanDepTrai() => Console.WriteLine("Tan dep trai vai l*n");
    
    static void Main(string[] args)
    {
        Testing.example += PrintInt;
        Testing.example += PrintString;
        Testing.example += PrintTanDepTrai;
 
        Testing.example?.Invoke(); // Error - không thể gọi invoke ở đây
 
    }
 
}
Tại class Program, gọi đến testing và subscribe cho example các method ở ví dụ trước, tuy nhiên ta không thể gọi ?.Invoke tại class này

Vậy thì ứng dụng trong lập trình game như thế nào?

Trong lập trình game, cụ thể là Unity - C#, mình sẽ ít dụng delegate và event lý do là nó khai báo nhiều :)), thay vào đó mình sẽ dùng Action Func

Đừng sợ, nó chỉ là đơn giản hóa của delegate mà thôi.

Thay vì bạn phải khai báo một Delegate và tạo ra một instance của nó với 2 dòng code, thì Action làm việc đó chỉ với 1 dòng và tương tự với Func

public delegate void VoidDelegate(int a);
public static VoidDelegate voidDelegate;
 
//Same
 
public static Action<int> voidDelegate;

or

public delegate string MyDelegate(int);
public static MyDelegate myDelegate;
 
//same
 
Func<intstring> myDelegate;

Action: là một delegate void với các parameter 

Func : là một delegate với kiểu dữ liệu giống với parameter cuối cùng

Lưu ý: Do Func có giá trị trả về => giá trị của nó sẽ là method cuối cùng được invoke

Oke, xong cơ bản về Func và Action. Như những gì mình đã nêu ở mục đặt vấn đề ta thường dùng để giao tiếp giữa gameplay và UI ví dụ như tăng điểm số, trừ máu, mana point, ...

Mình sẽ demo nhỏ một ví dụ ứng dụng vào game như sau.

Trong class GameManager ta có:

public static Action<int> e_SetScore;
 
[Header("Game state")]
public bool isPause;
public int gameScore
{
    get => _gameScore;
    set
    {
        _gameScore = value;
        e_SetScore?.Invoke(_gameScore);
    }
}

Ta có một Action với đầu vào là một số nguyên dùng để Set Score cho UI khi player ăn điểm

Khi ăn điểm thì property gameScore++ , lúc này thực hiện e_SetScore?.Invoke()

Và nơi ta đăng kí observer cho event này sẽ là UIManager

Tại UIManager ta có:

public void SetScore(int score)
{
    text_Score.text = score.ToString();
}
private void OnEnable()
{
    GameManager.e_SetScore += SetScore;
    SetScore(0);
}
 
private void OnDisable()
{
    GameManager.e_SetScore -= SetScore;
}

Oke, thay vì cứ đặt trong update liên tục ta sẽ dùng cách này giúp ta tối ưu game performance hơn, đặc biệt khi UnActive UIManager cũng sẽ chẳng làm ảnh hướng đến Gameplay

Đây chỉ là một phần của một ứng dụng rất rất nhỏ của event và delegate trong c#, còn rất nhiều, mình thì còn hay dùng khi truyền callback cho một function nữa nhưng mà thêm vào thì bài này lại dài quá, các bạn từ tìm hiểu nhé :v

Tổng kết

Vậy ứng dụng cực kì mạnh mẽ của delegate và event giúp chúng ta giải quyết được rất nhiều vấn đề, khiến cho mã nguồn trông sạch sẽ hơn, giúp bạn lên level mới trong lập trình game.

Okay, nếu thấy hay thì nhớ share :)) có gì thắc mắc thì nhớ comment bên dưới đề mình giải đáp nhé, cảm ơn mọi người đã đọc.

Share:

Chủ Nhật, 14 tháng 2, 2021

Cách viết một Chrome Extension

 

Intro

Chrome extension thì có lẽ không cần phải nói nhiều rồi, nó đã quá phổ biến và cực kì tiện dụng luôn. Các bạn ở đây chắn chắn ai cũng đã dùng qua một lần rồi. Với những extension hàng triệu người dùng trên Chrome web store kia thì bản thân mình cũng ao ước tạo ra một sản phẩm mang lại sự hữu ích cho mọi người. Vì vậy mà hôm nay mình sẽ hướng dẫn các bạn cách để viết một extension, nó cực kì dễ luôn (mình nói đùa đó :V ). Thật sự lúc bắt đầu thì mình chả biết phải làm thế nào luôn, nó như một cánh cửa mở ra một chân trời mới vậy nhưng sau thời gian tìm hiểu thì mình nhận thấy: để build một extension không hề khó nhưng để tạo được một sản phẩm tốt thì thật sự không hề dễ dàng. Nào, bắt đầu thôi...

Sơ lược về Chrome extension

Extensions are made of different, but cohesive, components. Components can include background scriptscontent scripts, an options pageUI elements and various logic files. Extension components are created with web development technologies: HTML, CSS, and JavaScript. An extension's components will depend on its functionality and may not require every option.

Cái này mình copy trên trang chủ Developer Chrome: https://developer.chrome.com/docs/extensions/mv2/getstarted

Tổng quan bạn có thể hiểu như sau: Extension là một đối tượng (1 cục code) có thể chạy trên 3 môi trường

  • Môi trường trình duyệt: Các tab khi ta mở browser.
  • Môi trường popup: Khi ta click vào những icon của extension, trình duyệt mở ra một popup và ta thao tác trên popup đó.
  • Môi trường background: Những tiến trình chạy ngầm, ta có thể check bằng cách mở Task manager lên. Điều này giải đáp thắc mắc của các bạn vì sao khi mở có 1 tab Chrome nhưng task manager lại chạy nhiều tiến trình của Chrome như vậy. Nếu mỗi tiến trình có yêu cầu quyền chạy background thì khi ta mở trình duyệt, đồng thời extension được khởi chạy và nó sẻ chiếm một tiến trình trong background của PC

Ta có thể viết một extension bằng một số kĩ năng về HTML, CSS, JS thuần hoặc có thể build một ứng dụng có sử dụng Back end server như Node. Việc sử dụng này cần có một thằng gọi là Bundler để bundle mớ code của bạn trước khi chạy trên web store. Trong bài viết này mình chỉ hướng đến việc xây một extension đơn giản sử dụng HTML, CSS, JS.

Tổng quan sản phẩm sắp thực hiện

Extension này mình đặ tên cho nó là Smart bookmark nha. Cụ thể công dụng của nó là Bookmark lại những trang web mà chúng ta cảm thấy thú vị hoặc tùy thích. Mình nhận thấy nó khá hay và đáp ứng tốt nhu cầu sử dụng hàng ngày của bản thân.

Hình ảnh này không có thuộc tính alt; tên tập tin này là image.png

Khi ta gặp một trang web muốn bookmark thì chỉ cần chọn vào extension, lựa chọn category phù hợp rồi bấm nút Add (+). Hoặc trong ta cũng có thể xóa một item khỏi bằng cách chọn Del (-)

Khi ta chọn vào một item đã lưu thì chrome sẽ mở ra một tab mới và dẫn ta đến địa chỉ đã lưu

Quá đơn giản đúng không nè. Bây giờ thì bắt đầu thôi...

Cấu trúc một folder code

Hình ảnh này không có thuộc tính alt; tên tập tin này là image-1.png

Đây là cách tổ chức folder của mình nhưng ở đây bạn chỉ cần quan tâm duy nhất 1 file có tên là manifest.json. File này chứa các thông tin định nghĩa cho extension của bạn

{
  "manifest_version": 2,
  "name": "Smart bookmark",
  "version": "1.0",

  "description": "Commonly useful tools for dev",
  "icons": {
    "16": "src/icons/popup.png",
    "48": "src/icons/popup.png",
    "128": "src/icons/popup.png"
  },
  "author": "PA_NQT",
  "browser_action": {
    "default_icon": {
      "16": "src/icons/popup.png",
      "24": "src/icons/popup.png",
      "32": "src/icons/popup.png"
    },
    "default_title": "Smart bookmark",
    "default_popup": "popup.html"
  },
  "permissions": ["tabs"],
  "toggle-feature-foo": {
    "suggested_key": {
      "default": "Shift+S+B",
      "mac": "Shift+S+B"
    },
    "description": "Toggle feature smart bookmark"
  },
  "content_security_policy": "script-src 'self' https://ajax.googleapis.com; object-src 'self'"
}

Một số fields ta cần quan tâm ở đây như:

  • "manifest_version": Version của extension (hiện tại có 3 phiên bản 1, 2, 3)
  • "icons": Icon hiện thị ngang với khung URL
  • "browser_action": có một field cần chú ý đó là "default_popup" đây là đường dẫn tới file HTML khi người dùng click vào icon của extension
  • "permissions": Cần trình duyệt cấp những quyền gì để chạy extension
  • "content_security_policy": Cái này mình mô tả để load AJAX call API vì mặc định browser chặn việc này

Các bạn có thể xem thêm các fields cụ thể hơn cần thiết cho dự án tại: https://developer.chrome.com/docs/extensions/mv2/manifest/

Oke khá là đơn giản, bây giờ ta thực hành luôn..

Setup server API

Mình không có ý định up extension này lên web store nên bây giờ mình sẽ hướng dẫn các bạn xây 1 API Server để sử dụng extension này cực dể bằng công cụ có sẵn: Mock API

  1. Vào trang chủ của nó và đăng kí một tài khoản: https://mockapi.io/
  2. New một project và đặt tên tùy ý
  3. Chọn project vừa tạo và New resource đặt tên là tools nha... đặt vậy để đỡ sửa code chứ không vấn đề gì

Kết quả như hình đây:

Mock API

À phần data bây giờ bạn mới thêm vô nè: Ta lấy data trong file store.json (nhớ copy cho đúng nha) rồi đặt vào đấy để làm data ban đầu.

Ở đây còn có một thông tin thêm là API URL: https://600e379f3bb1d100179de841.mockapi.io/api/v1/:endpoint

Endpoind ở đây chính là tools lúc nảy vừa tạo. Bạn có thể test thử server đã hoạt động chưa bằng cách vào đây: https://600e379f3bb1d100179de841.mockapi.io/api/v1/tools

Nó ra một đống Data là oke rồi nha...

Coding thôi

Phần giao diện popup

Ta không cần nói nhiều ha... Tùy thẩm mĩ của mỗi người

https://github.com/Nguyen-Quoc-Thai/smart-bookmark/blob/master/popup.html

Phần code chính

File util.js

https://github.com/Nguyen-Quoc-Thai/smart-bookmark/blob/master/src/js/util.js

File này chứa các utilities function... để giảm bớt số lượng code đó mà

/** Utils func */

// Item template
const itemTemplate = (resource) => {
  return `
    <div class="item">
      <button type="button" class="btn btn-light btn-sm" src=${
        resource.link
      } id="${resource.id}"
      data-toggle="tooltip" data-placement="bottom" title="${resource.name}"
      >
        <img class="icon" src=${resource.icon} alt=${
    resource.name
  } style="width: 18px; height: 18px;"/>
        <span class="name">${
          resource.name.length < 13
            ? resource.name
            : `${resource.name.slice(0, 10)}...`
        }</span>
      </button>
      <div class="btn-del"><span></span></div>
    </div>
  `;
};

// Add event click on an item
const addEventNewTabForEachBtn = (selector, except_selector, attr_url) => {
  $(selector)
    .not(except_selector)
    .click(function () {
      const url = $(this).attr(attr_url);

      // Create new tab
      chrome.tabs.create(
        {
          url
        },
        function () {}
      );
    });
};

Ta có các phần để rút ngắn code như là mỗi item thì có cấu trúc như nhau nên ta định nghĩa một template chung. Mỗi item lại cần sự kiện click để mở nên ta cũng làm tương tự.

Function addEventNewTabForEachBtn có sử dụng một lời gọi chrome.tabs chỉ định thao tác này cần tương tác với các tabs của trình duyệt (ta cần cấp permission cho thao tác này trong file manifest.json nha)

File main.js

https://github.com/Nguyen-Quoc-Thai/smart-bookmark/blob/master/src/js/main.js

Ta định nghĩa các thao tác để xữ lí dữ liệu thông qua API đã đề cập như:

  1. GetAll resource rồi render ra khi người dùng click mở popup
  2. Thêm một bookmark mới khi người dùng bấm Add
  3. Xóa một bookmark khi người dùng tùy chọn

Task 1:

fetch(`${BASE_API_URL}/tools`)
    .then((response) => response.json())
    .then((store) => { // Xử lí data render })

Sau khi render thì ta AddEventListener click cho từng button bằng function đã tạo ở file util

// Handle add listener: choose an item (create new tab)
$(window).ready(function () {
     addEventNewTabForEachBtn('.dev-engine button', '.btn-add', 'src');
});

Task 2:

$('.dev-engine .btn-add').click(function () {
      const curr = $(this);
      chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => {
          // Handle new API and POST req API
      })
})

Với mỗi btn-add của mỗi resource ta bắt sự kiện click. Lấy thông tin của tab hiện hành và POST req API đến Mock server

// Call API -  send new item
fetch(`${BASE_API_URL}/tools`, {
          method: 'POST',
          headers: {
              Accept: 'application/json',
              'Content-Type': 'application/json'
          },
          body: JSON.stringify(newItem)
      })
         .then((response) => response.json())
         .then((newResource) => {
              // Update view
         })
})
Task 3:

Tương tự task 2 nhưng lần này ta send req với Method là DELETE

$('.dev-engine .btn-del').click(function () {
      const curr = $(this);
      const id = curr.prev().attr('id');

      fetch(`${BASE_API_URL}/tools/${id}`, {
          method: 'DELETE'
      })
          .then((response) => response.json())
          .then((result) => {
              // Update view
           })
})

Okii vậy là hoàn tất bât giờ ta load thử extension lên Chrome nhé...

Vào đây: chrome://extensions/

Bật Developer mode rồi chọn Load unpacked và trỏ tới thư mục code là được.

Toàn bộ source code tại: https://github.com/Nguyen-Quoc-Thai/smart-bookmark

Đấy nó chỉ đơn giản thế thôi.. Cảm các bạn đã đọc...

WriterQuốc Thái

Follow me via:
Facebook
Github

Share: