יום חמישי, 8 בספטמבר 2011

Unity's Network

היי,

לאחרונה התחלתי לעבוד על הפיכת משחק offline שאני עובד עליו למשחק online.
בזמן העבודה גיליתי כמה דברים חדשים על יוניטי והופתעתי מחדש כשגיליתי עד כמה מנוע חוסך לך המון שורות קוד והמון זמן תכנות.
בתור אחד שעבד טיפה עם UDP/TCP בניסיון לבנות משחק רשת ב-XNA, זה היה שיפור משמעותי בנוחות ובמהירות הפיתוח.
בפוסט הזה אני אכתוב בבלוג על כמה "תגליות" שלי במהלך העבודה - מערכת הרשת של יוניטי: סינכרון אובייקטים, הפעלת פקודות מרחוק ועוד.


קודם כל, חובה לציין שהצוות של יוניטי עשה עבודה מעולה בעטיפה של פרוטוקולים (פרוטוקול הוא בעצם אוסף חוקים שמגדיר את הצורה שבה המידע בין שתי נקודות ישלח. לקריאה נוספת: ויקיפדיה) על מנת להקל על המפתחים.
גם מי שמעולם לא תכנת יישומים המצריכים שימוש בתקשורת בין מחשבים יוכל ללמוד בקלות כיצד להשתמש במערכת הרשת של יוניטי.

נתחיל מהסבר כללי על מערכת הרשת.

המודל של שרת ולקוח
על מנת ששני מחשבים או יותר יוכלו להשתתף באותו משחק, אחד מהמחשבים צריך להיות השרת (Server) ושאר המחשבים יהיו הקליינטים (Clients).
שרת - אחראי על קבלת בקשות התחברות של הקליינטים, אישור בקשות ההתחברות, קבלת מידע מהקליינטים ושליחת מידע אליהם.
קליינט - אחראי על התחברות לשרת, קבלת מידע ושליחת מידע.
הקליינטים לא מתחברים אחד לשני.
השרת מחובר לכל הקליינטים וכל קליינט מחובר לשרת בלבד, כך:
כאשר השרת מקבל מידע מקליינט מסויים הוא דואג לסנכרן אותו עם שאר הקליינטים.
*הערה ביוניטי המודל הזה שונה. מה שהולך מאחורי הקלעים עובד בדיוק כמו בתמונה, עם זאת נהוג ביוניטי ששרת מתארח אצל אחד הקליינטים.
תודה לעדיאל (adiel666) על ההערה.

העברת נתונים ברשת עם יוניטי:
על העברת הנתונים אחראי ה-component שנקרא Network View.
המאפיין העיקרי בו הוא ה-ID. הנתונים ברשת עוברים בין אובייקטים בעלי ID זהים.
כלומר אם בשרת יש אובייקט עם Network View שמספרו הסידורי הוא 2 וגם בקליינט קיים אובייקט כזה אך קיים גם אובייקט עם המספר הסידורי 1, ההודעות מהאובייקט שבשרת יגיעו לאובייקט מספר 2 בלבד.
על שאר המאפיינים של Network View אכתוב בהמשך.

קיימות שתי דרכים עיקריות להעברת נתונים ברשת כאשר מדובר ביוניטי:

State Synchronization:
בדרך זו נשלחים מאפיינים של component מסויים מ-Game Object אחד לשאר האובייקטים עם Network View ID זהה.

Remote Procedure Calls (RPCs):
בדרך זו נשלחת קריאה לפונקציה. ניתן גם לקרוא לפונקציה עם מאפיינים.
לדוגמה, נניח שיש לנו צ'אט. כאשר נשלחת הודעה בצ'אט תשלח קריאה לפונקציה שתוסיף את ההודעה לשאר ההודעות שכבר נשלחו על מנת שתופיע גם היא בחלון הצ'אט.
ב-RPCs הפונקציה תופעל רק באובייקטים בעלי Network View ID זהה.

לכל אחת משתי מהדרכים שימושים משלה וקל מאוד לעבוד איתן.

מחלקת Network
מחלקת Network אחראית על (כמה מפתיע) הרשת.
במחלקה קיימות פונקציות להתחברות והתנתקות מהשרת, יצירת שרת, בדיקת Ping, יצירת אובייקטים משותפים ברשת ועוד.
על מנת להתחיל תקשורת בין השרת לקליינט נצטרך לאתחל את השרת על מנת שיאשר בקשות התחברות ממשתמשים ולאחר מכן לשלוח, בעזרת פונקציה, בקשת התחברות מהקליינט לשרת.
לכן זוהי המחלקה הראשונה שנשתמש בה במהלך בניית משחקי רשת.

הפונקציה שמאתחלת את השרת היא:

Network.InitializeServer(int connections, int listenPort, bool useNat);


הפרמטרים:
connections - מספר החיבורים האפשריים (כלומר מהי הכמות המקסימלית של מחוברים).
listenPort - הפורט לו יאזין השרת. לפורט זה ישלחו בקשות ההתחברות של הקליינטים.
useNat - האם יהיה שימוש ב-NAT Punch Through (מאפשר תקשורת בין 2 מחשבים שנמצאים מאחורי ראוטר).

הפונקציה ששולחת בקשת התחברות מהקליינט לשרת היא:

Network.Connect(string IP, string remotePort, string password);

הפרמטרים:
IP - מחרוזת המייצגת את כתובת השרת (IP או דומיין).
remotePort - הפורט אליו הקליינט ינסה להתחבר שהוא listenPort שהוזן באתחול השרת.
password - (שדה אופציונלי) סיסמת השרת. אין צורך למלא אם לא הוגדרה כזאת סיסמה.

לאחר אתחול הסרבר יקרא האירוע OnServerInitialized().

לאחר שנוצר החיבור בין הקליינט לסרבר נקרא אירוע בהתאם.
בקליינט יקרא האירוע OnConnectedToServer() ובסרבר יקרא האירוע OnPlayerConnected().

ד"א אני ממליץ שברגע שנוצר חיבור תוודאו שהמשחק יעבוד גם כאשר החלון ממוזער. אחרת הקליינט לא יגיב לסרבר ולשאר השחקנים יראה כאילו הוא תקוע.
את זה ניתן לבצע באמצעות השורה הבאה:
Application.runInBackground = true;

אחד המשחקים שפיתחתי ביוניטי (המשחק שכתבתי עליו בתחילת הפוסט) כולל בתוכו GUI שבתחילת המשחק מאפשר לשחקן בחירה, האם להיות הקליינט או השרת. במידה והשחקן הוא קליינט, ה-GUI מקבל שם משתמש ו-IP ומתחבר לשרת. במידה והשחקן הוא שרת, ה-GUI מקבל שם משתמש בלבד ומאתחל את השרת.
השרת מתנהג כמו הקליינט, כלומר גם השחקן שהוא שרת משתתף במשחק ולכן הקוד של האתחול כמעט זהה.

הקוד:

public class PBClient : MonoBehaviour
{
    public GameObject player;
    public string ipString = "127.0.0.1";
    public string name;
    public int maxConnections = 1;

    GameObject chat;

    void Start()
    {
        name = "Guest" + Random.Range(0, 10000);
        chat = GameObject.Find("Chat");
        chat.active = false;
    }

    void OnConnectedToServer()
    {
        print("Connected!");
        InitGame();
    }

    void OnServerInitialized()
    {
        print("Server is ON");
        InitGame();
    }

    void InitGame()
    {
     //Init code here.
    }

    void OnGUI()
    {
        ipString = GUI.TextField(new Rect(10, 70, 200, 20), ipString, 25);
        name = GUI.TextField(new Rect(10, 100, 200, 20), name, 25);
        if (GUI.Button(new Rect(10, 10, 100, 20), "Start Server"))
        {
            Network.InitializeServer(maxConnections, 50000, false);
            Application.runInBackground = true;
        }
        else if (GUI.Button(new Rect(10, 40, 100, 20), "Connect"))
        {
            Network.Connect(ipString, 50000);
            Application.runInBackground = true;
        }
    }
}


