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

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

![]()
בואו נחזור לעניינינו.
מערכת ההפעלה אנדרואיד מספקת ספריות רחבות ועשירות לטיפול בנושא השמעת מוסיקה וצלילים. כמו בשיעורים הקודמים, גם הפעם אספק לכם הצצה קצרה, על קצה המזלג, לשימוש בספריות הללו וזאת בכדי שתוכלו כבר להתחיל ולהוסיף צלילים לאפליקציה שלכם.
אז בואו נתחיל!
תהליך השמעת מוסיקה/צלילים
נתחיל מהשאלה הפשוטה. היכן שומרים את הצלילים במשחק שלנו?
את הצלילים של האפליקציה נשמור בתוך אזור המשאבים שלנו (עליו דיברנו בשיעור ט').
תחת הענף res, ניצור ספריה חדשה בשם raw (חשוב להקפיד על השם!) ונגרור לתוכה את קבצי המוסיקה שלנו.
אנדרואיד תומך מגוון סוגי קבצים, כמו mp3, ogg, midi ועוד (את הרשימה המלאה תמצאו כאן) ותוכלו להוסיף כל אחד מהם כדי להשמיע אותו במהלך המשחק.
המחלקה SoungPool
מחלקת SoundPool, אשר נמצאת בתוך החבילה android.media.SoundPool אחראית על השמעת הצלילים באפליקציות, ואף מספקת לנו כלים שונים שבעזרתם ניתן ליעל את התהליך.
המתודה load
סוגי קבצי השמע השונים מייצגים בפורמטים אשר בעצם "מכווצים" את הצלילים שלנו, כדי שיתפסו פחות מקום. לדוגמא קובץ mp3 של 4 דקות תופס כ 4 מגה (תלוי באיכות כמובן) במקום 40 מגה, וזאת הודות לאלגוריתם מיוחד בפורמט המבצע פעולות שונות, ובינהם גם כיווץ הקובץ.
תהליך הפריסה (או הקידוד) של הקובץ הוא תהליך מעט כבד, אשר זולל לנו חלק ממשאבי המעבד, אך נדרש לביצוע בכל פעם שאנחנו רוצים להשמיע צליל.
לא היינו רוצים לפגוע בביצועים של המשחק שלנו ולבצע את הקידוד הזה בכל פעם מחדש, כשנרצה להשמיע צליל מסויים.
מסיבה זאת, בשלב הראשון של התוכנה שלנו, ועוד לפני שהאפליקציה/משחק שלנו התחיל לרוץ, אנחנו רוצים לקודד מראש את הצלילים שלנו ולשמור אותם בפורמט גולמי (raw) אשר פשוט אפשר להשמיע מבלי צורך לקודד.
המתודה load, במחלקה SoundPool, עושה בדיוק את זה, והיא מקבלת שלושה פרמטרים:
1. context של האפליקציה המכיל, בין השאר, את R אשר בעזרתו אנו מגיעים למשאבים שלנו (איפה ששמרנו את הצלילים).
2. resid, ה- ID של קובץ הקול שלנו, שנמצא בתוך המשאבים של האפליקציה שלנו.
3. priority, העדיפות של הצליל על צלילים אחרים, פרמטר אשר אינו בשימוש במערכת ההפעלה אנדרואיד בשלב זה.
המתודה פורסת את הצליל לפורמט raw, שומרת אותו אצלה בזיכרון ומחזירה לנו ID מסוג int אשר בעזרתו נוכל לגשת ולהשמיע את הצליל מאוחר יותר.
המתודה play
כעת, אחרי שטענו ופרסנו את הצלילים, כל מה שנותר הוא להשמיע אותם.
המתודה play מבצעת בדיוק את זה, והיא מקבלת את הפרמטרים הבאים:
1. soundID, ה ID של הצליל הפרוס אותו אנחנו רוצים להשמיע (אותו אחד שאנחנו מקבלים מהמתודה load, עליה כרגע דיברנו).
2. leftVolume, ערך מסוג float בין 0.0 ל- 1.0, המתאר את חוזק הצליל (Volume) בצד שמאל.
3. rightVolume, ערך מסוג float בין 0.0 ל- 1.0, המתאר את חוזק הצליל (Volume) בצד ימין.
4. priority, העדיפות של הצליל המושמע, על צלילים אחרים.
5. loop, פרמטר מסוג int המתאר כמה פעמים צריך להשמיע שוב את הצליל, בלולאה. לדוגמא אם נעביר בפרמטר זה 3, הצליל יושמע 3 פעמים ברציפות, ואילו אם נגדיר 1-, הצליל יושמע בלולאה אין סופית.
6. rate, פרמטר מסוג float בין 0.5 ל 2.0, המתאר את גובה הצליל (1.0 מתאר גובה רגיל).
מתי נרצה להשמיע צליל באופן אין-סופי (בעזרת הפרמטר loop)?
דוגמא לכך היא שאנחנו רוצים להשמיע מוסיקת רקע למשחק שלנו, אשר אמורה להיות מושמעת שוב ושוב וללא הפסקה.
המתודה stop
באופן טבעי, צליל מפסיק להיות מושמע אוטומטית, בהתאם לפרמטר loop שאנו מעבירים במתודה play. יחד עם זאת, יתכן ונרצה להפסיק באופן יזום השמעה של צליל מסויים (לדוגמא, אם השמענו אותו בלולאה אין סופית).
לשם כך אנו משתמשים במתודה stop, אשר מקבלת פרמטר אחד, ה- ID של הצליל אותו אנחנו רוצים להפסיק
הבנאי של SoundPool
שמרתי את ההסבר על הבנאי של SoundPool לשלב זה, כדי שהפרמטרים שלו יהיו יותר קצת יותר ברורים.
הבנאי של מחלקת SoundPool מקבל שלושה פרמטרים:
1. maxStreams, פרמטר מסוג int המתאר לנו כמה צלילים אנו רוצים להשמיע בו-זמנית.
כאשר אנו משמיעים מספר צלילים במקביל, המחלקה צריכה לבצע "מיזוג" של הצלילים ובעזרת פרמטר זה, אנו יכולים להגדיר כמה מיזוגים כאלה יבוצעו, כאשר אם נעבור אותם, הצליל הראשון שהשמענו פשוט יפסק מלהיות מושמע.
2. streamType, סוג, או צורת ה stream שאנחנו משמיעים. על פרמטר זה לא נדבר בשיעור זה, אך בדוגמאות הבאות נסביר איזה ערך יש להעביר לו.
3. srcQuality, איכות הצלילים עליה אנחנו רוצים לשמור בפריסה. פרמטר זה אומנם מועבר לבנאי, אך אינו נמצא בשימוש בשלב זה באנדרואיד.
המתודה release
כאשר נסיים להשתמש בצלילים שלנו, נרצה להפסיק להשמיע את כל הצלילים ולפנות את הזכרון המכיל את הצלילים הפרוסים (בד"כ בסוף המשחק).
לשם כך, נשתמש במתודה release שתבצע זאת עבורנו.
שימו לב! לאחר הקריאה ל release כל הצלילים הפרוסים יפונו מהזכרון ולא ניתן יהיה להשתמש בהם בעזרת play או בשום דרך אחרת, מבלי לטעון אותם מחדש.
מחברים את כל החלקים
לשם הדגמת השימוש במתודות השונות, אנו נגדיר מחלקה חדשה בשם GameSounds אשר נוכל להשתמש בה במשחק שובר הלבנים שלנו, כדי להשמיע את הצלילים השונים.
המחלקה שאנו כותבים בעצם יוצרת סוג של "מעטפת" למחלקה SoundPool ומאפשרת לנו להוסיף לה צלילים ולהשמיע אותם.
המעטפת שלנו תאפשר לנו לקבוע לכל צליל שנוסיף ID משלנו, אשר נוכל להשתמש בו כדי להשמיע את הצלילים.
הצורך ב ID כזה נובע מהעובדה, שכאשר אנו קוראים למתודה load ב SoundPool, אין לנו שליטה על ה- ID שמוחזר לנו ממנה ויכול לחזור לנו בעצם כל ID שהמחלקה תבחר.
כדי לקבל שליטה על ה- ID הזה, אנו נשתמש במחלקה HashMap אשר תעזור לנו "לקשר" ID שאנחנו בוחרים, עם ה ID שמוחזר לנו מהמתודה load.


![]()
איך זה נשמע לכם?
המחלקה GameSounds
להלן קוד המחלקה:
package iAndroid.pingPong;
import java.util.HashMap;
import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
public class GameSounds {
private SoundPool soundPool;
private HashMap soundPoolHashMap;
public GameSounds() {
soundPool = new SoundPool(4, AudioManager.STREAM_MUSIC, 0);
soundPoolHashMap = new HashMap();
}
public void addSound(int index, int soundID, Context context) {
int soundPoolID = soundPool.load(context, SoundID, 1);
soundPoolHashMap.put(index, soundPoolID);
}
public void play(int index, boolean loop) {
if (!loop)
soundPool.play((Integer) soundPoolHashMap.get(index), 1, 1, 1, 0, 1f);
else
soundPool.play((Integer) soundPoolHashMap.get(index), 1, 1, 1, -1, 1f);
}
public void stop(int index) {
soundPool.stop((Integer) soundPoolHashMap.get(index));
}
public void release() {
soundPool.release();
}
}
הסבר על הקוד:
שורה 09: הגדרת מחלקה חדשה בשם GameSounds, אשר בעזרתה נשמיע צלילים במשחק שלנו.
שורה 10: הגדרת משתנה פרטי מסוג SoundPool בשם soundPool.
שורה 11: הגדרת משתנה פרטי מסוג HashMap בשם soundPoolHashMap, אשר ישמש כ"מתווך" בין ה- IDים השונים.
שורות 13-16: הגדרת של בנאי המחלקה, היוצר את soundPool ואת soundPoolHashMap.
שורה 18: הגדרה של מתודה בשם addSound, המקבלת index המהווה את ה ID שאנחנו בוחרים לצליל, ID של קובץ הצליל בתוך ה resource שלנו ואובייקט מסוג Context, בו נמצא ה resource שלנו.
שורה 19: קריאה למתודה load ב soundPool כדי להפוך את הצליל שמוגדר ב soundID ל raw, והכנסת ה- ID שחוזר מ load לתוך soundPoolID.
שורה 20: הוספת ערך חדש ל soundPoolHashMap בעזרת put, בצורה שתקשר בין index לה ID שחזר לנו מ soundPoolID.
שורה 23: הגדרה של מתודה בשם play המקבלת פרמטר index המצביע על קוד הצליל שאנחנו רוצים להשמיע, ופרמטר בוליאני בשם loop אשר מגדיר האם להשמיע את הצליל באופן מחזורי (אין סופי) או לא.
שורה 24: משפט תנאי הבודק את הערך של loop.
שורה 25: במידה והערך ב loop הוא false, אנו קוראים למתודה play שב soundPool, מעבירים את קוד הצליל בעזרת soundPoolHashMap, קובעים את הוליום הגבוה ביותר לרמקול ימין ולרמקול שמאל, ומגדירים כי את הצליל אין להשמיע במחזוריות.
שורה 26: במידה והערך ב loop הוא true, מתבצעת קריאה בדומה לשורה 25, רק שבערך שמגדיר את המחזוריות שמנו 1- לשם הגדרת מחזוריות אין-סופית.
שורות 30-32: הגדרה של מתודה בשם stop המקבלת index של הצליל אותו אנו רוצים להפסיק להשמיע, ומפסיקה את השמעתו בהתאם.
שורות 34-36: הגדרה של מתודה בשם release הקוראת ל release בתוך soundPool לשם עצירת כל הצלילים ושיחרור מלא של הזכרון.
כעת, כל מה שנותר הוא להגדיר את GameSounds בתוך PingPong.java ולהוסיף את הצלילים שגררנו.
לשימושכם, הכנו לכם 2 צלילים של פגיעה, אחד שיכול לשמש פגיעה בקיר והשני שיכול לשמש פגיעה בלבנה.
הורידו את הצלילים בלחיצה כאן -> iAndroid PingPong Sounds
את הצלילים, הוסיפו לסיפריה raw תחת res בעץ הפרויקט שלכם בה eclipse (אם הסיפריה לא קיימת, צרו אותה).
להלן קטע הקוד שיש להוסיף ב PingPong.java כדי להוסיף את הצלילים.
GameSounds gameSounds = new GameSounds();
gameSounds.addSound(1, R.raw.hit1, getBaseContext());
gameSounds.addSound(2, R.raw.hit2, getBaseContext());
שורה 1: הגדרת המשתנה gameSounds מסוג GameSounds.
שורה 3: הוספת הצליל hit1 לתוך gameSounds, תוך שאנו מעניקים לו את הקוד 1.
שורה 4: הוספת הצליל hit2 לתוך gameSounds, תוך שאנו מעניקים לו את הקוד 2.
שימו לב חברים, שלצורך קריאות טובה יותר, ובמיוחד במידה ותוסיפו עוד צלילים למשחק, כדאי להגדיר את המזהים שהשתמשנו בהם בשורות 3 ו-4 כקבועים, בעלי משמעות ע"פ סוג הצלילים שהוספתם.

![]()
הסיבה שאנחנו לא הגדרנו את המחלקה כסטטית (static) או כ Singleton היא כי עדיין לא לימדתי את הנושאים האלו ולא רציתי לבלבל את המשתמשים הפחות מנוסים שלנו.
יחד עם זאת, הצורה שאתה מציע היא בהחלט טובה ונכונה יותר!
אז מה עכשיו?
עכשיו כל מה שצריך לעשות הוא לקרוא למתודה play בכל מקום בו נרצה להשמיע צליל פגיעה.
כמובן, שנצטרך להעביר את האובייקט gameSounds לתוךכל אובייקט ממנו נרצה להשמיע את הצליל.
אנו נשאיר חלק זה לכם, תלמידים יקרים.
אם לא הסתדרתם, הסתבכתם, אם יש לכם שאלות נוספות או הצעות, כמו תמיד תוכלו למצוא אותנו באשכול השיעור אשר נמצא בפורום הפיתוח לאנדרואיד.
למרות הדיונים הסוערים שהתחוללו באשכול השיעור הקודם, לא נמצא מפתח שענה על אתגר הזהב מהשיעור הקודם.
השבוע אני מזמין אתכם להעלות לנו את הגירסא שלכם למשחק שובר הלבנים, תוךשימוש בכל האלמנטים שלמדנו במהלך הקורס האחרון.
זה הכל להפעם, תלמידים יקרים!
אני מקווה שנהנתם מהשיעור שלנו השבוע ומהקורס הראשון שלנו, בכלל.
בקורס הבא שלנו, נעשה עצירה קטנה מלימודי המשחקים ונתמקד בעיקר בלימוד על שימוש באלמנטים בסיסיים של ממשק משתמש באנדרואיד, כגון layouts, וויג'טים, menus ועוד. בעזרת אלמנטים אלו, תוכלו ליצר מסכים ותפריטים לאפליקציות שלכם.
להית' עד לקורס הבא!
תגובות
השאר תגובה טראקבאקים