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

![]()
כולם קולטים את גודל הרגע?!

![]()
טוב, לפחות ניסינו.
אגב, כמה זה תשנ"ב במספרים?
טוב, בואו ניגש אם כך לעניינים. המטרה בשיעור זה היא להדגים לכם על קצה במזלג (ובצורה לא הכי יעילה האמת) דרך אחת בה ניתן להוסיף שלבים שונים למשחק שלנו. הרעיון הוא לגרום לכם לפתוח את הראש גם לגבי משחקים מסוג אחר, שאותם אולי תרצו לפתח. הדגש הוא סביב נושא השלבים (Level-ים) ואני אשמח לנסות לענות לשאלות בנושא זה באשכול השיעור בפורום הפיתוח לאנדרואיד.
מערכים (Arrays) ב- java
רגע לפני שמתחילים, נושא סופר חשוב בעולם התיכנות, למען אלה מכם שאינם מכירים. הנושא הוא מערכים.
לעיתים יש לנו צורך להגדיר קבוצה גדולה של משתנים מאותו טיפוס, אך הקבוצה היא כל כך גדולה שאין הגיון בהגדרת משתנים רבים באופן תוכנותי.
דוגמא נוספת היא כשאנחנו רוצים להגדיר קבוצה גדולה של משתנים ולהזין את הערכים באופן אוטומטי (לדוגמא, בעזרת לולאה).
כרגע, אין לנו דרך נוחה לעשות זאת ועלינו להגדיר משתנה משתנה ולהזין לכל אחד מהם ערך בנפרד.
בוא ניתן דוגמא. את משחק הפינג פונג שלנו אנו רוצים להפוך למשחק "שובר לבנים" וכדי לעשות את זה, אנחנו צריכים להוסיף לו לבנים ![]()
כל לבנה נצטרך לצייר, לבדוק האם הכדור פגע בה ואם כן, אז למחוק אותה.
נניח שאנחנו רוצים להוסיף 10 לבנים ב 4 שעורות. אם כל לבנה זה משתנה, זה אומר שאנחנו צריכים להגדור 40 משתנים שונים!
הגדרה של כל משתנה בנפרד היא דרך מאד לא יעילה. אז מה עושים? משתמשים במערכים!
מערך הוא בעצם אוסף של משתנים מסוג מסויים, אשר מוגדרים כרשימה ושלכל אחד מהם יש מספר המייצג את מיקומו ברשימה.
בואו ניתן דוגמא מעשית:
הגדרת מערך
להלן דוגמא להגדרת מערך בגודל 10 (כלומר 10 משתנים) מסוג int:
int a[]; a = new int[10];
בדוגמא זאת הגדרנו משתנה בשם a שהוא מערך מסוג int, והקצנו לו בשורה הבאה 10 תאים חדשים.
כלומר המערך מכיל 10 "תאים" שונים מסוג int.
מה עכשיו? עכשיו נזין לתוך חלק מהמערך נתונים:
int a[]; a = new int[10]; a[0] = 111; a[4] = 222; a[9] = 555;
כרגע הזנו לתוך התא הראשון (תא מספר 0), התא החמישי (תא מספר 4) והתא העשירי (תא מספר 9), ערכים שונים וזאת על ידי ציון מיקום התא שאליו אנו רוצים להזין את הנתונים.
ניתן לראות את המערך שלנו בצורה כזאת:
![]()
להלן כמה נקודות מפתח בקשר למערכים:
התא הראשון מתחיל מ- 0
שימו לב בדוגמא שלנו שהזנו את המספר 111 לתא הראשון במערך, אז השתמשנו במספר 0 ולא במספר 1 כדי להגיע אליו.
זאת בגלל שהתא הראשון במערך נמצא במיקום 0 ולא במיקום 1, כפי שניתן לחשוב.
כתוצאה מכך, התא האחרון (תא 10) נמצא במיקום 9 (אליו הזנו את המספר 555).
מערכים הם בגודל קבוע
לאחר שהגדרנו מערך ונתנו לו גודל, לא ניתן לשנות את גודלו בעתיד. כלומר בדוגמא שלנו הגדרנו מערך מסוג int בשם a בגודל 10, ולא ניתן לשנות לו יותר את הגודל לאחר ההגדרה.
לא ניתן לגשת לתא שלא קיים
נסיו לגשת לתא שלא קיים תגרום לקבלת הודעת שגיאה בזמן ריצה ולא בזמן קידוד. כלומר ה- eclipse, במקרה שלנו, לא יתריע לנו שניסינו לכתוב/לקרוא מתוך מקום שלא קיים.
סוגי המשתנים במערך, זהים
ברגע שהגדרנו מערך מסוג מסויים (טיפוס או עצם, לא משנה), כל התאים שלו יהיו מאותו סוג.
כלומר אם הגדרנו מערך מסוג int בגודל 10, כל התאים יהיו תמיד מסוג int