אחרי שהבנתם (אני מקווה) איך ליצור חיבור, בין השרת לקליינט נעבור לסינכרון האובייקטים.

 Network Views
כאשר מתכנתים משחקי רשת, חלק חשוב בתכנות הוא לקבוע (ולתכנת בהתאם כמובן) איזה מידע לסנכרן בין הקליינטים והשרת ואיך הוא יסונכרן.
ביוניטי קיים Component שמנהל את החלק הזה של התכנות והוא ה-Network View.
לכל Network View יש 3 מאפיינים:
  1. State Synchronization
  2. Observed
  3. View ID
 שני המאפיינים הראשונים קשורים ל-State Synchronization ואסביר עליהם בקרוב.
המאפיין השלישי הוא בעצם המספר הסידורי של ה-View ID. מידע שנשלח מ-Network View במחשב אחד, יגיע ל-Network View בעל אותו View ID במחשב אחר.
אובייקט שתצרו בעורך יקבל באופן אוטומטי ID, שישמש אותו גם במהלך המשחק.
לדוגמא, אם יצרתם אובייקט צ'אט בעורך שמכיל Component מסוג Network View, תדעו שכל המידע שנשלח דרך האובייקט הזה, יגיע בדיוק לאותו אובייקט צ'אט בשאר הקליינטים.

*הערה: בדרך כלל ה-ID ייקבע אוטומטית כאשר תצרו את האובייקט אך יש מקרים מסויימים בהם תצטרכו לקבוע את ה-View ID, ע"י המתודה:
Network.AllocateViewID()
לקריאה נוספת: Network.AllocateViewID

העברת הנתונים:
עד עכשיו למדנו על כל מה שמסביב. מודל שרת-לקוח, התחברות, Network Views, איך המידע מגיע ליעד וכו'.
בחלק הזה נלמד על העברת הנתונים בפועל - איך לממש את זה במנוע ובקוד.

נתחיל משיטת הסנכרון. בעזרתה ניתן לראות את התוצאות כמעט באופן מיידי וללא צורך בתכנות.
עם זאת, בשביל אפשרויות יותר מתקדמות מהאפשרויות הבסיסיות שהמנוע מציע יש צורך בתכנות.

State Synchronization
קודם כל, אם הספקתם לשכוח מה זה State Synchronization (מעכשיו אני אכתוב State Sync), כתבתי הסבר קצר בתחילת המדריך .
כשהסברתי על Network Views היו שני מאפיינים שלא פירטתי עליהם והם נוגעים לאילו נתונים ישלחו וכיצד כאשר מתבצע State Sync.


מחזור למאפיינים של Network View:
  1. State Synchronization
  2. Observed
  3. View ID
את המאפיין השלישי מחקתי כי אתם כבר מכירים אותו (ובעיקר כי רציתי למצוא שימוש לקו האמצעי הזה P:).

המאפיין הראשון - State Synchronization קובע את צורת שליחת הנתונים.
ניתן לשלוח את הנתונים בשתי צורות:

Unreliable -
שליחה של כל הנתונים, לאחר תקופת זמן מסויימת (ברירת המחדל היא 15 פעמים לשנייה).
לא בטוח שכל החבילות יגיעו בסדר הנכון או יגיעו בכלל ליעד.

ReliableDeltaCompressed -
שליחה רק של הנתונים שהשתנו מאז השליחה האחרונה. במידה ואין נתונים שהשתנו, לא יישלח כלום.
השולח מוודא שהחבילה הגיעה ליעד. אם היא לא הגיעה ליעד החבילה תשלח שוב.

מהקריאה הראשונה נראה שהסוג השני יותר טוב מכל בחינה. אך כאשר מנתחים לעומק את היתרונות והחסרונות מתברר שלכל סוג יש תפקיד משלו.
הסוג הראשון מתאים מאוד לאובייקטים שהמידע שלהם משתנה כל הזמן. מפני שכך אפשר לדעת כמעט בודאות שכל שליחה תכיל נתונים שונים ולכן אין צורך בדחיסה שהסוג השני מציע. בנוסף, אם חבילה תגיע בסדר לא נכון או שלא תגיע בכלל, יתבצע תיקון כמעט מידי על ידי החבילות הבאות ולכן לא יווצר לאג כאשר אחד השחקנים מחכה לשליחה חוזרת של חבילה שלא הגיעה.
דוגמה טובה היא מיקום של מכונית (ומיקום בכלל) במשחק מרוץ מכוניות. המיקום של המכונית משתנה כמעט בכל פריים ולכן היתרונות של הסוג הראשון תקפים לגביה.

הסוג השני מתאים לנתונים שמשתנים בתדירות נמוכה יותר אך חובה שיגיעו ליעד.
לדוגמה אובייקט שמנהל את הציוד שנמצא על השחקן במשחקי MMORPG למיניהם.
אם לכל ציוד יש ID משלו, האובייקט יעדכן את ה-ID וישלח אותו לשרת.
שינוי של ציוד לא מתבצע כל פריים אלא לעיתים נדירות יחסית וחשוב שהמידע הזה יגיע לשרת כמו שהוא ובסדר הנכון.

ניתן גם לכבות לגמרי את הסכרון ע"י בחירה (או שינוי בקוד) באפשרות Off.

המאפיין השני הוא Observed, המאפיין הזה מכיל את ה-Component שעליו אחראי ה-Network View.
הוא יכול להכיל אחד מהבאים:
  • Transform - יסנכרן את מיקום, סיבוב וגודל האובייקט.
  • RigidBody - יסנכרן את המיקום בנוסף לתכונות פיזיקליות.
  • Animation - יסנכרן את האנימציה של האובייקט.
  • סקריפט - המתכנת קובע איזה מידע יסונכרן.
 כדי לכתוב סקריפט שיסנכרן מידע ברשת, הסקריפט צריך להכיל את המתודה:
void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)

BitStream - זרם ביטים. זהו אובייקט שכותב או קורא נתונים בהתאם למצב שלו.
NetworkMessageInfo - מידע בסיסי על ההודעה - שולח, Network View וחתימת זמן (בשניות).

הקוד הבא מסנכרן את המיקום של האובייקט (במצב כתיבה הוא שולח אותו ובמצב קריאה הוא מקבל אותו):

        void OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)
        {
            //If the stream is in write mode
            if (stream.isWriting)
            {
                Vector3 position = transform.position;
                stream.Serialize(ref position); //Write data
            }
            //If the stream is in read mode
            else
            {
                Vector3 position = Vector3.zero;
                stream.Serialize(ref position); //Read data
                transform.position = position;
            }
        }


לעוד מידע ודוגמאות:

RPCs
דרך נוספת לסינכרון אובייקטים היא קריאה לפונקציות מרחוק.
הקריאה לפונקציות מרחוק פשוטה מאוד, כמעט כמו קריאה לפונקציה רגילה.
זכרו שכמו בסנכרון, גם כאן ניתן לקרוא למתודות על אובייקטים בעלי NetworkViewID זהה.

ניקח דוגמה של צ'אט. בכל פעם שאחד השחקנים שולח הודעה תרצו לעדכן את שאר השחקנים בכך.
אם נשתמש בשיטה של סנכרון מידע, נצטרך לחבר את המחרוזת החדשה לכל הטקסט שכבר קיים בצ'אט ואז לסנכרן את המחרוזת הזאת עם כל שאר השחקנים. זה בזבוז מפני שנשלח המון מידע, הרבה יותר ממה שצריך להשלח (חוץ מזה שהסנכרון לא תומך במחרוזות, לפחות לא בפעם האחרונה שניסיתי).
היה הרבה יותר פשוט אם היינו קוראים לפונקציה "הוסף הודעה לצ'אט" שמקבלת כפרמטר את ההודעה שיש להוסיף לצ'אט.
לשם כך נוצרו ה-RPCs.

כדי להגדיר פונקציה להיות RPC צריך להוסיף לה מאפיין בהתאם: [RPC] (ב-JS זה התחביר: @RPC)

