สุดยอดคู่มือสำหรับการประมวลผลภาษา ตอนที่ II: การสร้างเกมอย่างง่าย
เผยแพร่แล้ว: 2022-03-11นี่เป็นส่วนที่สองของคู่มือขั้นสูงสุดสำหรับภาษาการประมวลผล ในส่วนแรก ฉันได้ให้คำแนะนำเบื้องต้นเกี่ยวกับภาษาการประมวลผล ขั้นตอนต่อไปสำหรับคุณในการเรียนรู้การประมวลผลคือการเขียนโปรแกรมเชิงปฏิบัติมากขึ้น
ในบทความนี้ ผมจะแสดงให้คุณเห็นถึงวิธีการใช้การประมวลผลเพื่อติดตั้งเกมของคุณเองทีละขั้นตอน แต่ละขั้นตอนจะอธิบายโดยละเอียด จากนั้นเราจะพอร์ตเกมไปยังเว็บ
ก่อนที่เราจะเริ่มบทช่วยสอนการประมวลผล นี่คือรหัสของแบบฝึกหัดโลโก้ดีวีดีจากส่วนก่อนหน้า หากคุณมีคำถามใด ๆ โปรดแสดงความคิดเห็น
บทช่วยสอนการประมวลผล: เกมง่ายๆ
เกมที่เราจะสร้างในบทช่วยสอนการประมวลผลนี้เป็นการผสมผสานระหว่าง Flappy Bird, Pong และ Brick Breaker เหตุผลที่ฉันเลือกเกมแบบนี้ก็คือมีแนวคิดส่วนใหญ่ที่ผู้เริ่มต้นต้องดิ้นรนเมื่อเรียนรู้การพัฒนาเกม อิงจากประสบการณ์ของฉันเมื่อครั้งเป็นผู้ช่วยสอน ช่วยให้โปรแกรมเมอร์ใหม่เรียนรู้วิธีใช้การประมวลผล แนวคิดเหล่านี้รวมถึงแรงโน้มถ่วง การชน การรักษาคะแนน การจัดการหน้าจอต่างๆ และการโต้ตอบของแป้นพิมพ์/เมาส์ เครื่องปัดโป่งมีครบทุกอย่าง
เล่นเกมเลย!
หากไม่ใช้แนวคิดการเขียนโปรแกรมเชิงวัตถุ (OOP) มันไม่ง่ายเลยที่จะสร้างเกมที่ซับซ้อน เช่น เกมแพลตฟอร์มที่มีหลายระดับ ผู้เล่น เอนทิตี ฯลฯ เมื่อเราก้าวไปข้างหน้า คุณจะเห็นว่าโค้ดมีความซับซ้อนอย่างรวดเร็วเพียงใด ฉันพยายามอย่างเต็มที่เพื่อให้บทแนะนำการประมวลผลนี้เป็นระเบียบและเรียบง่าย
ฉันแนะนำให้คุณติดตามบทความ คว้าโค้ดทั้งหมด เล่นด้วยตัวเอง เริ่มคิดเกี่ยวกับเกมของคุณเองโดยเร็วที่สุด และเริ่มใช้งาน
เริ่มกันเลย
ตึกเครื่องปัดโป่ง
ขั้นตอนการสอนการประมวลผล #1: เริ่มต้นและจัดการหน้าจอต่างๆ
ขั้นตอนแรกคือการเริ่มต้นโครงการของเรา สำหรับผู้เริ่มต้น เราจะเขียนการตั้งค่าและวาดบล็อคตามปกติ ไม่มีอะไรแปลกใหม่หรือแปลกใหม่ จากนั้นเราจะจัดการกับหน้าจอต่างๆ (หน้าจอเริ่มต้น หน้าจอเกม เกมโอเวอร์หน้าจอ ฯลฯ) เลยเกิดคำถามว่า ทำอย่างไรให้ Process แสดงหน้าที่ถูกต้องในเวลาที่ถูกต้อง ?
การทำภารกิจนี้ให้สำเร็จนั้นค่อนข้างง่าย เราจะมีตัวแปรส่วนกลางที่เก็บข้อมูลของหน้าจอที่ใช้งานอยู่ในปัจจุบัน จากนั้นเราวาดเนื้อหาของหน้าจอที่ถูกต้องขึ้นอยู่กับตัวแปร ในบล็อกการวาด เราจะมีคำสั่ง if ที่ตรวจสอบตัวแปรและแสดงเนื้อหาของหน้าจอตามลำดับ เมื่อใดก็ตามที่เราต้องการเปลี่ยนหน้าจอ เราจะเปลี่ยนตัวแปรนั้นเป็นตัวระบุหน้าจอที่เราต้องการให้แสดง จากที่กล่าวมานี่คือสิ่งที่รหัสโครงกระดูกของเราดูเหมือน:
/********* VARIABLES *********/ // We control which screen is active by settings / updating // gameScreen variable. We display the correct screen according // to the value of this variable. // // 0: Initial Screen // 1: Game Screen // 2: Game-over Screen int gameScreen = 0; /********* SETUP BLOCK *********/ void setup() { size(500, 500); } /********* DRAW BLOCK *********/ void draw() { // Display the contents of the current screen if (gameScreen == 0) { initScreen(); } else if (gameScreen == 1) { gameScreen(); } else if (gameScreen == 2) { gameOverScreen(); } } /********* SCREEN CONTENTS *********/ void initScreen() { // codes of initial screen } void gameScreen() { // codes of game screen } void gameOverScreen() { // codes for game over screen } /********* INPUTS *********/ public void mousePressed() { // if we are on the initial screen when clicked, start the game if (gameScreen==0) { startGame(); } } /********* OTHER FUNCTIONS *********/ // This method sets the necessary variables to start the game void startGame() { gameScreen=1; }
นี้อาจดูน่ากลัวในตอนแรก แต่ทั้งหมดที่เราทำคือสร้างโครงสร้างพื้นฐานและแยกส่วนต่างๆ ที่มีบล็อกความคิดเห็น
อย่างที่คุณเห็น เราได้กำหนดวิธีการที่แตกต่างกันสำหรับแต่ละหน้าจอที่จะแสดง ในบล็อกการวาดของเรา เราเพียงแค่ตรวจสอบค่าของตัวแปร gameScreen
และเรียกวิธีการที่เกี่ยวข้อง
ในส่วน void mousePressed(){...}
เรากำลังฟังการคลิกเมาส์ และหากหน้าจอที่ใช้งานอยู่เป็น 0 หน้าจอเริ่มต้น เราจะเรียก startGame()
ซึ่งเริ่มเกมตามที่คุณคาดหวัง บรรทัดแรกของวิธีนี้เปลี่ยนตัวแปร gameScreen
เป็น 1 หน้าจอเกม
หากเข้าใจแล้ว ขั้นตอนต่อไปคือการใช้หน้าจอเริ่มต้นของเรา ในการดำเนินการดังกล่าว เราจะทำการแก้ไข initScreen()
นี่มันไป:
void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }
ตอนนี้หน้าจอเริ่มต้นของเรามีพื้นหลังสีดำและข้อความธรรมดา "คลิกเพื่อเริ่ม" ซึ่งอยู่ตรงกลางและจัดชิดตรงกลาง แต่เมื่อเราคลิกไม่มีอะไรเกิดขึ้น เรายังไม่ได้ระบุเนื้อหาใด ๆ สำหรับหน้าจอเกมของเรา เมธอด gameScreen()
ไม่มีอะไรอยู่ในนั้น ดังนั้นเราจึงไม่ครอบคลุมเนื้อหาก่อนหน้าที่ดึงมาจากหน้าจอสุดท้าย (ข้อความ) โดยมี background()
เป็นบรรทัดแรกของการวาด นั่นเป็นสาเหตุที่ข้อความยังคงอยู่ที่นั่นแม้ว่าจะไม่มีการเรียกบรรทัด text()
อีกต่อไป (เช่นเดียวกับตัวอย่างลูกบอลที่กำลังเคลื่อนที่จากส่วนสุดท้ายซึ่งทิ้งร่องรอยไว้เบื้องหลัง) พื้นหลังยังคงเป็นสีดำด้วยเหตุผลเดียวกัน มาเริ่มใช้งานหน้าจอเกมกันเลย
void gameScreen() { background(255); }
หลังจากการเปลี่ยนแปลงนี้ คุณจะสังเกตเห็นว่าพื้นหลังเปลี่ยนเป็นสีขาวและข้อความหายไป
ขั้นตอนการสอนการประมวลผล #2: การสร้างลูกบอลและการใช้แรงโน้มถ่วง
ตอนนี้เราจะเริ่มทำงานบนหน้าจอเกม ก่อนอื่นเราจะสร้างลูกบอลของเรา เราควรกำหนดตัวแปรสำหรับพิกัด สี และขนาด เนื่องจากเราอาจต้องการเปลี่ยนค่าเหล่านั้นในภายหลัง ตัวอย่างเช่น หากเราต้องการเพิ่มขนาดของลูกบอลในขณะที่ผู้เล่นทำคะแนนให้สูงขึ้นเพื่อให้เกมนั้นยากขึ้น เราจะต้องเปลี่ยนขนาดจึงควรเป็นตัวแปร เราจะกำหนดความเร็วของลูกบอลด้วยเช่นกัน หลังจากที่เราใช้แรงโน้มถ่วง
ขั้นแรก ให้เพิ่มสิ่งต่อไปนี้:
... int ballX, ballY; int ballSize = 20; int ballColor = color(0); ... void setup() { ... ballX=width/4; ballY=height/5; } ... void gameScreen() { ... drawBall(); } ... void drawBall() { fill(ballColor); ellipse(ballX, ballY, ballSize, ballSize); }
เรากำหนดพิกัดเป็นตัวแปรส่วนกลาง สร้างวิธีการดึงลูกบอล เรียกจากวิธี gameScreen สิ่งเดียวที่ต้องใส่ใจในที่นี้คือเรา เริ่มต้น พิกัด แต่เรากำหนดไว้ใน setup()
เหตุผลที่เราทำนั่นคือเราต้องการให้บอลเริ่มที่หนึ่งในสี่จากด้านซ้ายและหนึ่งในห้าจากด้านบน ไม่มีเหตุผลเฉพาะเจาะจงที่เราต้องการ แต่นั่นเป็นจุดที่ดีสำหรับลูกบอลที่จะเริ่มต้น ดังนั้นเราจึงจำเป็นต้องได้ width
และ height
ของร่างแบบไดนามิก ขนาดร่างถูกกำหนดใน setup()
หลังจากบรรทัดแรก width
และ height
ไม่ได้ถูกตั้งค่าก่อนการรัน setup()
นั่นเป็นสาเหตุที่เราไม่สามารถทำสิ่งนี้ได้หากเรากำหนดตัวแปรไว้ด้านบน
แรงโน้มถ่วง
ตอนนี้การใช้แรงโน้มถ่วงเป็นส่วนที่ง่าย มีเพียงไม่กี่เทคนิค นี่คือการใช้งานก่อน:
... float gravity = 1; float ballSpeedVert = 0; ... void gameScreen() { ... applyGravity(); keepInScreen(); } ... void applyGravity() { ballSpeedVert += gravity; ballY += ballSpeedVert; } void makeBounceBottom(float surface) { ballY = surface-(ballSize/2); ballSpeedVert*=-1; } void makeBounceTop(float surface) { ballY = surface+(ballSize/2); ballSpeedVert*=-1; } // keep ball in the screen void keepInScreen() { // ball hits floor if (ballY+(ballSize/2) > height) { makeBounceBottom(height); } // ball hits ceiling if (ballY-(ballSize/2) < 0) { makeBounceTop(0); } }
และผลลัพธ์ก็คือ:
ถือม้าของคุณนักฟิสิกส์ ฉันรู้ว่าแรงโน้มถ่วงในชีวิตจริงไม่ได้ทำงานแบบนั้น แทนที่จะเป็นกระบวนการแอนิเมชั่นมากกว่าสิ่งใด ตัวแปรที่เรากำหนดเป็น gravity
เป็นเพียงค่าตัวเลข— float
เพื่อให้เราสามารถใช้ค่าทศนิยม ไม่ใช่แค่จำนวนเต็ม—ที่เราเพิ่มใน ballSpeedVert
ในทุกลูป และ ballSpeedVert
คือความเร็วแนวตั้งของลูกบอล ซึ่งเพิ่มเข้ากับพิกัด Y ของลูกบอล ( ballY
) ในแต่ละลูป เราดูพิกัดของลูกบอลและตรวจสอบให้แน่ใจว่าลูกบอลอยู่ในหน้าจอ ถ้าเราไม่ทำ ลูกบอลจะตกสู่อนันต์ สำหรับตอนนี้ ลูกบอลของเราเคลื่อนที่ในแนวตั้งเท่านั้น ดังนั้นเราจึงดูขอบพื้นและเพดานของหน้าจอ ด้วยวิธี keepInScreen()
เราตรวจสอบว่า ballY
( + รัศมี) น้อยกว่า height
และในทำนองเดียวกัน ballY
( - รัศมี) มากกว่า 0
หากเงื่อนไขไม่เป็นไปตามนั้น เราจะทำการเด้งลูกบอล (จากด้านล่างหรือด้านบน) ด้วย makeBounceBottom()
และ makeBounceTop()
ในการทำให้ลูกบอลกระดอน เราเพียงแค่ย้ายลูกบอลไปยังตำแหน่งที่แน่นอนซึ่งมันจะต้องเด้งและคูณความเร็วแนวตั้ง ( ballSpeedVert
) ด้วย -1
(การคูณด้วย -1 จะเปลี่ยนเครื่องหมาย) เมื่อค่าความเร็วมีเครื่องหมายลบ บวกพิกัด Y ความเร็วจะกลายเป็น ballY + (-ballSpeedVert)
ซึ่งก็คือ ballY - ballSpeedVert
ดังนั้นลูกบอลจึงเปลี่ยนทิศทางทันทีด้วยความเร็วเท่าเดิม จากนั้นเมื่อเราเพิ่ม gravity
ให้กับ ballSpeedVert
และ ballSpeedVert
มีค่าลบ มันเริ่มเข้าใกล้ 0
ในที่สุดก็กลายเป็น 0
และเริ่มเพิ่มขึ้นอีกครั้ง ที่ทำให้ลูกสูงขึ้น สูงขึ้นช้าลง หยุดและเริ่มล้ม
มีปัญหากับกระบวนการแอนิเมชั่นของเรา ลูกบอลยังคงกระเด้งอยู่ หากเป็นสถานการณ์จริง ลูกบอลจะต้องเผชิญกับแรงต้านของอากาศและการเสียดสีทุกครั้งที่สัมผัสพื้นผิว นั่นคือพฤติกรรมที่เราต้องการสำหรับกระบวนการสร้างแอนิเมชั่นเกมของเรา ดังนั้นการปรับใช้สิ่งนี้จึงเป็นเรื่องง่าย เราเพิ่มสิ่งต่อไปนี้:
... float airfriction = 0.0001; float friction = 0.1; ... void applyGravity() { ... ballSpeedVert -= (ballSpeedVert * airfriction); } void makeBounceBottom(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); } void makeBounceTop(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); }
และตอนนี้กระบวนการแอนิเมชั่นของเราทำให้เกิดสิ่งนี้:
ตามชื่อของมัน friction
คือการเสียดสีพื้นผิว และแรง airfriction
ทานอากาศคือการเสียดสีของอากาศ เห็นได้ชัดว่าต้อง friction
ทุกครั้งที่ลูกบอลสัมผัสพื้นผิวใดๆ airfriction
ต้องใช้อย่างต่อเนื่อง นั่นคือสิ่งที่เราทำ applyGravity()
ทำงานในแต่ละลูป ดังนั้นเราจึงลบ 0.0001
เปอร์เซ็นต์ของมูลค่าปัจจุบันออกจาก ballSpeedVert
ในทุกลูป makeBounceBottom()
และ makeBounceTop()
จะทำงานเมื่อลูกบอลสัมผัสพื้นผิวใดๆ ดังนั้นในวิธีการเหล่านั้น เราทำสิ่งเดียวกัน เฉพาะครั้งนี้ด้วย friction
ขั้นตอนการสอนการประมวลผล #3: การสร้างแร็กเกต
ตอนนี้เราต้องการไม้แร็กเก็ตเพื่อให้ลูกบอลกระดอนไป เราควรจะควบคุมแร็กเกต มาทำให้สามารถควบคุมได้ด้วยเมาส์ นี่คือรหัส:
... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); }
เรากำหนดสี ความกว้าง และความสูงของแร็กเกตเป็นตัวแปรส่วนกลาง เราอาจต้องการให้เปลี่ยนระหว่างการเล่นเกม เราใช้เมธอด drawRacket()
ซึ่งทำตามชื่อของมัน เราตั้งค่า rectMode
ให้อยู่ตรงกลาง ดังนั้นแร็กเกตของเราจึงถูกจัดชิดกับกึ่งกลางของเคอร์เซอร์
ตอนนี้เราสร้างแร็กเกตแล้ว เราต้องทำให้ลูกบอลกระดอนกับมัน
... int racketBounceRate = 20; ... void gameScreen() { ... watchRacketBounce(); ... } ... void watchRacketBounce() { float overhead = mouseY - pmouseY; if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { makeBounceBottom(mouseY); // racket moving up if (overhead<0) { ballY+=overhead; ballSpeedVert+=overhead; } } } }
และนี่คือผลลัพธ์:
ดังนั้นสิ่งที่ watchRacketBounce()
ทำคือทำให้แน่ใจว่าไม้และลูกบอลชนกัน มีสองสิ่งที่ต้องตรวจสอบที่นี่ ซึ่งก็คือถ้าลูกและแร็กเกตเรียงกันในแนวตั้งและแนวนอน อันดับแรก ถ้าคำสั่งตรวจสอบว่าพิกัด X ของด้านขวาของลูกบอลมากกว่าพิกัด X ของด้านซ้ายของแร็กเกตหรือไม่ (และในทางกลับกัน) ถ้าใช่ ข้อความที่สองตรวจสอบว่าระยะห่างระหว่างลูกบอลกับแร็กเกตน้อยกว่าหรือเท่ากับรัศมีของลูกบอลหรือไม่ (ซึ่งหมายความว่ากำลังชนกัน) ดังนั้นหากเงื่อนไขเหล่านี้ makeBounceBottom()
จะถูกเรียกและลูกบอลจะกระดอนบนแร็กเกตของเรา (ที่ mouseY
ที่แร็กเก็ตอยู่)
คุณสังเกตเห็น overhead
ของตัวแปรที่คำนวณโดย mouseY - pmouseY
หรือไม่? ตัวแปร pmouseX
และ pmouseY
เก็บพิกัดของเมาส์ไว้ที่เฟรมก่อนหน้า เนื่องจากเมาส์สามารถเคลื่อนที่ได้เร็วมาก จึงมีโอกาสสูงที่เราอาจตรวจไม่พบระยะห่างระหว่างลูกบอลกับแร็กเกตระหว่างเฟรมอย่างถูกต้อง หากเมาส์เคลื่อนเข้าหาลูกบอลเร็วพอ ดังนั้นเราจึงนำความแตกต่างของพิกัดของเมาส์ระหว่างเฟรมและพิจารณาด้วยในขณะตรวจจับระยะห่าง ยิ่งเมาส์เคลื่อนที่เร็วขึ้นเท่าใด ระยะห่างก็ยิ่งยอมรับได้
เราใช้ overhead
ด้วยเหตุผลอื่น เราตรวจพบว่าเมาส์เคลื่อนที่ไปทางใดโดยการตรวจสอบเครื่องหมาย overhead
หากค่าโสหุ้ยเป็นค่าลบ แสดงว่าเมาส์อยู่ด้านล่างของเฟรมก่อนหน้า ดังนั้นเมาส์ (แร็กเกต) ของเราจึงขยับขึ้น ในกรณีนั้น เราต้องการเพิ่มความเร็วพิเศษให้กับลูกบอลและเคลื่อนมันให้ไกลกว่าการกระดอนปกติเล็กน้อยเพื่อจำลองผลกระทบของการตีลูกบอลด้วยแร็กเกต หาก overhead
น้อยกว่า 0
เราจะเพิ่มเข้าไปใน ballY
และ ballSpeedVert
เพื่อให้ลูกบอลสูงขึ้นและเร็วขึ้น ดังนั้นยิ่งแร็กเกตตีลูกบอลได้เร็วเท่าไหร่ แร็กเกตก็จะยิ่งสูงขึ้นและเร็วขึ้นเท่านั้น
ขั้นตอนการสอนการประมวลผล #4: การเคลื่อนที่ในแนวนอนและการควบคุมลูกบอล
ในส่วนนี้ เราจะเพิ่มการเคลื่อนไหวในแนวนอนให้กับลูกบอล จากนั้นเราจะทำให้สามารถควบคุมลูกบอลในแนวนอนด้วยแร็กเกตของเราได้ ไปเลย:
... // we will start with 0, but for we give 10 just for testing float ballSpeedHorizon = 10; ... void gameScreen() { ... applyHorizontalSpeed(); ... } ... void applyHorizontalSpeed(){ ballX += ballSpeedHorizon; ballSpeedHorizon -= (ballSpeedHorizon * airfriction); } void makeBounceLeft(float surface){ ballX = surface+(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } void makeBounceRight(float surface){ ballX = surface-(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } ... void keepInScreen() { ... if (ballX-(ballSize/2) < 0){ makeBounceLeft(0); } if (ballX+(ballSize/2) > width){ makeBounceRight(width); } }
และผลลัพธ์ก็คือ:
แนวคิดในที่นี้ก็เหมือนกับที่เราทำในการเคลื่อนที่ในแนวดิ่ง เราสร้างตัวแปรความเร็วแนวนอน ballSpeedHorizon
เราสร้างวิธีการใช้ความเร็วแนวนอนกับ ballX
และขจัดแรงเสียดทานของอากาศ เราได้เพิ่มคำสั่ง if อีกสองคำสั่งในวิธี keepInScreen()
ซึ่งจะคอยดูลูกบอลจากการชนขอบด้านซ้ายและขวาของหน้าจอ ในที่สุด เราก็สร้าง makeBounceLeft()
และ makeBounceRight()
เพื่อจัดการกับการเด้งจากซ้ายและขวา

ตอนนี้เราได้เพิ่มความเร็วแนวนอนให้กับเกมแล้ว เราต้องการควบคุมลูกบอลด้วยแร็กเกต เช่นเดียวกับเกม Breakout Atari ที่มีชื่อเสียงและในเกมทำลายอิฐอื่น ๆ ทั้งหมด ลูกบอลควรไปทางซ้ายหรือขวาตามจุดบนแร็กเกตที่ตี ขอบของไม้แร็กเกตควรให้ความเร็วลูกในแนวราบมากขึ้น ในขณะที่ไม้ตรงกลางไม่ควรมีผลใดๆ รหัสก่อน:
void watchRacketBounce() { ... if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { ... ballSpeedHorizon = (ballX - mouseX)/5; ... } } }
ผลลัพธ์คือ:
การเพิ่มบรรทัดง่าย ๆ นั้นลงใน watchRacketBounce()
ก็ทำได้สำเร็จ สิ่งที่เราทำคือเรากำหนดระยะห่างของจุดที่ลูกบอลกระทบจากจุดศูนย์กลางของแร็กเกตด้วย ballX - mouseX
. จากนั้นเราก็ทำให้มันเป็นความเร็วแนวนอน ความแตกต่างที่แท้จริงนั้นมากเกินไป ดังนั้นฉันจึงลองสองสามครั้งและพบว่าหนึ่งในสิบของค่าให้ความรู้สึกเป็นธรรมชาติที่สุด
ขั้นตอนการสอนการประมวลผล #5: การสร้างกำแพง
สเก็ตช์ของเราเริ่มดูเหมือนเกมมากขึ้นในแต่ละขั้นตอน ในขั้นตอนนี้ เราจะเพิ่มกำแพงเคลื่อนที่ไปทางซ้าย เช่นเดียวกับ Flappy Bird:
... int wallSpeed = 5; int wallInterval = 1000; float lastAddTime = 0; int minGapHeight = 200; int maxGapHeight = 300; int wallWidth = 80; color wallColors = color(0); // This arraylist stores data of the gaps between the walls. Actuals walls are drawn accordingly. // [gapWallX, gapWallY, gapWallWidth, gapWallHeight] ArrayList<int[]> walls = new ArrayList<int[]>(); ... void gameScreen() { ... wallAdder(); wallHandler(); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { int randHeight = round(random(minGapHeight, maxGapHeight)); int randY = round(random(0, height-randHeight)); // {gapWallX, gapWallY, gapWallWidth, gapWallHeight} int[] randWall = {width, randY, wallWidth, randHeight}; walls.add(randWall); lastAddTime = millis(); } } void wallHandler() { for (int i = 0; i < walls.size(); i++) { wallRemover(i); wallMover(i); wallDrawer(i); } } void wallDrawer(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; // draw actual walls rectMode(CORNER); fill(wallColors); rect(gapWallX, 0, gapWallWidth, gapWallY); rect(gapWallX, gapWallY+gapWallHeight, gapWallWidth, height-(gapWallY+gapWallHeight)); } void wallMover(int index) { int[] wall = walls.get(index); wall[0] -= wallSpeed; } void wallRemover(int index) { int[] wall = walls.get(index); if (wall[0]+wall[2] <= 0) { walls.remove(index); } }
และสิ่งนี้ส่งผลให้:
แม้ว่าโค้ดจะดูยาวและข่มขู่ แต่ฉันสัญญาว่าไม่มีอะไรเข้าใจยาก สิ่งแรกที่ควรสังเกตคือ ArrayList
สำหรับผู้ที่ไม่รู้ว่า ArrayList
คืออะไร มันเป็นเพียงการใช้งานรายการที่ทำหน้าที่เหมือน Array แต่มีข้อดีเหนือกว่าบางอย่าง มันสามารถปรับขนาดได้ มันมีวิธีการที่มีประโยชน์เช่น list.add(index)
, list.get(index)
และ list.remove(index)
เราเก็บข้อมูลวอลล์เป็นอาร์เรย์จำนวนเต็มภายในรายการอาร์เรย์ ข้อมูลที่เราเก็บไว้ในอาร์เรย์นั้นมีไว้สำหรับช่องว่างระหว่างสองกำแพง อาร์เรย์มีค่าต่อไปนี้:
[gap wall X, gap wall Y, gap wall width, gap wall height]
ผนังจริงถูกวาดตามค่าผนังช่องว่าง โปรดทราบว่าสิ่งเหล่านี้สามารถจัดการได้ดีกว่าและสะอาดกว่าโดยใช้คลาส แต่เนื่องจากการใช้ Object Oriented Programming (OOP) ไม่อยู่ในขอบเขตของบทช่วยสอนการประมวลผลนี้ เราจะจัดการกับมันอย่างไร เรามีสองวิธีพื้นฐานในการจัดการ wall, wallAdder()
และ wallHandler
wallAdder()
จะเพิ่มวอลล์ใหม่ในทุกๆ มิลลิวินาทีของ wallInterval
ให้กับรายการอาร์เรย์ เรามีตัวแปร lastAddTime
ซึ่งเก็บเวลาที่เพิ่มกำแพงสุดท้าย (เป็นมิลลิวินาที) หากมิลลิวินาทีปัจจุบัน millis()
ลบด้วย lastAddTime
มิลลิวินาทีที่เพิ่มล่าสุด นั้นมากกว่าค่าช่วงเวลาของเรา wallInterval
แสดงว่าถึงเวลาแล้วที่จะต้องเพิ่มวอลล์ใหม่ จากนั้นตัวแปรช่องว่างสุ่มจะถูกสร้างขึ้นตามตัวแปรส่วนกลางที่กำหนดไว้ที่ด้านบนสุด จากนั้นวอลล์ใหม่ (อาร์เรย์จำนวนเต็มที่เก็บข้อมูลช่องว่างวอลล์) จะถูกเพิ่มลงในรายการอาร์เรย์ และ lastAddTime
ถูกตั้งค่าเป็นมิลลิวินาทีปัจจุบัน millis()
wallHandler()
วนรอบกำแพงปัจจุบันที่อยู่ในรายการอาร์เรย์ และสำหรับแต่ละรายการในแต่ละลูป จะเรียก wallRemover(i)
, wallMover(i)
และ wallDrawer(i)
ด้วยค่าดัชนีของรายการอาร์เรย์ วิธีการเหล่านี้ทำตามชื่อของพวกเขา wallDrawer()
ดึงผนังจริงตามข้อมูลผนังช่องว่าง มันคว้าอาร์เรย์ข้อมูลวอลล์จาก arraylist และเรียกเมธอด rect()
เพื่อดึงวอลล์ไปยังตำแหน่งที่ควรจะเป็น wallMover()
ดึงองค์ประกอบจากรายการอาร์เรย์ เปลี่ยนตำแหน่ง X ตามตัวแปรทั่วโลกของ wallSpeed
สุดท้าย wallRemover()
จะลบกำแพงออกจากรายการอาร์เรย์ที่อยู่นอกหน้าจอ ถ้าเราไม่ทำเช่นนั้น การประมวลผลจะถือว่าพวกเขายังคงอยู่ในหน้าจอ และนั่นจะเป็นการสูญเสียประสิทธิภาพอย่างมาก ดังนั้นเมื่อกำแพงถูกลบออกจากรายการอาร์เรย์ ผนังจะไม่ถูกวาดในลูปที่ตามมา
สิ่งท้าทายสุดท้ายที่เหลือที่ต้องทำคือการตรวจจับการชนระหว่างลูกบอลกับกำแพง
void wallHandler() { for (int i = 0; i < walls.size(); i++) { ... watchWallCollision(i); } } ... void watchWallCollision(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; int wallTopX = gapWallX; int wallTopY = 0; int wallTopWidth = gapWallWidth; int wallTopHeight = gapWallY; int wallBottomX = gapWallX; int wallBottomY = gapWallY+gapWallHeight; int wallBottomWidth = gapWallWidth; int wallBottomHeight = height-(gapWallY+gapWallHeight); if ( (ballX+(ballSize/2)>wallTopX) && (ballX-(ballSize/2)<wallTopX+wallTopWidth) && (ballY+(ballSize/2)>wallTopY) && (ballY-(ballSize/2)<wallTopY+wallTopHeight) ) { // collides with upper wall } if ( (ballX+(ballSize/2)>wallBottomX) && (ballX-(ballSize/2)<wallBottomX+wallBottomWidth) && (ballY+(ballSize/2)>wallBottomY) && (ballY-(ballSize/2)<wallBottomY+wallBottomHeight) ) { // collides with lower wall } }
watchWallCollision()
ถูกเรียกสำหรับแต่ละกำแพงในแต่ละลูป เราจับพิกัดของผนังช่องว่าง คำนวณพิกัดของผนังจริง (บนและล่าง) และตรวจสอบว่าพิกัดของลูกบอลชนกับผนังหรือไม่
ขั้นตอนการสอนการประมวลผล #6: สุขภาพและคะแนน
ตอนนี้เราสามารถตรวจจับการชนของลูกบอลและกำแพงได้แล้ว เราจึงตัดสินใจเกี่ยวกับกลไกของเกมได้ หลังจากปรับแต่งเกมแล้ว ฉันก็สามารถทำให้เกมเล่นได้ค่อนข้างดี แต่ถึงกระนั้นมันก็ยากมาก ความคิดแรกของฉันเกี่ยวกับเกมนี้คือการทำให้มันเหมือน Flappy Bird เมื่อลูกบอลสัมผัสกับกำแพง เกมจะจบลง แต่แล้วฉันก็รู้ว่ามันเป็นไปไม่ได้ที่จะเล่น นี่คือสิ่งที่ฉันคิดว่า:
ควรมีแถบสุขภาพที่ด้านบนของลูก ลูกบอลควรเสียสุขภาพในขณะที่สัมผัสกับผนัง ด้วยตรรกะนี้ มันไม่สมเหตุสมผลเลยที่จะทำให้ลูกบอลเด้งกลับจากกำแพง ดังนั้นเมื่อสุขภาพเป็น 0 เกมจะจบลงและเราควรสลับไปที่เกมผ่านหน้าจอ ไปเลย:
int maxHealth = 100; float health = 100; float healthDecrease = 1; int healthBarWidth = 60; ... void gameScreen() { ... drawHealthBar(); ... } ... void drawHealthBar() { // Make it borderless: noStroke(); fill(236, 240, 241); rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth, 5); if (health > 60) { fill(46, 204, 113); } else if (health > 30) { fill(230, 126, 34); } else { fill(231, 76, 60); } rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth*(health/maxHealth), 5); } void decreaseHealth(){ health -= healthDecrease; if (health <= 0){ gameOver(); } }
และนี่คือการดำเนินการง่ายๆ:
เราสร้าง health
ตัวแปรระดับโลกเพื่อรักษาสุขภาพของลูกบอล จากนั้นจึงสร้างเมธอด drawHealthBar()
ซึ่งวาดสี่เหลี่ยมสองรูปบนลูกบอล อันแรกคือแถบสุขภาพพื้นฐาน อีกอันหนึ่งคือแถบที่ใช้งานซึ่งแสดงสุขภาพปัจจุบัน ความกว้างของส่วนที่สองเป็นไดนามิก และคำนวณด้วย healthBarWidth*(health/maxHealth)
อัตราส่วนของสุขภาพปัจจุบันของเราเทียบกับความกว้างของแถบสุขภาพ สุดท้ายสีเติมจะถูกกำหนดตามค่าของสุขภาพ สุดท้ายแต่ไม่ท้ายสุด คะแนน :
... void gameOverScreen() { background(0); textAlign(CENTER); fill(255); textSize(30); text("Game Over", height/2, width/2 - 20); textSize(15); text("Click to Restart", height/2, width/2 + 10); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { ... // added another value at the end of the array int[] randWall = {width, randY, wallWidth, randHeight, 0}; ... } } void watchWallCollision(int index) { ... int wallScored = wall[4]; ... if (ballX > gapWallX+(gapWallWidth/2) && wallScored==0) { wallScored=1; wall[4]=1; score(); } } void score() { score++; } void printScore(){ textAlign(CENTER); fill(0); textSize(30); text(score, height/2, 50); }
เราต้องยิงประตูเมื่อบอลผ่านกำแพง แต่เราต้องเพิ่มคะแนนสูงสุด 1 คะแนนต่อกำแพง หมายความว่า ถ้าบอลผ่านกำแพงมากกว่าย้อนและส่งอีก ไม่ควรบวกคะแนนอื่น เพื่อให้บรรลุสิ่งนี้ เราได้เพิ่มตัวแปรอื่นให้กับ gap wall array ภายใน arraylist ตัวแปรใหม่เก็บ 0
ถ้าลูกบอลยังไม่ผ่านกำแพงนั้นและ 1
ถ้าผ่านกำแพงนั้น จากนั้น เราแก้ไข watchWallCollision()
เราได้เพิ่มเงื่อนไขที่ยิงวิธี score()
และทำเครื่องหมายว่ากำแพงว่าผ่านเมื่อลูกบอลผ่านกำแพงที่ไม่เคยผ่านมาก่อน
ตอนนี้เราใกล้ถึงจุดสิ้นสุดแล้ว สิ่งสุดท้ายที่ต้องทำคือใช้การ click to restart
เกมใหม่ผ่านหน้าจอ เราจำเป็นต้องตั้งค่าตัวแปรทั้งหมดที่เราใช้เป็นค่าเริ่มต้นและเริ่มเกมใหม่ นี่คือ:
... public void mousePressed() { ... if (gameScreen==2){ restart(); } } ... void restart() { score = 0; health = maxHealth; ballX=width/4; ballY=height/5; lastAddTime = 0; walls.clear(); gameScreen = 0; }
มาเพิ่มสีสันกันดีกว่า
โว้ว! เรามี Flappy Pong!
รหัสเกมการประมวลผลแบบเต็มสามารถพบได้ที่นี่
การโอนรหัสเกมการประมวลผลไปยังเว็บโดยใช้ p5.js
p5.js เป็นไลบรารี JavaScript ที่มีรูปแบบคล้ายกับภาษาโปรแกรมการประมวลผล ไม่ใช่ห้องสมุดที่สามารถเรียกใช้รหัสการประมวลผลที่มีอยู่ได้ แทน p5.js ต้องเขียนโค้ด JavaScript จริง—คล้ายกับพอร์ต JavaScript ของการประมวลผลที่เรียกว่า Processing.js งานของเราคือการแปลงรหัสการประมวลผลเป็น JavaScript โดยใช้ p5.js API ไลบรารีมีชุดของฟังก์ชันและไวยากรณ์ที่คล้ายกับการประมวลผล และเราต้องทำการเปลี่ยนแปลงบางอย่างในโค้ดของเราเพื่อให้ทำงานใน JavaScript แต่เนื่องจากทั้งการประมวลผลและ JavaScript มีความคล้ายคลึงกันกับ Java จึงมีการก้าวกระโดดน้อยกว่าที่คิด . แม้ว่าคุณจะไม่ใช่นักพัฒนา JavaScript การเปลี่ยนแปลงนั้นก็ไม่สำคัญ และคุณควรติดตามได้ดี
ก่อนอื่น เราต้องสร้าง index.html
อย่างง่าย และเพิ่ม p5.min.js
ในส่วนหัวของเรา เราต้องสร้างไฟล์อื่นที่เรียกว่า flappy_pong.js
ซึ่งจะเป็นที่เก็บโค้ดที่แปลงแล้วของเรา
<html> <head> <title>Flappy Pong</title> <script tyle="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.19/p5.min.js"></script> <script tyle="text/javascript" src="flappy_pong.js"></script> <style> canvas { box-shadow: 0 0 20px lightgray; } </style> </head> <body> </body> </html>
กลยุทธ์ของเราในขณะแปลงโค้ดควรคัดลอกและวางโค้ดทั้งหมดของเราลงใน flappy_pong.js
แล้วทำการเปลี่ยนแปลงทั้งหมด และนั่นคือสิ่งที่ฉันทำ และนี่คือขั้นตอนที่ฉันใช้เพื่ออัปเดตโค้ด:
Javascript เป็นภาษาที่ไม่ได้พิมพ์ (ไม่มีการประกาศประเภทเช่น
int
และfloat
) ดังนั้นเราต้องเปลี่ยนการประกาศประเภททั้งหมดเป็นvar
ไม่มี
void
ใน Javascript เราควรเปลี่ยนการfunction
ทั้งหมดเราจำเป็นต้องลบการประกาศประเภทของอาร์กิวเมนต์ออกจากฟังก์ชันลายเซ็น (เช่น
void wallMover(var index) {
เพื่อfunction wallMover(index) {
)ไม่มี
ArrayList
ใน JavaScript แต่เราสามารถบรรลุสิ่งเดียวกันได้โดยใช้อาร์เรย์ JavaScript เราทำการเปลี่ยนแปลงดังต่อไปนี้:-
ArrayList<int[]> walls = new ArrayList<int[]>();
ถึงvar walls = [];
-
walls.clear();
กับwalls = [];
-
walls.add(randWall);
ไปที่walls.push(randWall);
-
walls.remove(index);
กับwalls.splice(index,1);
-
walls.get(index);
กับwalls[index]
-
walls.size()
กับwalls.length
-
เปลี่ยนการประกาศอาร์เรย์
var randWall = {width, randY, wallWidth, randHeight, 0};
ถึงvar randWall = [width, randY, wallWidth, randHeight, 0];
ลบคำหลัก
public
ทั้งหมดย้ายการประกาศ
color(0)
ทั้งหมดไปยังfunction setup()
เนื่องจากจะไม่มีการกำหนดcolor()
ก่อนการเรียกsetup()
เปลี่ยน
size(500, 500);
เพื่อcreateCanvas(500, 500);
เปลี่ยนชื่อ
function gameScreen(){
เป็นอย่างอื่นเช่นfunction gamePlayScreen(){
เพราะเรามีตัวแปรส่วนกลางชื่อgameScreen
แล้ว เมื่อเราทำงานกับการประมวลผล ตัวหนึ่งเป็นฟังก์ชัน และอีกตัวเป็นตัวแปรint
แต่ JavaScript ทำให้สิ่งเหล่านี้สับสนเนื่องจากไม่ได้พิมพ์เช่นเดียวกันสำหรับ
score()
ฉันเปลี่ยนชื่อเป็นaddScore()
คุณสามารถดูโค้ด JavaScript แบบเต็มที่ครอบคลุมทุกอย่างในบทช่วยสอนการประมวลผลนี้ได้ที่นี่
กำลังประมวลผลรหัสเกม: คุณเองก็ทำได้
ในบทช่วยสอนการประมวลผลนี้ ฉันพยายามอธิบายวิธีสร้างเกมที่ง่ายมาก อย่างไรก็ตาม สิ่งที่เราทำในบทความนี้เป็นเพียงส่วนเล็กๆ ของภูเขาน้ำแข็ง ด้วยภาษาโปรแกรมการประมวลผล คุณสามารถทำอะไรก็ได้ ในความคิดของฉัน มันเป็นเครื่องมือที่ดีที่สุดในการตั้งโปรแกรมสิ่งที่คุณกำลังจินตนาการ ความตั้งใจจริงของฉันกับบทช่วยสอนการประมวลผลนี้ไม่ใช่การสอนการประมวลผลและการสร้างเกม เพื่อพิสูจน์ว่าการเขียนโปรแกรมไม่ได้ยากขนาดนั้น การสร้างเกมของคุณเองไม่ใช่แค่ความฝัน ฉันต้องการแสดงให้คุณเห็นว่าคุณทำได้โดยใช้ความพยายามและความกระตือรือร้นเพียงเล็กน้อย ฉันหวังว่าบทความทั้งสองนี้จะสร้างแรงบันดาลใจให้ทุกคนได้ลองเขียนโปรแกรมดู