מבריק פרופסור!
זה נשמע שימושי ביותר!
מערכים הם עצמים
עובדה מעניינת נוספת היא שמערך הוא תמיד עצם. דוגמא טובה להמחיש זאת היא הצורה שהגדרנו מערך מסוג int.
זוכרים שלמדנו בשיעור ג שהמשתנה int הוא משתנה פרימיטיבי ולא עצם ולפיכך לא צריך להגדיר אותו עם new, אלה פשוט להזין לתוכו מספר.
לא כך הדבר עם מערכים. ברגע שהגדרנו מערך, של משתנה פרימיטיבי או של עצם, המשתנה שהגדרנו תמיד יהיה עצם מסוג "מערך", ונהיה חייבים ליצור אותו עם new.
מכיוון שמערך הוא עצם, יש לו תכונות ופעולות שניתן להשתמש בהם. שימו לב לדוגמא הבאה:
Ball ballsArray[] = new Ball[10]; for (int i = 0;i < ballsArray.length;i++) ballsArray[i] = new Ball(i * 10, i * 10, 100, 100);
שימו לב לקטע הקוד המעניין הזה.
שורה 1: הגדרנו מערך של כדורים (Ball הינו עצם מיוחד שהגדרנו בשיעור ו', זוכרים?) בגודל 10 תאים.
שורה 3: הגדרנו לולאה שרצה length פעמים. שימו לב לשימוש המעניין שעשינו במערך. ניגשנו אליו, מבלי לציין תא ספציפי, ונחשפה לפנינו תכונה בשם length, המחזירה לנו את גודל המערך. בצורה כזאת יכלנו לכתוב לולאה שהיא בעצם "מנותקת" מגודל המערך ויכולה לרוץ על כל מערך כדורים, ללא קשר לגודל המסוים שלו.
שורה 4: יצירת עצם חדש מסוג Ball במיקום i במערך הכדורים שלנו. שימו לב ש i משתנה בהתאם ללולאה.
בעצם יצרנו בעזרת 2 שורות לולאה שיוצרת לנו את כל המשתנים שלנו. בהחלט יעיל יותר מאשר ליצור אותם אחד אחד.
מערכים רב-מימדיים
מערכים בעצם אינם מוגבלים רק ל"שורה אחת" של רשימת ערכים. למעשה, ניתן להגדיר כמה "מימדים" שונים למערך.
הכי טוב להסביר זאת בצורת דוגמא. שימו לב לקטע הקוד הבא:
int a[][]; a = new int[5][10]; a[0][0] = 100;
מה שעשינו בקטע הקוד הזה הוא הגדרה של מערכו דו-מימדי, בעל 5 שורות, שכל שורה מכילה 10 תאים, והכנסו את הערך 100 לתוך התא הראשון בשורה הראשונה.
ניתן לראות את המערך הדו-מיימדי שיצרנו בצורה כזאת:

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

כמו במשחק שובר לבנים קלאסי, סידור הלבנים יכול להשתנות – כלומר השורות לא יהיו בהכרח מלאות בלבנים.
אנחנו נבנה סוג של תשתית נוחה שבעזרתה יהיה ניתן בקלות יחסית "לסדר" את הלבנים, ולאחר מכן להרחיב אותה ולהוסיף להן תכונות נוספות.
מערך הלבנים
כדי להשיג את "הרשת הוירטואלית" הזאת, שתאפשר לנו להגדיר בקלות את מיקום הלבנים, אנו נגדיר מערך דו-מימדי מסוג int. התאים שיכילו את הערך 1 יציינו שיש לבנה במיקום הספיציפי הזה על המסך, והתאים שיכילו 0 יציינו שאין לבנה במיקום.
כלומר המערך שלנו יראה כך:
int[][] newLevel = { {0, 1, 1, 0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 1, 0, 1, 1, 0}
};
נסו לדמיין בראש כיצד יראה סידור הלבנים על המסך במקרה הזה.
המחלקה GameLevel
כדי לבנות לנו תשתית נוחה, אנחנו ניצור מחלקה אבסטרקטית אשר מכילה מערך דו-מימדי של מיקום הלבנים ודואגת לכל הפונקציונליות הקשורה בנושא (חוץ מציור הלבנים, שלשם כך נבנה מחלקה נוספת), כגון הגדרה של גודל לבנה, בדיקה האם קיימת לבנה במיקום מסויים, בדיקה האם הכדור פגע בלבנה מסויימת וכדומה.
למה המחלקה היא אבסרקטית בעצם?
בגלל שהפונקציונליות (לדוגמא, בדיקה האם הכדור פגע בלבנה) תמיד תהיה זהה בין כל שלב ושלב בתוכנית. הדבר היחידי שמשתנה הוא סידור הלבנים. לפיכך כל פעם שנרצה ליצור רמה חדשה אנו ניצור מחלקה אשר יורשת את GameLevel ובסה"כ מגדירה את המערך הדו-מימדי שמכיל את סידור הלבנים.
להלן הקוד של המחלקה GameLevel:
package iAndroid.pingPong;
abstract public class GameLevel {
// Space from the top screen from which the bricks will be drawn
public final int TopPadding = 25;
// Space between each brick
public final int BrickPadding = 1;
// Brick Width and Height
public final int BrickWidth = 31;
public final int BrickHeight = 15;
// Max number of brick rows and columns
public final int MaxRows = 4;
public final int MaxCols = 10;
// The bricks array that we will be working on.
protected int[][] level;
// Constructor just creates an empty array of bricks
public GameLevel()
{
this.level = new int[this.MaxRows][this.MaxCols];
}
// Calculating a brick top border
public int getBrickTop(int col, int row)
{
return this.TopPadding + (row * (this.BrickHeight + this.BrickPadding));
}
// Calculating a brick bottom border
public int getBrickBottom(int col, int row)
{
return this.TopPadding + (row * (this.BrickHeight + this.BrickPadding)) + this.BrickHeight;
}
// Calculating a brick left border
public int getBrickLeft(int col)
{
return col * (this.BrickWidth + this.BrickPadding);
}
// Calculating a brick right border
public int getBrickRight(int col)
{
return (col * (this.BrickWidth + this.BrickPadding)) + BrickWidth;
}
// Receive a brick position in the array and return True if there is a brick there
public boolean isBrickAt(int row, int col)
{
if (this.level[row][col] == 1)
return true;
else
return false;
}
// Receive row number, col number and a ball, and check if
// there is a hit
private boolean checkHit(int row, int col, Ball ball) {
// Check for a hit from below
if (ball.getTop() <= this.getBrickBottom(col, row) &&
ball.getTop() >= this.getBrickTop(col, row) &&
ball.getRight() >= this.getBrickRight(col) &&
ball.getLeft() <= this.getBrickLeft(col))
return true;
// Check for a hit from below
if (ball.getBottom() >= this.getBrickTop(col, row) &&
ball.getBottom() <= this.getBrickBottom(col, row) &&
ball.getRight() >= this.getBrickRight(col) &&
ball.getLeft() <= this.getBrickLeft(col))
return true;
// No hit
return false;
}
// Receive a ball and check if there is a hit on one of the bricks.
// If so, "removing" the brick from the array by changing the value to 0
public boolean updateIfHit(Ball ball) {
// Looping over the bricks rows
for (int row = 0; row < this.MaxRows ; row ++)
// Looping over the bricks columns
for (int col = 0 ; < this.MaxCols ; col ++)
// Check to see if we have a brick here
if (this.isBrickAt(row, col))
{
// Check to see if there is a ball hit on brick
if (this.checkHit(row, col, ball))
{
// HIT! Removing the brick from this specific place
this.level[row][col] = 0;
return true;
}
}
// If we are here, it means that there was no hit.
return false;
}
}
אז בואו נעבור על הקוד של המחלקה:
שורה 3: הגדרה של מחלקה אבסטרקטית חדשה בשם GameLevel.
שורה 5: הגדרת משתנה בשם TopPadding, אשר יגדיר את המרחק בין השורה הראשונה של הלבנים לבין הגבול העליון של המסך. אנו משתמשים במשתנה זה בעצם כדי שהלבנים לא יהיו צמודות לחלק העליון של המסך.
שורה 8:הגדרת משתנה בשם BrickPadding, אשר מגדיר את המרחק בין כל לבנה ולבנה וזאת כדי שהם לא יהיו "דבוקות" אחת לשניה.
שורה 11: הגדרה של רוחב כל לבנה
שורה 12: הגדרה של גובה כל לבנה
פה חשוב לעצור רגע ולתת כמה מילים. אורך המסך משתנה בין מכשיר למכשיר. הסיבה שבחרתי לשים רוחב 31 הוא בגלל שמסך הנקסוס הוא ברוחב 320. אם אמרנו שאנחנו רוצים להכניס 10 לבנים בשורה, אז רוחב כל לבנה הוא 31 + 1 (הרווח בין כל לבנה ולבנה), כלומר 32.
כלומר עקרונית, קטע קוד זה אינו מכסה מכשירים שאינם נקסוס. למה עשיתי את זה כך? כדי שיהיה לכם מה לעשות בשיעורי הבית, כמובן
שורה 15: קבוע המגדיר כמה מקסימום שורות של לבנים יש לנו. אנו משתמשים בקבוע זה בכל הלולאות שלנו במחלקה.
שורה 16: קבוע המגדיר כמה מקסימום לבנים יש לנו בכל שורה. גם בקבוע זה אנו משתמשים בכל לולאות המחלקה.
שורה 19: הגדרה של מערך דו-מימדי בשם level, שיתאר איפה יש לנו לבנים ואיפה אין.
שורות 22-25: הגדרת בנאי המחלקה, אשר יוצר את level אבל לא מזין לו נתונים. במקרה כזה ערכי המערך יהיו אפס בכל התאים, מה שאומר שאין לבנים על המסך (אנו נשנה זאת במחלקות שירשו את GameLevel בהמשך).
שורות 28-31: מתודה המקבלת מיקום של לבנה על המערך, ומחזירה את הגבול העליון שלה על המסך עצמו. המתודה בעצם מחשבת את המיקום על ידי הכפלה של מיקום הלבנה בגובה הלבנה+המרחק ללבנה הבאה. לנוסחה זאת מוסיפים את המרחק מהגבול העליון של המסך – ויש לנו את הגבול העליון של הלבנה!
שורות 34-37: מתודה המקבלת מיקום של לבנה על המערך, ומחזירה את הגבול התחתון ע"י נוסחה דומה למתודה הקודמת.
שורות 40-43: מתודה המקבלת מיקום של לבנה על המערך, ומחזירה את הגבול השמאלי שלה על ידי הכפלה של מיקום הלבנה באורך הלבנה+המרחק ללבנה הבאה.
שורות 46-49: מתודה המקבלת מיקום של לבנה על המערך ומחזירה את הגבול הימני ע"י נוסחה דומה למתודה הקודמת.
שורות 52-58: מתודה המקבלת מיקום של לבנה ומחזירה true אם יש שם לבנה או false אם אין.
שורות 62-79: מתודה המקבלת מיקום של לבנה וכדור, ובודקת האם יש פגיעה בלבנה על ידי השוואת גבולות הכדור לגבולות הלבנה.
שורות 83-103: מתודה המקבלת כדור, ורצה בלולאה על כל הלבנים במערך, בודקת האם יש לבנה בכל מיקום ומיקום ואם כן, בודקת בכל מיקום האם יש פגיעה של הכדור בלבנה הזאת. במידה ויש, המתודה מעדכנת את המערך ובעצם "מסירה" את הלבנה מהמערך.
מה שיצרנו בעצם זאת מחלקה שמטפלת בכל הקשור ללבנים על המסך וזאת ללא קשר לסידור הלבנים עצמם.
כעת, אם נרצה ליצור"שלב" חדש למשחק שלנו, כל מה שנצטרף זה ליצור מחלקה שתירש את GameLevel ולהגדיר לה את סידור הלבנים על המסך.
להלן דוגמא למחלקה כזאת:
package iAndroid.pingPong;
public class GameLevel1 extends GameLevel {
public GameLevel1() {
// Creating the bricks on the level
int[][] newLevel = { {0, 1, 1, 0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 1, 0, 1, 1, 0}
};
// Updating our level
this.level = newLevel;
}
}
שימו לב כמה קצר ולעניין.
המחלקה GameLevel1 יורשת את GameLevel ורק מגדירה את סידור הלבנים בצורה פשוטה וקריאה לעין.
כך אמורות להיות מוצגות הלבנים על המסך שלנו, בהתאם ל GameLevel1
(אגב, מדובר בתמונה אמיתית מתוך המשחק עצמו).
המחלקה GameLevelView
כעת כל מה שנותר לנו זה רק לצייר את הלבנים על המסך. לשם כך אנו ניצור מחלקה בשם GameLevelView, שמקבלת משתנה מסוג GameLevel, רצה בלולאה על הלבנים ומציירת אותם.
להלן קוד המלקה GameLevelView:
package iAndroid.pingPong;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;
public class GameLevelView extends View {
private GameLevel level; //the bricks array
private Paint brickPaint; //the paint to paint each brick with
public GameLevelView(Context context, GameLevel newLevel) {
super(context);
this.level = newLevel;
}
public void setGameLevel(GameLevel newLevel) {
this.level = newLevel;
}
//returns the paint that should be used to paint the ball
private Paint getBrickPaint() {
if (brickPaint == null)
{
brickPaint = new Paint();
brickPaint.setStrokeWidth(2);
brickPaint.setColor(Color.WHITE);
}
return brickPaint;
}
//do the actual drawing itself
@Override
public void onDraw(Canvas canvas) {
// Looping over the rows
for (int row = 0; row < level.MaxRows ; row ++)
// Looping over the columns
for (int i = 0 ; i < level.MaxCols ; i ++)
// Check to see if we have a brick here
if (level.isBrickAt(row, i))
//Drawing the brick
canvas.drawRect(level.getBrickLeft(i),
level.getBrickTop(i, row),
level.getBrickRight(i),
level.getBrickBottom(i, row),
getBrickPaint());
}
}
שורה 9: הגדרה של מחלקה חדשה בשם GameLevelView היורשת מ View
שורה 11: הגדרה של משתנה בשם gameLevel מסוג GameLevel, שיכיל את הלבנים והפונקציונליות שלהם.
שורה 12: הגדרה של המשתנה מסוג Paint, בו נשתמש כדי לצייר את הלבנים (על נושא זה דיברנו בשיעור ו')
שורות 14-18: הגדרת בנאי למחלקת GameLevelView אשר מקבל כפרמטר את GameLevel, בו נשתמש כדי להציג את הלבנים.
שורות 20-22: הגדרת מתודה בשם setGameLevel, שתאפשר לנו להחליף את סידור הלבנים מבלי ליצור מחלקת GameLevelView חדשה. נושא זה יכול להיות שימושי, לדוגמא, כאשר המשתמש עובר לשלב הבא וצריך לקבל סידור לבנים חדש, או שהמשתמש נפסל וצריך לקבל את סידור הלבנים הקודם.
שורות 25-35: הגדרת מתודה getBrickPaint המחזירה את המשתנה Paint מבלי ליצור אותו בכל פעם מחדש. גם על נושא זה דיברנו דיברנו בשיעור ו'.
שורות 39-52: הגדרת המתודה onDraw המציירת את הלבנים. המתודה עוברת בלולאה על מערך הלבנים, בודקת האם יש לבנה במיקום מסויים, ואם כן, היא מציירת אותה.
מדביקים את כל החלקים
כל מה שנותר לנו בשלב זה הוא להדביק שלושת המחלקות שלנו (GameLevel, GameLevel1 ו GameLevelView) לתוך המשחק שלנו.
מה שאנחנו צריכים לעשות הוא:
1. להגדיר את GameLevel ואת GameLevelView ב- Activity הראשי שלנו, במחלקת PingPong.
2. להוסיף את GameLevelView לתוך מחלקת PingPongView שלנו, ולקרוא משם ל onDraw של GameLevelView.
3. להוסיף את GameLevel למחלקת PingPongGame, ולקרוא ל updateIfHit בכל מחזור של הלולאה. במידה ויש פגיעה, יש לשנות את כיוון הכדור חזרה למטה.
את ביצוע של החלקים האלו, אני אשאיר הפעם לכם מכיוון שמדובר בנושא יחסית פשוט ודומה כמעט לכל מה שעשינו בשיעורים הקודמים.

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

תגובות
השאר תגובה טראקבאקים