וכך זה נראה בקוד:

        [RPC]
        void ChatMessage(string message)
        {
            messages += message + "\n";
        }


הערה: לא ניתן להשתמש בכל סוג משתנה כפרמטר של RPC, ה-types הנתמכים הם אלה בלבד:

  • int
  • float
  • string
  • NetworkPlayer
  • NetworkViewID
  • Vector3
  • Quaternion

על מנת לקרוא למתודה מרחוק נשתמש במתודה RPC של המחלקה NetworkView.
המתודה היא:
networkView.RPC(string name, RPCMode mode, object[] params args); 
name - שם המתודה שתרצו לקרוא לה.
mode - למי תשלח המתודה והאם היא תשמר ב-Buffer.
זהו enum שמכיל 5 אפשרויות:
  1. Server - המתודה תתבצע אצל הסרבר בלבד.
  2. All - המתודה תתבצע אצל כולם.
  3. Others  - המתודה תתבצע אצל כולם מלבד השולח.
  4. AllBuffered - המתודה תתבצע אצל כולם, בנוסף היא תשמר ב-Buffer. לכל שחקן חדש שמתחבר, כל ה-RPCs נשלחים. לדוגמה שחקן זרק חפץ מהתיק שלו ושאר השחקנים יכולים לאסוף אותו. אם שחקן חדש יתחבר וה-RPC של זריקת החפץ לא ישמר ב-Buffer רק השחקנים שכבר התחברו יוכלו לראות את אותו החפץ. דוגמה נוספת היא טעינת שלב, בדרך כלל נרצה שכל השחקנים יטענו את השלב הנוכחי ברגע שהם מתחברים ולא רק כאשר הסרבר מכריז על שינוי שלב.
  5. OtherBuffered - המתודה תתבצע אצל כולם מלבד השולח ותשמר ב-Buffer.
args - הפרמטרים שהמתודה מקבלת.

אם נקח לדוגמה את ה-RPC של הצ'אט, הקריאה למתודה תראה כך:

networkView.RPC("ChatMessage", RPCMode.All, message);

הקריאה צריכה להעשות מתוך Component שמוצמד לצ'אט. לצ'אט צריך להיות מוצמד גם Component מסוג Network View.
הקוד ניגש ל-Network View שמוצמד לצ'אט, וקורא לRPC דרכו. שם הפונקציה הוא ChatMessage, הפרמטר הוא ההודעה (מסוג סטרינג) והוא ישלח RPC לכל המחוברים (כולל את עצמו).
אם תרצו שהצ'אט ישמור היסטוריה של ההודעות ויציג את כל ההודעות שנשלחו אי פעם לשחקנים, ניתן לשנות את RPCMode ל- AllBuffered.
נניח שבנוסף להודעה, נשלח פרמטר מסוג int שמגדיר את צבע ההודעה, במקרה כזה יש צורך לשנות את ה-RPC עצמו כך שיקבל שני פרמטרים והקריאה לפונקציה תראה ככה:


networkView.RPC("ChatMessage", RPCMode.All, message, color);

ניתן גם לשלוח RPC לשחקן ספציפי, במקום mode המתודה מקבלת כפרמטר NetworkPlayer.

אלו היו שתי הדרכים לשליחת מידע ברשת ואנחנו ממש לקראת הסוף.
עכשיו נעבור על כמה פונקציות שימושיות, סיכום ויאללה הביתה D:

חזרה למחלקת Network

קיימות מספר פונקציות שימושיות במחלקת Network שתצטרכו כמעט בכל משחק.

Network.Instantiate
יצירת אובייקט ברשת. האובייקט יכיל Network View ID זהה אצל כל המחוברים וחוסך המון עבודה.
בנוסף, המתודה מתנהגת כ-RPC שנשמר ב-buffer. כלומר שכל שחקן שיצטרף יראה את כל האובייקטים שנוצרו עד עכשיו.
כך נראית המתודה:

Network.Instantiate(Object prefab, Vector3 position, Quaternion rotation, int group);

group - הקבוצה אליה ה-RPC ישתייך. בהמשך תראו שניתן למחוק RPCs מה-buffer באמצעות הקבוצה שאליה הם שייכים.
*גם ל-Network View קיים group ID שנקבע עפ"י המאפיין group של האובייקט.


Network.Destroy
השמדת אובייקט ברשת.

Network.Destroy(GameObject gameObject);

 Network.IntializeSecurity
המתודה מאתחלת את שכבת האבטחה של הסרבר.
יש לקרוא לה לפני אתחול הסרבר ואין לקרוא לה בקליינט.

Network.RemoveRPCsInGroup, Network.RemoveRPCs
המתודות מנקות את ה-buffer מ-RPCs.
ניתן למחוק RPCs מסויימים עפ"י מאפיינים שונים:

מנקה את כל ה-RPCs מקבוצה מסויימת.
Network.RemoveRPCsInGroup(int group);


מנקה את כל ה-RPCs שנשלחו משחקן מסויים.
Network.RemoveRPCs(NetworkPlayer playerID);

מנקה את כל ה-RPCs שנשלחו משחקן מסויים ונמצאים בקבוצה מסויימת.
Network.RemoveRPCs(NetworkPlayer playerID, int group);

מנקה את כל ה-RPCs שנשלחו מ-NetworkView בעל ID מסויים.
Network.RemoveRPCs(NetworkViewID viewID);

Network.incomingPassword
הסיסמה לשרת (מסוג string) אותה מזינים בפעולה Connect בקליינט.
כלומר במתודה הזאת:
Network.Connect(string IP, string remotePort, string password); 


יש לי עוד המון מה לכתוב בנושא הזה אבל הפוסט מתחיל לתפוס נפח ולכן אני נאלץ להפסיק כאן.

סיכום:
המנוע יוניטי מאפשר לשלב רשת במשחק גם למי שמעולם לא נגע בתכנות הכולל את הנושא הזה.
קיימות שתי דרכים עיקריות להעברת נתונים:
-סינכרון - העברת נתונים עם דחיסה או בלי דחיסה.
-קריאה לפונקציות מרחוק.
הכל עטוף באופן שקריא ונוח למתכנת.
בנוסף קיימת אפשרות אבטחה מובנית, סיסמה, יצירת אובייקטים משותפים, הריסת אובייקטים משותפים ועוד...

לקריאה נוספת, פונקציות, מאפיינים ועוד:

ונושא שלא כתבתי עליו ויכול להיות שיעניין חלק מכם:

כמו תמיד, קצת מוזיקה. Trivium, אחת הלהקות המוצלחות שיצא לי אי פעם לשמוע. אף שיר שלהם (טוב נו, אולי השניים החדשים) לא אכזב אותי.

לילה טוב לכולם, ולכל התלמידים והסטונדנטים - שנת לימודים מוצלחת (גם במקצועות שהם לא מדעי המחשב ;) ).

יום שלישי, 2 באוגוסט 2011

משחק חדש ועדכון קצר

היי,

עבר המון זמן מאז העידכון האחרון, אך אני עדיין עובד על פוסטים חדשים לבלוג.

בשבועיים האחרונים חזרתי לעבוד על המשחק שהשתתפתי בהכנתו ב-Global Game Jam עם אחד הגרפיקאים שעיצבו מודלים למשחק.
שיפרנו את המשחק בעיקר מבחינת אופן השליטה בשחקן שעד אז היה מסורבל ולא התאים לאווירת המשחק.
את השחקן הסרנו לגמרי, כלומר לא ראו יותר מודל של חייל שרץ. לאחר מכן שינינו את מיקום וכיוון המצלמה כך שיראו את מהלך המשחק מלמעלה כמו במשחקי אסטרטגיה רבים.
האינטרקציה עם אובייקטים מתבצעת עכשיו באמצעות לחיצת עכבר עליהם. כדי לקחת רעל לוחצים עליו, וכדי להרעיל גזר לוחצים עליו.
לבסוף שיפרנו מעט את ה-GUI (ממשק המשתמש), זה התבטא בהוספת חלון המראה את מספר הארנבים שנותרו, הוספת רקע לחלון התחמושת (שמראה כמה ערכות רעל נותרו) וכפתור Reset למשחק לאחר שמנצחים / מפסידים.

