תכנות לילדים באמצעות Game Maker/משחק מרובה משתתפים
מתוך ויקיספר, אוסף ספרי הלימוד והמדריכים החופשי.
אין לי כוח לחלק לקטעים אבל תהנו!
המדריך המקורי: http://www.fxp.co.il/showthread.php?t=2634732
מדריך ל39DLL
מה זה 39DLL ולמה הוא משמש?
39DLL הוא קובץ DLL שתוכנת בשפת C++ על ידי 39ster, חבר בקהילה הרשמית של Game Maker. ה DLL הזה מאפשר ל Game Maker להשתמש ב Sockets של Windows וכך בעצם מאפשר לנו להשתמש בפעולות רשת רבות, וליצור חיבורים למחשבים אחרים, אבל לפני הכל - איזה סוג חיבור נבחר?
ישנם שני סוגי התחברות: TCP ו- UDP לכל סוג יש את היתרונות והחסרונות שלו.
אחד היתרונות החשובים של חיבור מסוג TCP הוא שהוא חיבור אמין, ומה שאתם תשלחו לשחקנים האחרים דרכו יגיע כמו ששלחתם, בצורה תקינה, ובאותו הסדר ששלחתם.
החסרון בשימוש ב TCP הוא ש TCP הוא חיבור מאוד איטי, ולכן כדאי להשתמש בו רק בשליחות חשובות, ולא בשליחות שמתבצעות כל הזמן, כמו שליחת מיקום השחקן, X ו- Y שלכם לשחקנים האחרים.
לעומת חיבור ה TCP, אחד היתרונות של חיבור מסוג UDP הוא שהוא חיבור מהיר ביותר. בהשוואה ל TCP, חיבור מסוג UDP עובד במהירות וביעילות רבה יותר מה שמבטיח משחקיות חלקה.
מצד שני גם לחיבור UDP יש חסרון גדול. בניגוד לחיבור מסוג TCP, חיבור מסוג UDP אינו אמין במיוחד. לעיתים יכול לקרות שאתם שולחים משהו לשחקנים האחרים, אבל מה ששלחתם לא הגיע, או שהשליחה הגיעה, אבל לא בשלמותה או לא באותו הסדר ששלחתם אותה, ולכן כדאי להשתמש בחיבור UDP רק בשליחות מתחדשות שמתבצעות הרבה פעמים, כל פריים, כמו למשל שליחת מיקום השחקן, X ו- Y שלכם, מה שעדיף לא לעשות בחיבור מסוג TCP, כי אפילו אם שליחה אחת הגיעה לא בסדר ואפילו אם השליחה לא הגיעה כלל, שליחה נוספת תגיע מיד לאחריה ותתקן את הבעיה.
חסרון גדול נוסף ב UDP הוא שכדי להשתמש בסוג החיבור הזה, על השחקנים לתאם את חומת האש שלהם שתאפשר להם להתחבר באמצעות UDP לפורט מסויים.
עכשיו כשאתם יודעים מה ההבדל בין החיבורים, באיזה חיבור כדאי לבחור? התשובה היא - שניהם! אין שום דבר שמונע מכם להינות מהיתרונות של TCP ו- UDP ביחד חוץ מרמת העצלות וההבנה שלכם, אז רק שתדעו מראש - המדריך הזה הוא לרציניים בלבד!
איך מתחילים?
לפני שאני אסביר איך לעבוד עם ה DLL ואספק הסבר על הפקודות, אתם צריכים לדעת כיצד מאתחלים את ה DLL כדי שנוכל להשתמש בו. הורידו את ה DLL מכאן:
לאחר מכן שימו את ה DLL בתיקיית המשחק שלכם. כמו כן יש לכם קובץ עם סיומת GML. זהו קובץ שמכיל את הסקריפטים שבאמצעותם מתקשרים עם ה DLL. לכו ל Game Maker ותבחרו בלשונית Scripts את Import Scripts ותבחרו את קובץ ה GML וזה ייטען לכם את הסקריפטים אוטומטית לתוך תיקייה בשם 39DLL (אני חושב).
פקודת אתחול ה DLL היא:
קוד: dll_init(0, true, true);לפקודה הזאת קוראים פעם אחת בלבד בתחילת המשחק, ורק אז תוכלו להשתמש בשאר הפקודות של ה DLL.
עכשיו כשאתחלנו את ה DLL, אני יכול להסביר על הפקודות החשובות של ה DLL. אני אלמד אותכם איך יוצרים חיבור חדש, מה המאפיינים שצריכים להיות לחיבור, איך שולחים שליחה ואיך מקבלים שליחה.
הסבר על הפקודות המרכזיות ב39DLL:
אני אסביר אך ורק על הפקודות המרכזיות והחשובות!
קצת על חיבורים
כדי ליצור חיבור עליכם לספק את הפרטים הבאים: IP, Port, Blocking Mode. IP - זה כתובת ה IP של המחשב שאליו אתם מנסים להתחבר. זה גם יכול להיות קישור לאתר, אבל עדיף שתספקו IP תקני. IP זה מחרוזת (טקסט) שנראית כך: XXX.XXX.XXX.XXX. כדי לדעת מה ה IP שלכם לכו לאתר: www.whatismyip.com.
Port - זה כמו מספר ה"שער" שדרכו אתם יוצרים את החיבור. אם יש לכם ראוטר ואתם רוצים לקבל חיבורים חדשים, אתם צריכים לפתוח את מספר הפורט שבחרתם כדי שייאפשר חיבורי TCP ו UDP.
Blocking Mode - זו היא צורת ההתחברות שלכם, אם לעשות חיבור רגיל, חיבור מוגן ומסונכרן, או חיבור רגיל שלא מחכה עד שהסרבר יגיב. Blocking Mode זה מספר מ0 עד 2. 0 - חיבור רגיל, 1 - חיבור מוגן ומסונכרן ו2 זה חיבור רגיל שלא ממתין לסרבר. אני ממליץ לכם להשתמש בחיבור מסוג 0 או 1 (אני משתמש בדרך כלל ב 1).
על מנת להתחבר למחשבים אחרים עליכם להשתמש בפעולת ההתחברות:
קוד: tcpconnect(ip, port, blocking_mode);לדוגמא:
קוד: global.serversocket = tcpconnect("192.116.115.227", 12345, 1);כאן אנחנו מתחברים ל IP הזה: 192.116.115.22, דרך פורט מספר 12345 וסוג ההתחברות שלנו היא התחברות מוגנת ומסונכרנת. הפקודה הזאת תיצור חיבור, תפתח Socket ותחזיר את המספר הסידורי של החיבור. אנחנו נשתמש במספר הזה בשביל לשלוח ולקבל דברים מהסרבר שאליו אנחנו מתחברים.
מהצד השני, אתם צריכים לבנות שרת שאמור להמתין ולקבל את החיבורים החדשים, אחרת אתם סתם תנסו להתחבר למחשב שלא ייקבל אותכם. אז, כדי ליצור סרבר שממתין לחיבורים חדשים עליכם להשתמש בפקודה
קוד: tcplisten(port, maximum_connections, blocking_mode);הפרמטר הראשון, port, זה איזה שער לפתוח לחיבורים. הפרמטר השני, maximum_connections אומר כמה חיבורים יכולים להתחבר בו זמנית למחשב דרך הport שציינתם. אם נוצר חיבור אבל אין בשבילו מקום, החיבור ייכנס למעין רשימת ממתינים, וכשייתפנה מקום, הסרבר ימשיך לטפל בו.
הפקודה הזאת תחזיר לכם את המספר הסידורי של ה Socket שפתחתם, אתם לא צריכים להבין בדיוק מה זה עושה, אבל אתם חייבים לשמור את המשתנה הזה, כי אתם תשתמשו בו בהמשך.
דוגמא לשימוש בפקודה:
קוד: global.server = tcplisten(12345, 2, 1);פה עשיתי שהפקודה תמתין לחיבורים בשער מספר 12345. מספר החיבורים המקסימלי האפשרי הוא 2, כלומר משחק ל3 שחקנים (2 לא כולל את עצמכם), וסוג החיבור הוא 1, כלומר חיבור מוגן ומסונכרן.
עכשיו פתחנו את השער, ואנחנו יכולים להתחיל לקבל חיבורים, אבל איך נדע מתי אנחנו מקבלים חיבור? ואיך נדע לטפל בחיבורים האלה? בשביל זה יש לנו את הפקודה:
קוד: tcpaccept(socket, blocking_mode);הפרמטר הראשון, socket, זה המספר הסידורי של ה Socket שפתחתם באמצעות הפקודה הקודמת - tcplisten. הפקודה מחזירה מספר סידורי של חיבור. אם אין חיבור, יוחזר מספר שלילי או 0, ואם זוהה חיבור חדש, הפקודה תחזיר מספר סידורי חיובי של החיבור. המספר הזה מייצג את מספר ה Socket שדרכה נבצע תקשורת בין מי שהתחבר לסרבר שלנו.
דוגמא לשימוש בפקודה:
קוד: global.client = tcpaccept(global.server, 1);הפקודה הזאת ממתינה לחיבורים מה Socket שקיבלנו בדוגמא של הפקודה הקודמת ומכניסה את מספר החיבור למשתנה גלובאלי בשם client.
פקודה שימושית נוספת היא פקודה שסוגרת חיבור מסויים, כלומר סוגרת Socket נתון. ככה נראית הפקודה:
קוד: closesocket(socket);socket זה משתנה שמכיל את המספר הסידורי של החיבור שקיבלנו מהפקודה tcpaccept או tcpconnect. ברגע שסגרתם את הSocket שלכם, אתם לא תקבלו יותר מסרים, ולא תוכלו לשלוח שוב דבר באמצעות החיבור. __________________
יצירה, שליחה, וקבלה של מסרים ברשת:
על מנת ליצור תקשורת כלשהיא בין השחקנים, אתם צריכים לשלוח נתונים חשובים לשחקנים האחרים כמו המיקום שלכם, השם שלכם, וכ'ו, אבל איך עושים את זה?
אני אנסה להמחיש לכם מה זה מסר. מסר זה בעצם כמו רשימת קניות. כל שורה מכילה נתון אחר שאתם רוצים לשלוח או לקרוא. אחרי שסיימתם לכתוב את רשימת הקניות שלכם, אתם פשוט שולחים אותה לשחקן האחר, והשחקן האחר מהצד השני אמור לקרוא אותה ולבצע אותה.
אבל יש בעיה קטנה...
במהלך המשחק, אתם תצטרכו לשלוח הרבה סוגים שונים של מסרים: מסרים שמכילים את המיקום שלכם מסרים שמכילים את השם שלכם מסרים שמכילים את המראה שלכם במשחק מסרים שמכילים את אחוז השומנים שלכם וכ'ו.
הבעיה היא שכשאתם באים לקבל מסר ששלחתם, איך תדעו מה אתם אמורים לקבל? איך תדעו אם המסר שאתם מקבלים מכיל את שם השחקן או המיקום שלו? בשביל זה לכל מסר יש מספר מזהה משלו, בראש כל רשימת קניות כזאת יש כותרת שאומרת מה היא מכילה, וכך כשאנחנו קוראים את הרשימה, אנחנו נדע איך לטפל בה לפי מה שכתוב בשורה הראשונה שלה, הכותרת שלה.
למשל מסר עם מספר מזהה 0 אומר שהוא מכיל מידע לגבי מיקום של שחקן מסויים מסר עם מספר מזהה 1 אומר שהוא מכיל מידע על שם השחקן מסר עם מספר מזהה 2 אומר שהוא מכיל מידע על המראה של השחקן, וכ'ו.
איך שולחים מסר? על מנת לשלוח מסר עלינו: 1. להכין את המסר לשליחה 2. לכתוב את כל מה שאנחנו רוצים לשלוח לרשימה 3. לשלוח את המסר
לפני שליחת כל סוג של מסר, עלינו לקרוא לפקודה:
קוד: clearbuffer();הפקודה הזאת תכין מסר לשליחה. עכשיו אנחנו צריכים לכתוב את המסר עצמו.
למסר אפשר לכתוב הרבה סוגי נתונים: בייטים (byte), מספרים שלמים (short), מספרים שלמים גדולים (int), מספרים עשרוניים(float), מספרים עשרוניים גדולים (double), אותיות (chars), טקסט (string) ועוד.
מה ההבדל בין סוגי המספרים? לכל קטגוריה יש טווח מספרים שהיא מסוגלת לטפל, וגודל הזכרון שהיא תופסת במסר. בייטים הם מספרים שלמים מ0 עד 255, והם צורכים מעט מאוד זכרון, ולכן אנחנו נשתמש בהם בשביל זיהוי סוג המסר. כשאנחנו כותבים מסר, השורה הראשונה שנכתוב בו תהיה מספר בייט שיכיל את המספר המזהה של המסר.
מספרים שלמים (short) הם מספרים בטווח מינוס 32768 עד ל32768. בדרך כלל נשתמש בהם בשביל לשלוח מספרים שיכולים להיות גדולים כמו ערכי X ו Y. במקרים נדירים נאלץ להשתמש המספר שלם ארוך (int) שיכול להכיל מספרים ממינוס 2147483648 עד 2147483648. ההבדל בין מספרים מסוג short למספרים מסוג int הוא שמספרים מסוג short צורכים הרבה פחות זכרון ולכן תמיד עדיף לשלוח short ולא int אם אפשר.
מספרים עשרוניים (float) הם מספרים עשרוניים, כמו short רק שיכולים להיות לא שלמים. מספרים עשרוניים גדולים (double) הם מספרים עשרוניים כמו int רק שיכולים להיות לא שלמים. גם כאן, ההבדל בין המספרים העשרוניים הוא הגודל שהם תופסים בזכרון. עדיף להשתמש בfloat מאשר double אם אפשר.
כמו כן אפשר לשלוח טקסטים, ולא רק מספרים. אפשר לשלוח רצף אותיות (chars) אבל אני תמיד שולח מחרוזת רגילה (string). אני מניח שההבדל היחיד הוא הזכרון אבל אל תתפסו אותי במילה, אני לא בטוח בעצמי.
אז איך כותבים נתונים למסר? אתם כותבים את המילה write וישר אחריה את סוג הנתון שאתם רוצים לשלוח, לדוגמא short ואחר כך בתוך סוגריים עגולות רגילות את הערך שאתם רוצים לשלוח, לדוגמא:
קוד: writeshort(0);זה יכתוב לרשימה של המסר מספר שלם מסוג short בעל ערך 0. אתם יכולים לכתוב כמה נתונים במסר אחד, ולאחר שאתם תסיימו לכתוב את המסר, אתם צריכים לשלוח אותו. פקודת שליחת המסר היא:
קוד: sendmessage(socket);כשSocket זה מספר מזוהה של החיבור שיצרתם. לדוגמא אנחנו יכולים להשתמש בפקודה כך:
קוד: sendmessage(global.serversocket);כשהמשתנה global.serversocket הוא אותו משתנה שאליו קיבלנו את המספר המזוהה של החיבור לשרת, כפי שניתן לראות בדוגמאות שנתתי למעלה.
דוגמא לשליחת מסר מלאה:
קוד: clearbuffer(); writebyte(0); writestring("Yay message!!1 ZOMG!!1"); writestring("Yay!!1 Another message!!1 ZOMG!!1"); sendmessage(global.serversocket);שורה ראשונה אנחנו מכינים מסר לשליחה שורה שנייה אנחנו כותבים את המספר המזוהה של המסר, כדי שהסרבר יידע איך הוא צריך לקרוא את המסר שהוא קיבל. שורות שלוש וארבע שולחות שני טקסטים. והשורה האחרונה, השורה החמישית שולחת את המסר לחיבור global.serversocket.
עכשיו אנחנו צריכים לטפל בצד השני, ולראות איך אפשר לקבל ולקרוא מסר. איך מקבלים מסר? אם שרדתם עד עכשיו את המדריך, אתם יודעים איך לשלוח מסר, ואתם יודעים איך מסר בנוי. עכשיו אתם צריכים לקבל את המסר, לקרוא אותו, ולבצע את הפעולות המתאימות. לכל חיבור, Socket, שיש לכם אתם יכולים לשלוח מסרים ולקבל מסרים, אז כדי לקבל מסר אנחנו קודם כל צריכים לדעת מאיזה חיבור לקבל את המסר. בשביל זה יש לנו את הפקודה הזאת:
קוד: messagesize = receivemessage(socket);socket זה משתנה המכיל את המספר המזהה של החיבור שלנו. הפקודה הזאת מחזירה את מספר הבייטים שקיבלנו, כלומר אם נשלח לנו מסר, הפקודה תחזיר את מספר הבייטים של המסר, כלומר את המשקל שלו. אם לא קיבלנו כלום, הפקודה תחזיר 0 בגלל שקיבלנו 0 בייטים, ואם כן קיבלנו משהו, אז הפקודה תחזיר מספר חיובי גדול מ0.
אז לפני שאנחנו מנסים לקרוא מסרים אנחנו צריכים לבדוק אם בכלל יש לנו מסרים חדשים. ככה תראה הבדיקה:
קוד: while (true) {
messagesize = receivemessage(socket);
if (messagesize > 0)
{
//start reading the message!
}
else
{
break;
}
}הסבר קצרצר: אני לא אסביר על ה GML עצמו, אלא רק מה הוא עושה. מטרת הקוד הזה הוא לבדוק אם התקבל מסר חדש, וכל עוד יש מסר חדש צריך לקרוא אותו ולטפל בו. אם אין מסרים חדשים, צריך לעבור הלאה. הקוד הזה מבטיח לנו שאפילו אם נקבל כמה מסרים באותו הפריים, נוכל לטפל בכולם באותו הפריים וכך לא יווצר לאג.
אנחנו נקרא ונטפל במסרים שאנו מקבלים בין הסוגריים המסולסלות שבשורות 5 עד 7, שם אנחנו נכתוב את הקוד שלנו.
כמו שאתם זוכרים, למסר שלנו יש צורה, פורמט קבוע שאומר שהשורה הראשונה שלו תגיד לנו בעצם מה הוא אמור להכיל. זה כדי שאנחנו נוכל לקרוא ולבצע את המסר כהלכה, אז קודם כל אנחנו נקרא את מה שיש בשורה הראשונה, אבל לפני זה, איך אנחנו בכלל יכולים לקרוא מה ששלחנו?
זוכרים את פקודות ה write שלנו? לדוגמא writebyte או writeshort וכל אלה? בשביל שנוכל לקרוא את הנתונים שקיבלנו במסר אנחנו פשוט נצטרך להשתמש בפקודות read במקום write, לדוגמא readbyte בשביל לקרוא byte, או readshort בשביל לקרוא short.
המסר שלנו בנוי כמו רשימה, זה אומר שאנחנו נאלץ לקרוא את המסר באותו סדר שבו כתבנו אותו, כלומר אם קודם כתבנו משתנה מסוג byte ואז כתבנו משתנה מסוג string ואז משתנה מסוג short אנחנו קודם נקרא byte, לאחר מכן נקרא string ולבסוף short, כי זה הסדר שבו כתבנו את המסר, וזה הסדר שעלינו לקבל אותו.
בואו ניקח לדוגמא את המסר הזה:
קוד: clearbuffer(); writebyte(0); writeshort(obj_player.x); writeshort(obj_player.y); sendmessage(global.serversocket);המספר המזהה של המסר הוא 0, והוא נשלח כמשתנה מסוג byte. המסר מכיל את ה X ו Y של השחקן שלנו, והם נשלחים כמשתנים מסוג short.
כשאנחנו נקרא את המסר הזה אנחנו קודם נקרא את המספר המזהה שלו, אנחנו נקרא משתנה מסוג byte, ולאחר מכן נבצע עוד שתי קריאות למשתנים מסוג short. אחד בשביל ה X ששלחנו ואחד בשביל ה Y ששלחנו.
אז כדי שנוכל לקרוא מסרים, דבר ראשון שנצטרך לעשות הוא לקרוא את המספר המזהה שלהם כדי לדעת איך לקרוא את ההמשך של המסרים ולכן נשתמש בפקודה:
קוד: messageid = readbyte();הכנסנו לתוך משתנה messageid את המספר המזהה של המסר שלנו. עכשיו אנחנו נצטרך להמשיך לקרוא את המסר בהתבסס על הערך של messageid. הדרך הכי נוחה לעשות את זה היא להשתמש במבנה switch, בצורה כזאת:
קוד: switch (messageid) {
case 0:
{
break;
}
case 1:
{
break;
}
}בתוך case 0 אנחנו נכתוב את מה שייקרה אם messageid שווה ל0, ובתוך case 1 אנחנו נכתוב את מה שייקרה אם messageid שווה ל 1 וכן הלאה. אתם יכולים להוסיף כמה cases שאתם רוצים.
אזזז, לפי המסר שקיבלנו, אם המספר המזהה שלו הוא 0, זה אומר שהוא מכיל את ה X ו Y של השחקן שלנו, ולכן נשים בתוך case 0 משהו כזה:
קוד: obj_player_other.x = readshort(); obj_player_other.y = readshort();והקוד המלא שלנו ייראה בערך ככה:
קוד: קוד: while (true) {
messagesize = receivemessage(socket);
if (messagesize > 0)
{
messageid = readbyte();
switch (messageid)
{
case 0:
{
obj_player_other.x = readshort();
obj_player_other.y = readshort();
break;
}
case 1:
{
break;
}
}
}
else
{
break;
}
} את הקוד הזה אנחנו שמים ב STEP של האובייקט שאמור לקבל את המסרים, ואת האובייקט הזה לשים בחדר המשחק שלכם וכך תוכלו לקבל ולטפל במסרים כל פריים D:
רק תזכרו, מתי שאתם יוצאים מהמשחק, או רוצים להתנתק מהרשת אתם חייבים לקרוא לפקודה:
קוד: dllfree();הפקודה הזאת תוציא מהזכרון את ה DLL כדי שתוכלו להשתמש בו בפעם הבאה.
תכלס זה כל הבסיס של 39DLL. כמובן שיש עוד המון המון פקודות שונות ומגוונות, אבל עכשיו כשיש לכם את הבסיס תוכלו להתנסות כבר בעצמכם.
דוגמא למשחק פשוט ב39DLL למי שלומד יותר טוב דרך דוגמאות, כמוני, הכנתי לכם משחק סופר פשוט ב39DLL:
קרדיט ליריב מ-FXP על המדריך המפורט