כרגע אנו אוספים תגובות ודעות על המשחק: מה אפשר לשפר, איך אפשר לשפר, אילו אפשרויות להוסיף וכו'.
אני מקווה שבקרוב אפרסם בבלוג את המשחק על מנת שתוכלו להגיב.

ככה נראית הגרסה החדשה:
אותם הארנבים, זווית שונה.

ובנושא אחר, בחודש האחרון אני עובד (לא ברצף כמובן) על פוסט מאוד מפורט ומושקע על נושא משחקי הרשת של יוניטי.
אני מקווה שהפוסט יהיה מוכן השבוע ואיתו כנראה אפרסם את הגרסה החדשה של המשחק.

בקיצור, יש למה לצפות :)

**עדכון!**
את המשחק אני מפרסם בפוסט הזה, ואשמח אם תוכלו לתת ביקורת.

הארנבים מתרבים והולכים ליצור פיצוץ אוכלוסין בעולם.
המשימה שלכם היא למנוע את זה ע"י הרעלת הגזרים שהם אוכלים.
אז איך משחקים?
לקיחת רעל והרעלת גזר באמצעות לחיצה על העכבר.
את המפה ניתן לגלול באמצעות הזזת העכבר לגבולות המפה, ע"י החצים או WASD.
Zoom in & out - גלגלת.
בהצלחה!

ולינק למשחק עצמו: לינק


לפני הסיום, כמו תמיד, שיר! אני לא ממש מכיר את הלהקה אלא רק את השיר הזה וזה מספיק לי בשביל לפרסם אותו:


אז שיהיה לילה טוב לכולם.

יום ראשון, 5 ביוני 2011

Particle System


היי,

הפוסט הזה יעסוק במערכות חלקיקים וההיגיון מאחוריהן, הפוסט נכתב בעיקר למשתמשי XNA אך ניתן לממש קוד כזה גם בסביבות עבודה אחרות.
מערכת חלקיקים היא אחד האפקטים הויזואליים החשובים והמרהיבים ביותר במשחקי מחשב.
הרעיון במערכת הזאת הוא הגדרת אבטיפוס (Prototype, דגם ראשוני) של חלקיק באמצעות מאפיינים רבים.
לאחר מכן יוצרים מספר רב של חלקיקים בעלי התכונות של האבטיפוס.
עם קצת ניסיון ומשחק בפרמטרים תוכלו ליצור אפקטים רבים במספר דקות.

תמונה מה-Particle System שלי
ההתחלה
קודם כל צריך להחליט מה יהיה במערכת. 
לדוגמה:
  • מה יהיו המאפיינים של כל חלקיק.
  • מה יהיו המאפיינים של כל מערכת חלקיקים.
  • כיצד תעבוד ההיררכיה בין מערכת חלקיקים לחלקיקים שבה.
  • האם יהיו אינטרקציות בין חלקיקים.
כדי להחליט על כל אלה מומלץ, כמו בכל דבר, לקחת השראה מאחרים. 
מערכות שאני ממליץ להסתכל עליהן:
למחולל המשחקים Game Maker יש מערכת חלקיקים דו מימדית מאוד גמישה ובעלת אפשרויות רבות.
למנוע Unity יש מערכת חלקיקים תלת מימדית מצויינת.
וכמובן שתוכלו לחפש ביוטיוב ולקבל מאות אם לא אלפי תוצאות.


מתחילים לכתוב:
נתחיל מלכתוב מחלקה לחלקיק. המחלקה תכיל את תכונות החלקיק שהוא יורש (הכוונה בהורשה היא לא הורשה של מחלקת אב למחלקת בן, אלא פשוט הורשה) מהאבטיפוס והמידע על מצבו.

אם לדוגמה החלטתם על המאפיינים: ספרייט, צבעים (שמתחלפים לאורך הזמן), גודל (גודל הספרייט), סיבוב (סיבוב הספרייט), מהירות וכיוון, המחלקה צריכה להראות בערך כך:
מחלקת Particle

אבל גם לחלק מהמאפיינים אפשר לקבוע מאפיינים משלהם. למהירות, גודל, כיוון וסיבוב ניתן לקבוע מינימום, מקסימום, קצב גדילה וכו'...
במקום לחשב בנפרד עבור כל מאפיין את הערך שלו, ניתן ליצור מחלקה שתטפל במאפיין.
מחלקת Property תחזיק מידע על מאפיין. במערכת שלי קיימים מינימום, מקסימום, קצב גדילה וגידול ודעיכה מחזוריים של הערך. אתם כמובן יכולים להוסיף על זה, יש אינסוף אפשרויות.
מחלקת Property

המחלקה Property אצלי נועדה רק להחזיק מידע ולא לטפל בו, על מנת לטפל במידע יצרתי מחלקה נוספת שמקבלת Property ומחשבת את הערך של המאפיין עפ"י הזמן שעבר ותכונות ה-Property.
רוב החישובים במחלקה שמעדכנת ומחשבת את הערך של המאפיין הם על ה-wiggle (גדילה ודעיכה מחזוריים), אם אתם לא מבינים משהו מהחישובים תגיבו על הפוסט ואסביר לכם בפירוט.
מחלקת PropertyUpdater



עכשיו יש לנו מחלקה שאחראית על החזקת מידע על מאפייני של החלקיק ומחלקה שמעדכנת אותם.
מה שנשאר הוא לעדכן את מחלקת Particle עם המאפיינים מהסוג החדש, ולהוסיף מאפיינים חדשים במידת הצורך.
למחלקה המעודכנת הוספתי מספר מאפיינים כמו זמן החיים של החלקיק והאם הסיבוב של הספרייט יהיה ביחס לכיוון התנועה של החלקיק. בנוסף צריך SpriteBatch שעליו נצייר את כל החלקיקים ו-GraphicsDeviceManager על מנת שתהיה גישה למידות חלון המשחק. כך נדע מתי חלקיקים יוצאים מהמסך ולכן לא נצייר אותם.
מחלקת Particle החדשה

עכשיו יש לנו חלקיק עם מאפיינים, והרבה. אבל הוא בעצם לא עושה כלום מעבר לאחסון נתונים, לכן צריך להוסיף לו פונקציות שיבצעו את כל הפעולות שחלקיק עושה.
נתחיל מ-Ctor (פונקציית בנאי, Constructor), אין הרבה מה לעשות מלבד קליטת מאפיינים (תסלחו לי על השימוש ב-keyword this):
Particle Constructor

לאחר שיצרנו את החלקיק נצטרך לעדכן אותו בכל פריים, לכן נכתוב פונקציה שתטפל בזה.
הפונקציה מעדכנת את הטיימר, בודקת האם החלקיק צריך להעלם, ומעדכנת את המאפיינים (באמצעות קריאה לפונקציה Update של PropertyUpdater.
לאחר מכן נעשה חישוב של המיקום החדש של שהחלקיק בהתאם לזווית ולמהירות שלו.
החישוב הוא המרה מגודל (מהירות) וזווית (כיוון התנועה) לרכיבי X ו-Y של וקטור המהירות (למי שלא הבין אני ממליץ לקרוא על המרת וקטורים: ויקיפדיה), וקטור המהירות נוסף למיקום וכך מתקבל המיקום החדש.

Update Particle

 יש לנו את כל המידע שאנחנו צריכים כדי לצייר את החלקיק, אז מה שנשאר בעצם זה רק לצייר אותו.
פונקציית הציור בודקת האם החלקיק נמצא בגבולות המסך (מבלי להתחשב בסיבוב או הגדלה, במקרה של ספרייטים קטנים כמעט שלא מבחינים בהבדל). אם כן היא מציירת אותו בהתאם לנתוני המחלקה ופונקציה נוספת שמחשבת איזה צבע צריך לקחת ממערך הצבעים עפ"י הזמן שהחלקיק קיים.
Draw Particle
Get Color

בשלב הזה תוכלו לצייר חלקיקים באמצעות יצירה ועדכון באופן "ידני". אם תחזיקו אותם ברשימה או רשימה מקושרת, תרעננו את החלקיקים מתי שצריך ותציירו אותם תהיה לכם מערכת שעובדת.
לעבוד כך זה מסורבל ובכלל לא מומלץ, עם זאת תוכלו כבר עכשיו ליצור אפקטים כאלה:


לכן ניצור מחלקות חדשות שיטפלו ביצירה ועדכון של מספר חלקיקים.


Emitter - פולט חלקיקים:
אנחנו צריכים מחלקה שתדע לעשות 3 דברים:
  • ליצור כמות חלקיקים מסויימת ביחידת זמן (כלומר X חלקיקים בשנייה).
  • להחזיק את כל החלקיקים בזיכרון.
  • לעדכן ולצייר אותם.
כדי שנוכל ליצור חלקיקים בצורה נוחה, ניצור מחלקה שמכילה נתונים על חלקיק ופונקציה כדי ליצור חלקיק מסוג זה.
המחלקה ParticleData שכתבתי מטפלת בנושא הזה ואין הרבה מה להרחיב עליו:
מחלקת ParticleData

בנוסף יש צורך לתת לפולט מידע על זמן היצירה בין חלקיק לחלקיק, לכן ניצור מחלקה נוספת ופשוטה, שפשוט מכילה מידע על איזה חלקיק ליצור וכמה ליצור ממנו בשנייה:
מחלקת ParticleCreationData

עכשיו הגיע הזמן ליצור את מחלקת פולט החלקיקים.
מאפייני הפולט:
הפולט יהיה נקודתי (כלומר כל החלקיקים יווצרו באותה נקודה) ולכן נצטרך נקודת התחלה (Vector2).
רשימת החלקיקים שהפולט ייצור (מערך של ParticleCreationData).
טיימרים שיבדקו כמה זמן עבר מיצירת החלקיק האחרון מסוג מסויים כדי לדעת מתי ליצור את החלקיק הבא (מערך של double).
והחלקיקים עצמם יהיו ברשימה מקושרת של חלקיקים (למה רשימה מקושרת? קראו כאן).
במידה ונרצה שתהיה גישה למספר החלקיקים הקיימים, נוסיף גם מאפיין שיחזיר את מספר האיברים ברשימה המקושרת.
המחלקה צריכה להראות כך בינתיים: מחלקת Emitter.

למחלקה יהיו 3 פונקציות: אתחול (Ctor), עדכון וציור.
בפונקציית האתחול יוצרים את הרשימה המקושרת ואת מערך הטיימרים (טיימר לכל סוג חלקיק) ומאפסים את הטיימרים.
פונקציית האתחול

פונקציית העדכון היא בעצם הפונקציה הכי מורכבת במחלקה ויש בה שני חלקים: עדכון החלקיקים הקיימים ויצירת חלקיקים חדשים.
החלק הראשון של הפונקציה עובר על כל החלקיקים, מעדכן אותם ואם הם צריכים להעלם אז הקוד דואג להסיר אותם.
החלק השני בודק עבור כל אחד מהטיימרים אם הוא עבר את זמן היצור של החלקיק עליו הוא "אחראי" במידה והוא עבר נוצרים חלקיקים בהתאם לכמה פעמים הוא עבר את זמן היצור. כדי שלא יקרה מצב לדוגמה שהעדכון הקודם היה לפני שנייה אבל אמורים להווצר 5 חלקיקים בשנייה, ויווצר רק חלקיק אחד.
פונקציית הציור פשוטה ביותר, מעבר על כל החלקיקים ברשימה וציורם:

פונקציות עדכון וציור החלקיקים

כל מה שנותר הוא ליצור מערכת חלקיקים כזאת, לאחר שתשחקו קצת עם הפרמטרים תראו שזה ממש פשוט.
האפקט בתמונה הראשונה בפוסט נוצר כך: הקוד

הערה: לקבלת האפקט המרשים ביותר, בדרך כלל תצטרכו להשתמש ב-SpriteBlendMode.Additive בפקודה Begin של ה-SpriteBatch עליו אתם מציירים.

אם תשקיעו אף יותר תוכלו להוסיף עוד אפשרויות למערכת (תגובות בין חלקיקים, כוח כבידה, שינוי ספרייט, תנועה מורכבת יותר וכו'), להתאים אותה למנוע / שפה / סביבת עבודה אחרים ואפילו להוסיף עורך אפקטים ויזואלי.

לעוד פרטים והורדה של ה-Particle System שלי:
http://www.fxp.co.il/showthread.php?t=6745484

סרטון הדגמה:


ועידכון קטן בנוגע ל-Minecraft שלי ב-XNA, הוספתי יום עם שמש ולילה עם ירח:

זה סוף הפוסט, מקווה שהפוסט הבא יפורסם בקרוב. אני לא יכול להבטיח את זה מפני שאין לי הרבה זמן לכתוב בתקופת הבגרויות.

ומוזיקה! להקה מצויינת שלא אכזבה באף אלבום שהיא הוציאה:



לילה טוב :)
ד"א אם מישהו מעוניין שאכתוב על נושא מסויים, תגיבו על הפוסט עם הנושא ובמידה והוא יתאים לבלוג אני אדאג לכתוב עליו פוסט.



יום חמישי, 26 במאי 2011

לברוא את העולם ב-C#

היי,


לפני מספר חודשים שיחקתי במשחק MineCraft, למי שלא מכיר זה משחק שבו המטרה היא ליצור ולבנות ביום ולהגן על עצמך מפני זומבים בלילה.
מה שמיוחד במשחק הזה הוא שהדגש לא על הגרפיקה אלא על המשחקיות.
העולם של מיינקראפט בנוי מקוביות ונוצר באופן דינאמי, כלומר תוך כדי המשחק וברנדומליות.
לאחר תקופה מסויימת שבה שיחקתי רציתי ללמוד איך יוצרים עולם כמו של מינקראפט.
בפוסט זה אסביר על יצירת האדמה וקצת מעבר.
MineCraft
הכלי שנראה לי הכי מתאים לפרוייקט הוא XNA, ממספר סיבות:
  • השפה האהובה עלי היא C#.
  • אני מכיר את אופן העבודה עם XNA ויש לי ניסיון עם סביבת העבודה הזאת (בדו מימד בעיקר).
  • XNA נותן גישה ישירה לורטקסים (vertices) שירונדרו. כלומר אני אוכל לשלוט על כל נקודה ונקודה בעולם התלת מימדי באופן ישיר.
לאחר שהחלטתי על XNA גיליתי שאני צריך לרענן את הידע שלי בעבודה עם 3D, לשם כך נעזרתי בסדרת מדריכים שאני ממליץ עליה מאוד:
http://www.riemers.net/ 
(בצד ימין למטה תוכלו לראות את רשימת המדריכים,  3D series 1 ו-3D series 2 ילמדו את הידע הבסיסי הנדרש בעבודה ב-3D עם XNA).

לאחר שרעננתי את הידע שלי הייתי צריך למצוא דרך ליצור קוביות.
ליצור מודל של קוביה בתוכנה מסויימת ולצייר אותו על המסך כמה פעמים לא היה רעיון טוב, מפני שאז כל עניין השליטה בורטקסים (הנקודות בעולם התלת מימדי) היה מסתבך, שינוי הטקסטורה של קוביות מסוגים שונים היה מסורבל וכו'.
לכן הייתי צריך למצוא דרך לכתוב למערך את הורטקסים של הקוביה.

לכתוב קוד כזה זה סיוט, לכן אני מפרסם את הפונקציה שכתבתי (בעזרת כמה חיפושים בגוגל ושינויים שהכנסתי):

לאחר שהצלחתי ליצור קוביה, הגיע הזמן להתחיל לעבוד על יצירת העולם.


הקוביות מוחזקות במערך תלת מימדי. הגודל של כל קוביה הוא 10 יחידות, ולכן ההפרש במיקומים של קוביות בין שני תאים במערך הוא 10 יחידות.
העולם עדיין לא מתרחב באופן דינאמי אלא נוצר פעם אחת בתחילת המשחק בגודל מוגדר מראש.


התחלתי בניסיונות:


ניסיון 1:


הניסיון הראשון היה לא רע. הגובה בכל נקודה היה שווה לממוצע הגבהים של 2 הנקודות הסמוכות לה, לממוצע חיברתי מספר רנדומלי 1-, 1 או 0.
התוצאה הייתה קוביות בגבהים די אחידים אך היה המון "רעש" כלומר קפיצות קטנות בגובה ביחס לממוצע באותו אזור.


הקוד:
לינק

מה הקוד בעצם עושה?
קודם כל הוא יוצר שורה בציר ה-Z, את גובה הנקודה הראשונה בשורה (0, 0) הוא מגריל, לאחר מכן כל גובה תלוי בגובה שלפניו ונוסף אליו מספר רנדומלי (0, 1, 1-).
לאחר מכן הקוד יוצר שורה בציר ה-X, שנוצרת באותה דרך כמו השורה הקודמת.


על מנת להמחיש את כל התהליך הכנתי אנימציה:



המשבצות האדומות הן השורות הראשונות בציר ה-Z וציר ה-X.
לאחר מכן מתחיל תהליך בו כל משבצת צהובה נוצרת על סמך ממוצע הגבהים בין כל 2 משבצות בצבע תכלת.

התוצאה:


הניסיון הראשון. חזק בהרים, חלש במישורים.

התוצאה נחמדה ביותר, יש בעיה עם יצירת מישורים אבל ההרים נראים מצויין.
בעיה די מציקה עם האלגוריתם היא שהוא נוטה להנמיך את המפה ככל שמתרחקים מנק' ההתחלה.
כלומר אם בנקודה (0, 0) היה גובה של 10 קוביות, בנקודה (30, 30) בדרך כלל יהיה גובה של 2 קוביות ומטה.

הבעיות האלה די הפריעו ולכן ניסיתי לחשוב על פתרון אחר.


ניסיון 2:

אם תסתכלו טוב על המפות במיינקראפט, אפשר לראות שגובה האדמה משתנה בהדרגתיות, בקו מסויים.
כלומר אם הייתה עלייה בגובה בשורה מסויימת במיקום מסויים, יש סיכוי גדול שבשורה שאחריה גם תהיה עלייה בגובה באותו מיקום ביחס לשורה וגם בשורה שאחריה וכך הלאה.
בניסיון השני ניסיתי לכתוב אלגוריתם שיזכור בכל שורה מתי היה שינוי גובה ובכמה הגובה השתנה, ובאותו הזמן יבדוק האם בשורה הקודמת השתנה הגובה ואם כן לשנות גם בשורה זאת את הגובה בהתאם.


הקוד:
לינק


כמו שאפשר לראות בקוד, האלגוריתם מכניס לתור את כל שינויי הגובה ושורה לאחר מכן בודק אם השינוי בראש התור מתאים למשבצת שמחושבת כרגע. כמובן שהוספתי לזה קצת רנדומליות.


התוצאה:
ניסיון 2. עובד טוב לרוחב אבל לא לאורך

אני לא יודע מה גרם לתוצאה להיות כל כך גרועה, אולי הרעיון לא היה טוב מההתחלה, אולי המימוש לא היה נכון...
בכל מקרה זה לא עבד, יותר מדי משבצות באותו הגובה על ציר ה-Z אך בציר ה-X זה עובד מצויין.


עברתי לניסיון הבא.




ניסיון 3:

הרעיון לניסיון הזה צץ בראש שלי בשעות שרוב האנשים ישנים.
ד"א אם יש לכם בעיית תכנות שאתם לא מצליחים לפתור, תנסו לחשוב בשעות לא שגרתיות, אפילו לפני השינה או במקלחת. אני למשל חושב הכי טוב בין השעות 1 בלילה ל-5 לפנות בוקר.

בכל מקרה הרעיון היה ליצור פשוט "גושים" של משבצות באותו הגובה, זה יפתור את הבעיית הרעש מהניסיון הראשון ואת בעיית האורך והרוחב מהניסיון השני.
אז איך ממשים את זה? בעזרת תור!
האלגוריתם מתחיל כך שנבחר גובה רנדומלי בסיסי שישמש אותנו בהמשך.
לאחר מכן מתבצע חיפוש אחרי נקודה ריקה, כלומר שאין בה משבצות ועוד לא נקבע לה גובה, הגובה בנקודה זאת הוא ממוצע הגבהים של הנקודות סביבה ובמקרה שאין נקודות סביבה (כמו לדוגמה בנקודה הראשונה) הגובה בה הוא הגובה הבסיסי שנבחר בהתחלה. לגובה הממוצע נוסף מספר רנדומלי (0, 1, 1-).
לאחר מכן מתבצעת פעולה שדומה ל-flood fill (מילוי שטחים כמו של דלי הצבע בצייר), נקבע מספר רנדומלי של משבצות שיהיו ב"גוש" באותו הגובה והחל מהנקודה הריקה שהאלגוריתם מצא מתחיל סוג של מילוי. כל נקודה מוסיפה חלק מהנקודות (ברנדומליות) שמסביבה ל"גוש" שלה. כך נוצרים מספר משטחים בלי קפיצות מוזרות בגובה.
מהאלגוריתם הזה יכולות לצאת מגוון מפות הרריות ומישוריות.

את הקוד לאלגוריתם הזה אני לא אפרסם. מי שרוצה לממש אותו אני אעזור לו בשמחה אך אני לא אוהב לתת קודים בידיעה שאנשים יעתיקו וידביקו בלי ללמוד.

והתוצאה:



ניסיון 3. אין "רעש" ואין בעיות אורך רוחב.
אם יש משהו שעושה אותי מאושר בחיים האלה זה הפתרון הזה... XD

יש עוד שני דברים לפני סיום הפוסט, הדבר הראשון הוא שהמשכתי את המערכת מעבר ליצירת האדמה בלבד:



והדבר השני, כמו בכל סיום פוסט, מוזיקה!
שיר מצויין של להקה מצויינת, אני צריך לשמוע יותר שירים שלהם.


לילה (או מה שנשאר ממנו) טוב :)

יום רביעי, 25 במאי 2011

חזרתי לעדכן

היי, 
בזמן הקרוב אחזור לעדכן את הבלוג בין בגרות לבגרות. 
יש כבר מספר פוסטים בתכנון בנושאים כמו: Unity3D, Flash, XNA ועוד..

במידה ואתם רוצים שאכתוב על נושא מסויים, תגיבו על הפוסט עם הנושא ומקורות מידע (אם יש).

לילה טוב :)

יום רביעי, 23 בפברואר 2011

מרכז מדריכים נסיוני

היי,

היום יצא לי לעבוד קצת על מרכז המדריכים. במרכז המדריכים ארכז מדריכים שכתבתי בתחומים שונים.
את מרכז המדריכים תוכלו למצוא בצד ימין של הדף אך הוא עובד חלקית ואין בו מדריכים.

בכל מקרה בימים הקרובים אוסיף מדריכים שכתבתי וסוף סוף תהיה גישה ישירה ונוחה למדריכים שכתבתי.

הפוסט הזה הוא רק עדכון קצר ולכן אין מוזיקה.
לילה טוב :)

יום שבת, 19 בפברואר 2011

Flash, האם כדאי לעבור מ-AS2 ל-AS3

היי,

נושא שלא התעסקתי בו המון זמן זה פלאש.
בפוסט הזה אני אכתוב על הסיבות לעבור מ-AS2 ל-AS3.
הסיבה היא שראיתי במספר מקומות שאנשים עדיין משתמשים ב-AS2, והם לא יודעים מה הם מפסידים.

יצא לי קצת לעבוד עם פלאש ואפילו ליצור משחק די פשוט ולפרסם אותו.

אחת משפות התכנות (לדקדקנים מביניכם - שפת סקריפט) הראשונות שלמדתי היתה Action Script 2 או בקיצור AS2.
די אהבתי את השפה הזאת מפני שקל להבין אותה ובעיקר מפני שלא הכרתי הרבה שפות מעבר לה.
את הבסיס למדתי מחוג שלקחתי כשהייתי די צעיר ואת השאר למדתי לבד.

לאחר כמה שנים טובות שבהן עבדתי עם תוכנות אחרות (בעיקר Game Maker שהיה מאוד קל לעבור אליה מהסיבה שאופן העבודה ב-AS2 מזכיר את אופן העבודה בה), משתמש מהפורום החל לעבוד ב-AS3 והמליץ על שפת התכנות הזאת.
עד אז די פחדתי מ-AS3 מהסיבה שיצא לי לראות את הפרקים הראשונים של ספר ללימוד AS3 והדבר הראשון שחשבתי עליו הוא "WTF? למה אי אפשר לכתוב לאובייקט את הקוד שהוא אמור לבצע וצריכים לעשות הכל דרך הפריים?!?!".
עם זאת די רציתי לבדוק מה השיפורים מאז AS2, שאלתי את מי שהמליץ לי על AS3 מאיפה הוא לומד והוא הפנה אותי למדריך הזה: לינק.
במדריך יש הסבר על הבסיס וגם מעבר, בהחלט ממליץ עליו.
למדתי המון מהמדריך, עם הזמן יצא לי לפרסם משחק שכתבתי ב-AS3 ואף יצא לי לפרסם מדריך בעברית שדי מזכיר את המדריך שלמדתי ממנו.

 תמונה מהמשחק:

(את המדריך אפרסם בבלוג כשאמצא דרך ליצור ספריית מדריכים נורמלית).


למה Action Script 2?

פשטות - כשאני מנסה להזכר למה אהבתי את AS2 זה היה בגלל הפשטות.
כל מה שהייתי צריך למשחקים שאני יוצר היה שליטה על המיקום (X, Y), בדיקת התנגשות ובדיקה של מצב המקלדת (כלומר איזה כפתורים נלחצו).
התחביר לא היה נוקשה במיוחד, בעיקר בגלל שהיה ניתן ליצור אובייקט ולגשת אליו בקלות.
 לדוגמא:
duplicateMovieClip(_root.MyObject, "MyObject1", this - 1);
_root.MyObject1._x = _root._xmouse;

ברוב השפות הקומפיילר היה מנסה להרוג אותי בגלל השורה השנייה, מפני שלא הגדרתי משתנה בשם MyObject1, עם זאת זה מאוד אינטואיטיבי לעבוד ככה ולא ראיתי עם זה שום בעיה לפני שהכרתי צורות תכנות אחרות.

ברוב המקרים גם אין צורך ללמוד על מחלקות או תכנות מונחה עצמים, דבר שמפשט גם הוא את העבודה.


זמן פיתוח בפרוייקטים קטנים - בפרוייקטים קטנים אין צורך להשתמש בקוד מסודר במיוחד, במחלקות לכל דבר וכו'.
ב-AS2 ניתן להצמיד לאובייקט הרצוי קוד מסויים כך שיתנהג כמו שציפינו וזהו, לא צריך להתעסק בפרוייקט מעבר לזה.



אז למה בכל זאת Action Script 3?

מהירות - חיפוש קצר באינטרנט מראה שהרצת קוד AS3 מהירה יותר מ-AS2 פי כמה ברוב המקרים, בחלק מהמקומות תמצאו שזה פי 10-15 ובחלקם פי 2.
זה שיפור משמעותי שצריך לקחת בחשבון כשמפתחים משחק פלאש.

נוחות - AS3 היא שפה OOP, כלומר מונחית עצמים. אופן העבודה איתה הרבה יותר נוח ומסודר בפרוייקטים גדולים, במיוחד כאשר עובדים בצוות וכל אחד אחראי על מחלקה משלו.
בנוסף, למתכנתים בשפות עיליות כגון C#, C++, JAVA וכו' יהיה הרבה יותר קל להתרגל ל-AS3, מפני שאופן העבודה בהן כמעט זהה.

תמיכה - אני מניח שבתור השפה העדכנית ביותר, AS3 זוכה לתמיכה המירבית כרגע. זה אומר שמתפרסמים הרבה מדריכים, תוכלו לקבל עזרה וכמובן שהפיתוח של פלאש יתבטא בין היתר בהרחבה של AS3.
דוגמא טובה היא תמיכה בהאצת חומרה ו-3D ב-AS3. 
כמובן שכל זה בגדר השערה אך סביר מאוד שזאת גם המציאות.




לסיכום, AS2 שפה פשוטה. אם אתה גרפיקאי שמעצב בפלאש ורוצה להוסיף מעט סקריפטים לסרטון, AS2 עשויה להתאים לך.
AS3 שפה מורכבת, נוחה, מסודרת ועדכנית. אם אתה מתכנת שמתכנן ליצור פרוייקט גדול בפלאש, משחק מורכב או תוכנה אז כמובן ש-AS3 זאת השפה בשבילך.
אם אין לך רקע קודם בפלאש אני ממליץ בכל מקרה ללמוד AS3.

עד כאן הפוסט אבל כמובן מוזיקה!

באפריל Megadeth באים לארץ! כבר יומיים שאני שומע שירים שלהם ביוטיוב.
אחד השירים היפים זה Tornado of Souls:


שיהיה לנו שבוע מצויין! D:


יום ראשון, 6 בפברואר 2011

Global Game Jam 2011

היי,

אז כמו שכבר אמרתי בפוסט הקודם, בפוסט הזה אני אספר על הכנס Global Game Jam.
השנה יצא לי להשתתף בכנס וזאת חוויה כיפית ששווה את הזמן והמאמץ.



הכל התחיל לפני חודש וחצי בערך.
בזמן האחרון אני עובד על משחק עם צוות קטן שיצא לי להכיר דרך האינטרנט, את הצוות לא יצא לי לפגוש ואחד מחברי הצוות הציע לי שאבוא לכנס כדי שנוכל להפגש.
אז לאחר הרבה התלבטויות החלטתי ללכת על זה ולהגיע לכנס.
ביום של הכנס התעוררתי מאוחר (אבא שלי לא העיר אותי בזמן) והגעתי באיחור של 3 שעות כך שהפסדתי את החלק של הצגת הרעיונות והחלוקה לקבוצות.
די מהר מצאתי את מי שהציע לי לבוא לכנס (שלא פגשתי אותו עד אותו יום) ואת שאר הצוות שלי (הצוות שאני מפתח איתו משחק בלי קשר לכנס), לבסוף הצטרפתי לקבוצה בה היו 2 מחברי הצוות.
הייתה לנו קבוצה ענקית שכללה מתכנתים וגרפיקאים מאוד מוכשרים ולכן היינו צריכים ליצור חלוקת עבודה מסודרת.

בשלב הזה אני אסביר על איזה משחק הקבוצה שלי עבדה, הרעיון של המשחק הוא שיש ארנבים שכל הזמן מתרבים (סביר להניח שגרמנו לפיבונאצ'י להתהפך בקברו), אתה צריך לנסות להשמיד אותם לפני שהם יתרבו יותר מדי ע"י הרעלת הגזרים שהם אוכלים, את הרעל ניתן לאסוף מהרצפה.
הצוות החליט ליצור את המשחק ב-Unity3D, שאני עובד איתו די הרבה זמן. למי שלא מכיר זה מנוע משחק מקצועי המציע גם גרסה חינמית שכוללת המון אפשרויות. עוד על Unity3D תוכלו לקרוא באתר שלהם וגם ב-FAQ שכתבתי: לינק ל-FAQ .
את המשחק ייצאנו ל-Web Player כך שניתן להריץ אותו מהדפדפן (ממש כמו משחקי פלאש).

לאחר שחילקנו את התפקידים (אני הייתי אחראי על השחקן) ולאחר שהתגברתי על קשיים טכניים רבים (חוסר במחשבים, והמחשבים שהיו לא הפעילו טוב את התוכנות) התחלתי לעבוד.
מאותו רגע עבדתי כמעט ברציפות (מלבד שינה ואוכל) עד 8 בערב למחרת, בזמן הזה הספקתי לעבוד על מערכת תזוזה עם מצלמה (ואז גיליתי שהסקריפטים של יוניטי יותר טובים אז החלפתי את הסקריפטים שלי), אינטרקציה של השחקן עם אובייקטים אחרים (גזרים וערכות רעל), אנימציה לשחקן, הצגת מספר ערכות הרעל על המסך וטקסט זז כאשר נמצאים בטווח של גזר שניתן להרעיל ואפילו על משגר טילים.
משגר הטילים לא הוכנס לבסוף למשחק, כאשר יצרתי את הטילים השתמשתי בקפסולה והכל עבד טוב אבל לאחר שהחלפתי את הקפסולה למודל השתבשו צירי הסיבוב של הטיל והוא התחיל להתנהג מאוד מוזר. הצלחתי לסדר את הבעיה הזאת בסוף אבל כבר היה מאוחר מדי והגיע הזמן להציג את המשחקים.

 המשחקים שנוצרו בכנס היו ממש מגניבים. רוב הקבוצות הגיעו למצב כמו הקבוצה שלנו, משחק כיפי שאפשר לשחק בו אבל הוא לא מושלם. בנוגע לכלי הפיתוח, הרבה משחקים נוצרו ב-Unity וב-Flash, היו כמה שנוצרו ב-HTML5, משחק אחד שנוצר לאנדרואיד (מבוסס על חיישני תנועה) ומשחק לאייפון (מבוסס מיקום וקורא ברקודים), זה מה שאני זוכר.

אז לסיכום, היה ממש כיף ואני מקווה להשתתף בשנה הבאה (למרות שאני חושב שאני אהיה בצבא עד אז).
למדתי איך לעבוד בצוות, למדתי דברים חדשים על יוניטי, עבדתי עם אנשים נחמדים וזה העביר לי סוף שבוע שלם בכיף.
כמובן שגם יצאתי משם עם משחק!

תמונה מהמשחק (הארנב הירוק זה זומבי שנוצר כתוצאה מאכילה של גזר מורעל, הוא אוכל את שאר הארנבים):

המשחק מופיע בעמוד באתר של Global Game Jam -

וכדי לשחק במשחק עצמו -

עד כאן הפוסט, אבל קודם כל מוזיקה! D:
שיר שממש אהבתי מהאלבום החדש של Disturbed, הלהקה האהובה עלי!

לילה טוב :)

יום חמישי, 3 בפברואר 2011

מי אמר שמתמטיקה לא יכולה להיות יפה?

היי,

זה הפוסט הראשון בבלוג שלי שיכיל תוכן.
תכננתי לכתוב על Global Game Jam 2011 בישראל אבל ברגע האחרון החלטתי להחליף נושא.

היום דיברתי עם חבר שלי במסן על שההורים שלי החליטו להעיר אותי כשנרדמתי אחרי שחזרתי מבית הספר ואז אמרתי לו שלא הצלחתי לחזור להרדם.
הוא אמר שזה מזכיר לו את אפקט הפרפר ותורת הכאוס (אל תשאלו אותי איזה ספרים הילד קורא...), ואז הוא שלח לי לינק לויקיפדיה על כאוס ולאחר מכן על קבוצת מנדלברוט.
קבוצת מנדלברוט היא אוסף של נקודות המהוות פרקטל (פרקטל - צורה החוזרת על עצמה בקני מידה שונים, בחלק מהפרקטלים משתנה מעט הצורה).
 הגדרת הקבוצה היא כל הנקודות שעבורן הסדרה: zn+1 = zn2 + c חסומה (כלומר - יש ערך כלשהו שהיא לעולם לא תעבור אותו). z הוא מספר מרוכב, כלומר הוא כולל חלק ממשי וחלק לא ממשי וכך גם C. ממש לא בא לי להכנס לכל ההסבר המתמטי של מספרים מרוכבים, אז מי שלא הבין שיעבור למתמטיקה 5 יחידות או שייאלץ ללמוד לבד. :)
הדבר שכל כך יפה בקבוצה הזאת הוא שאם משחקים טיפה עם הפונקציה ניתן לקבל תמונות מדהימות.

לקריאה נוספת תוכלו למצוא מידע בויקיפדיה: לינק לערך בויקיפדיה . (בסוף העמוד יש גם מחולל שיוצר תמונה המציגה את קבוצת מנדלברוט אונליין, מומלץ ביותר!)
ולכל אלה שמתעצלים לקרוא, תמונה מרהיבה שתוכלו למצוא גם בעמוד של הערך: התמונה.
ועוד תמונה:

אז נחזור לשיחה שלי במסן, חבר שלי שלח לי קוד פסאודו שהוא מצא איפשהו ורציתי לבדוק האם הקוד יצור את הצורה הרצויה.
פתחתי פרוייקט XNA חדש והתחלתי לכתוב, בהתחלה התמונה הייתה קטנה ולא ברורה (6 פיקסלים בצבעים מוזרים) אבל לאחר שסידרתי קצת את ההגדרות של המיקום, קנה המידה והגדרת הצבעים ראיתי את התמונה הבאה:

נחמד, נכון?
שיחקתי עוד קצת עם ההגדרות והגעתי לתוצאה הזאת:

נראה מוכר, לא?
אז יש עוד מקום לשיפור, בעיקר בנוגע לצבעים שנראים בכלל לא אחידים כאשר עושים מספיק זום.

אז קוד הפסאודו למי שמעוניין:

והקוד שכתבתי ב-XNA (מכיל את יצירת התמונה בלבד, ללא הציור למסך):

כאן תם הפוסט השני בבלוג, מקווה שנהנתם.
בפוסט הבא אני מקווה שיצא לי לכתוב על Global Game Jam כמו שתכננתי.

ואיך אפשר לסיים בלי מוזיקה טובה?
אחד השירים הכי טובים של Linkin Park, אני כל כך רוצה שהם יחזרו ליצור מוזיקה טובה כמו פעם :(


 
אז שיהיה לכולנו לילה טוב! :)




יום רביעי, 2 בפברואר 2011

הגיע הזמן שגם לי יהיה בלוג :)

היי,

אני לא בטוח כמה אנשים באמת הולכים לקרוא בבלוג הזה אבל בכל זאת הרגשתי שאני צריך לפתוח בלוג.
הבלוג הזה לא יספר על החיים האישיים שלי אלא זה יהיה בלוג תכנות עם דגש על פיתוח משחקי מחשב.

אני מקווה שבקרוב אפרסם בבלוג את כל המדריכים שכתבתי עד היום בנושאי פיתוח משחקי מחשב (ויש הרבה כאלה), בנוסף למערכות, סקריפטים וקוד מקור.

אז.. עוד לא הצגתי את עצמי, אני יוני והרבה אנשים מכירים אותי לפי הניק Dark Tornado.
התחלתי לפתח משחקי מחשב (טוב נו.. סתם להתעסק עם RPG Maker) לפי כ-5 שנים בערך, בתכנות אני עוסק כ-3 שנים.

אם יהיה לי זמן יום אחד אני אפרט בדיוק איזה שפות אני יודע, באיזה שלב עברתי מתוכנה לתוכנה וכו'.. אבל השעה כבר 1 ומחר אני לומד (והרבה =\) אז...

לילה טוב :)


החלטתי שכל פוסט יסתיים בשיר מהפלייליסט שלי או סתם שיר שמצאתי ביוטיוב והתלהבתי ממנו.
השיר Evolution של Korn, שיר מצויין לכל שעה של היום ולכל מצב רוח!