到完整實現(xiàn))
簡介基于Python的pygame庫實現(xiàn)的煙花模擬代碼包面向初中級Python學(xué)習(xí)者、游戲開發(fā)入門者及圖形動畫愛好者適合作為趣味編程練習(xí)或課堂演示素材。代碼由主程序入口、粒子類模塊和背景圖片構(gòu)成同時包含編譯生成的pyc備用文件與項目配置忽略文件共6個文件、壓縮包約15.46MB下載解壓后即可運行觀察效果。項目通過隨機坐標(biāo)、顏色和多對象循環(huán)模擬煙花從發(fā)射、升空到爆炸、消散的完整過程并采用面向?qū)ο蠓绞綄⑾嚓P(guān)邏輯封裝在粒子類中可繼續(xù)加入音效、鼠標(biāo)點擊控制、多種煙花類型甚至多線程并發(fā)展示。已有400人參與學(xué)習(xí)下載。認(rèn)真閱讀主程序與粒子類實現(xiàn)有助于理解pygame的事件處理、屏幕刷新、對象狀態(tài)更新等核心機制也能掌握用Python做簡單2D動畫的基本思路修改坐標(biāo)、顏色、數(shù)量等參數(shù)還能快速看到不同視覺效果。1. 煙花動畫怎么做從需求拆解到技術(shù)選型我想先聊一個現(xiàn)象——很多剛接觸Python的人學(xué)完語法、變量、循環(huán)之后會陷入一種“我能寫代碼但寫不出東西”的尷尬期。語法懂函數(shù)會調(diào)但真要獨立做個小玩意兒腦子里一片空白。這時候最適合練手的就是這種“效果型小項目”煙花代碼。這個項目的本質(zhì)是用Python模擬一次煙花從發(fā)射、升空到炸裂、飄落的全過程。聽起來像圖形學(xué)其實核心就兩件事一是“粒子系統(tǒng)”一堆小點各自的運動軌跡二是“動畫刷新”每幀重畫一次畫面。這兩個概念在游戲開發(fā)、數(shù)據(jù)可視化、模擬仿真里都是基本功。所以別看它只是個“放煙花”的小程序拆開揉碎了講能幫你打通從“寫語法”到“做作品”的關(guān)鍵一步。技術(shù)選型上我建議優(yōu)先用Python自帶的turtle庫。原因有三個第一它是標(biāo)準(zhǔn)庫裝好Python就自帶不需要額外pip安裝任何依賴第二turtle的畫布模型足夠直觀每一個煙花粒子都可以看成一只“畫筆”移動、變色、消失邏輯和現(xiàn)實世界的煙花幾乎一一對應(yīng)第三它對新手極其友好即使你完全沒接觸過圖形編程也能在半小時內(nèi)跑出第一個像樣的效果。當(dāng)然你也可以選pygame它性能更強、控制更細(xì)適合做更復(fù)雜的交互動畫。但對“煙花代碼”這個需求來說pygame的窗口管理、事件循環(huán)、圖片加載這些機制反而會分散你對核心邏輯的注意力。我的建議是先用turtle把粒子系統(tǒng)的思路打通后面真有性能需求再遷移到pygame一點不難——粒子運動的核心代碼幾乎可以平移過去。2. 環(huán)境準(zhǔn)備與基礎(chǔ)知識點裝好Python就能開工動手之前先把環(huán)境搞利索。Python的安裝本身不復(fù)雜但有幾個細(xì)節(jié)容易讓新手卡住我逐個說。2.1 Python安裝注意兩個容易被忽略的勾選項去Python官網(wǎng)下載對應(yīng)系統(tǒng)版本的安裝包這個誰都會。真正關(guān)鍵的是安裝第一步那個勾選框——“Add Python to PATH”一定要勾上。如果你沒勾裝完在命令行里輸入python會提示“不是內(nèi)部或外部命令”到時候還得手動配環(huán)境變量太折騰。另外建議選“Customize installation”進到下一步把“Install for all users”也選上省得之后因為權(quán)限問題報錯。裝完之后驗證一下打開命令行Windows用WinR輸入cmdmacOS和Linux用終端輸入python --version能輸出版本號就說明裝好了。如果提示找不到命令大概率就是PATH沒配好重新運行安裝包修復(fù)一下勾上PATH再裝就行。2.2 turtle庫的運行邏輯把畫布想象成一塊白板turtle從入門角度說就是一只“海龜”在一個窗口里爬爬過的軌跡就是畫出來的線。這個比喻很形象但寫煙花的時候你會發(fā)現(xiàn)我們需要控制的不是一只海龜而是幾十上百只“同時”在動。這里就要用到turtle的一個關(guān)鍵機制turtle本身支持多實例也就是說你可以創(chuàng)建多個海龜對象它們互不干擾。所以煙花代碼的基本結(jié)構(gòu)就出來了每一發(fā)煙花彈升空時我們創(chuàng)建一個海龜對象炸裂時把這一發(fā)煙花拆成幾十個海龜對象每個粒子一個對象各自朝不同的方向飛。turtle內(nèi)部會自動管理這些對象的刷新你只需要在每一幀里更新它們的位置就行。有幾個基礎(chǔ)概念我需要提前說清楚后面代碼里會反復(fù)用坐標(biāo)系統(tǒng)turtle的默認(rèn)畫布是400x400的窗口坐標(biāo)原點在窗口中心x軸向右為正y軸向上為正。做煙花時發(fā)射點通常在底部所以初始y坐標(biāo)是負(fù)數(shù)向上飛行會讓y不斷增加。角度與方向海龜有“朝向”的概念用setheading(角度)設(shè)置朝向0度朝右90度朝上180度朝左270度朝下。煙花粒子朝各個方向飛散本質(zhì)上就是給每個粒子設(shè)置不同的朝向角度。抬筆與落筆penup()讓海龜移動時不畫線pendown()恢復(fù)畫線。做煙花時粒子飛行過程要畫軌跡但瞬移時要抬筆否則會留下亂七八糟的連接線。3. 逐步實現(xiàn)煙花效果從一發(fā)到一整屏下面進入正題。我會分三個版本講解先是單發(fā)煙花讓你把核心機制跑通再加循環(huán)實現(xiàn)連續(xù)發(fā)射最后加拖尾和隨機顏色讓畫面真正“像那么回事”。每個版本都能獨立運行你可以在任意版本基礎(chǔ)上繼續(xù)改。3.1 單發(fā)煙花理解粒子系統(tǒng)的核心循環(huán)先看一段能直接跑的代碼它實現(xiàn)的效果是一顆煙花彈從屏幕底部中央升空飛到接近頂部的位置炸開成30個粒子粒子受“重力”影響下落并逐漸消失。import turtle import random import time # 初始化畫布和基礎(chǔ)配置 screen turtle.Screen() screen.setup(800, 600) # 窗口寬800高600 screen.bgcolor(black) # 夜空背景 screen.tracer(0) # 關(guān)閉自動刷新手動控制動畫幀率 # 煙花彈一個從底部升空的小球 rocket turtle.Turtle() rocket.color(white) rocket.shape(circle) rocket.penup() rocket.goto(0, -250) # 從底部中央出發(fā) rocket.setheading(90) # 朝上飛 # 第一段升空 while rocket.ycor() 180: # 升到y(tǒng)180的位置爆炸 rocket.forward(5) screen.update() # 手動刷新畫面 time.sleep(0.005) # 控制動畫速度 # 第二段爆炸生成粒子 particles [] for _ in range(30): p turtle.Turtle() p.color(orange) p.shape(circle) p.penup() p.goto(rocket.xcor(), rocket.ycor()) # 所有粒子從爆炸點出發(fā) angle random.randint(0, 360) # 每個粒子隨機方向 p.setheading(angle) p.speed(0) # 速度交給下面的forward控制 particles.append(p) # 第三段粒子飛散并受重力影響 # 這里用一個簡單的物理模型每個粒子每幀先按當(dāng)前方向移動然后“速度”衰減 velocities [random.uniform(3, 8) for _ in range(30)] # 每個粒子的初始速度 for frame in range(60): # 一共更新60幀 for i, p in enumerate(particles): # 速度衰減模擬空氣阻力 velocities[i] * 0.98 p.forward(velocities[i]) # 模擬重力每幀把粒子的y坐標(biāo)往下拉一點 # 通過改變方向來實現(xiàn)越接近水平線重力影響越明顯 current_heading p.heading() if current_heading 90 and current_heading 270: # 粒子在向下半?yún)^(qū)不額外處理 pass else: # 粒子在上半?yún)^(qū)讓它的朝向逐漸向下偏 p.setheading(current_heading 6) screen.update() time.sleep(0.01) time.sleep(2) turtle.done()這個版本雖然能跑但有幾個明顯的問題粒子不會消失、爆炸形態(tài)不夠圓潤、重力效果比較生硬。別急這只是讓你理解流程的“骨架版”下面我逐步優(yōu)化。3.2 循環(huán)發(fā)射從一次爆炸到持續(xù)煙花秀單發(fā)煙花跑通之后自然想到要“多放幾發(fā)”。這時需要把上面的邏輯封裝成函數(shù)再通過循環(huán)控制發(fā)射節(jié)奏。import turtle import random import time screen turtle.Screen() screen.setup(800, 600) screen.bgcolor(black) screen.tracer(0) # 把所有粒子統(tǒng)一放到一個列表里管理 all_particles [] def create_explosion(x, y, color, particle_count30): 在指定位置生成一場爆炸 particles [] for _ in range(particle_count): p turtle.Turtle() p.color(color) p.shape(circle) p.penup() p.goto(x, y) angle random.randint(0, 360) p.setheading(angle) p.speed(0) particles.append(p) all_particles.extend(particles) def launch_rocket(target_y): 發(fā)射一顆煙花彈并返回爆炸點坐標(biāo) rocket turtle.Turtle() rocket.color(white) rocket.shape(circle) rocket.penup() rocket.goto(random.randint(-300, 300), -250) rocket.setheading(90) while rocket.ycor() target_y: rocket.forward(5) screen.update() time.sleep(0.003) x, y rocket.xcor(), rocket.ycor() rocket.clear() rocket.hideturtle() return x, y # 主循環(huán)每隔一段時間隨機放一炮 for round_num in range(10): target_y random.randint(100, 250) x, y launch_rocket(target_y) color random.choice([red, yellow, cyan, pink, lime]) create_explosion(x, y, color, particle_countrandom.randint(20, 40)) # 更新并顯示粒子飛散 for frame in range(50): for p in all_particles: p.forward(4) p.setheading(p.heading() random.uniform(-3, 3)) # 加一點隨機擾動 screen.update() time.sleep(0.01) # 清理本輪的粒子 for p in all_particles: p.clear() p.hideturtle() all_particles.clear() turtle.done()這段代碼把升空和爆炸拆成了兩個函數(shù)主循環(huán)里“發(fā)射-爆炸-飛散-清理”一氣呵成。這里有個小技巧用random.uniform(-3, 3)給每個粒子加隨機擾動爆炸形態(tài)會從“標(biāo)準(zhǔn)的圓”變成“不規(guī)則的、有生命感的炸裂效果”——自然界沒有完美的球形煙花全是這種隨機擾動帶來的靈動感。3.3 帶拖尾和漸隱的高級效果讓畫面真正出彩前面兩版能做出來但顏色單調(diào)、粒子生硬。要讓畫面真正“驚艷”需要解決三個問題粒子漸隱消失、飛行拖尾、顏色漸變。先說漸隱。turtle的顏色支持RGBA模式紅綠藍(lán)透明度但默認(rèn)RGB模式下透明度不生效。好在我們可以用另一個思路——讓粒子在最后一幀“原地消失”并且越到后期速度越慢模擬出“燃盡”的感覺。另一種更漂亮的做法是讓粒子的顏色逐漸變暗這需要實時調(diào)整畫筆顏色。def fade_particle(p, brightness): 調(diào)整粒子的亮度brightness取值范圍0-1 r, g, b p.pencolor() p.color(r * brightness, g * brightness, b * brightness)這個函數(shù)每幀調(diào)用時傳入一個遞減的brightness值粒子的顏色就會平滑變暗最終“熄滅”。注意turtle的pencolor()返回的是浮點顏色值所以可以做乘法運算。拖尾效果相對復(fù)雜。簡單方案是讓粒子每幀同時畫一條“細(xì)線”而不是一個“圓點”線的長度就是拖尾長度。但更推薦的做法是用“半透明殘影”來近似新建一塊透明畫布每幀把整個畫面填充為帶有透明度的黑色相當(dāng)于“擦除”一大部分但留下一點殘影這樣粒子移動的路徑會留下淡淡的痕跡形成天然的拖尾效果。聽起來復(fù)雜其實核心代碼就幾行# 在每次刷新前畫一個半透明的黑色矩形蓋住整個畫面 screen.bgcolor(black) # 確保底色是黑色 overlay turtle.Turtle() overlay.color(0, 0, 0) overlay.penup() overlay.goto(-400, 300) overlay.pendown() overlay.fillcolor(0, 0, 0) overlay.begin_fill() for _ in range(2): overlay.forward(800) overlay.right(90) overlay.forward(600) overlay.right(90) overlay.end_fill()但這個方案有個副作用是覆蓋圖層會擋住粒子本身。更好的實現(xiàn)方式是每幀調(diào)用screen.bgcolor()的時候設(shè)置透明度但turtle對透明度的支持比較有限。所以我的實際建議是拖尾用“粒子數(shù)量的層級衰減”來模擬——每個粒子在生命周期內(nèi)分裂出一個“殘影粒子”殘影速度更慢、顏色更暗、提前消失。兩三層疊加起來視覺上就是拖尾。我完整版的代碼放在下面注釋部分說明了每一塊的邏輯。直接跑就能看到比較驚艷的煙花秀。import turtle import random import time screen turtle.Screen() screen.setup(800, 600) screen.bgcolor(black) screen.tracer(0) COLORS [red, orange, yellow, green, cyan, blue, purple, pink, white] class Particle: 煙花粒子類包含位置、速度、方向、顏色、生命值 def __init__(self, x, y, angle, speed, color, life): self.t turtle.Turtle() self.t.color(color) self.t.shape(circle) self.t.shapesize(0.8) # 粒子大小 self.t.penup() self.t.goto(x, y) self.t.setheading(angle) self.angle angle self.speed speed self.life life # 生命值每幀減1 self.max_life life self.color color def update(self): 更新粒子位置和狀態(tài) self.life - 1 if self.life 0: self.t.clear() self.t.hideturtle() return False # 速度衰減 隨機擾動 重力 self.speed * 0.98 self.angle random.uniform(-5, 5) self.t.setheading(self.angle) self.t.forward(self.speed) # 重力拉低y坐標(biāo)簡單模擬直接調(diào)整y x, y self.t.position() self.t.goto(x, y - 1) # 顏色漸隱 r, g, b self.t.pencolor() ratio max(0.1, self.life / self.max_life) self.t.color(r * ratio, g * ratio, b * ratio) return True def create_firework(x, y, color, count40): 在x,y位置生成一場煙花爆炸 particles [] for _ in range(count): angle random.uniform(0, 360) speed random.uniform(5, 12) life random.randint(40, 80) particles.append(Particle(x, y, angle, speed, color, life)) return particles def launch(): 隨機位置發(fā)射一發(fā)煙花返回爆炸坐標(biāo)和顏色 # 煙花彈白色小球升空 rocket turtle.Turtle() rocket.color(white) rocket.shape(circle) rocket.shapesize(0.6) rocket.penup() launch_x random.randint(-300, 300) rocket.goto(launch_x, -250) rocket.setheading(90) target_y random.randint(120, 250) while rocket.ycor() target_y: rocket.forward(6) screen.update() time.sleep(0.003) x, y rocket.xcor(), rocket.ycor() rocket.clear() rocket.hideturtle() return x, y, random.choice(COLORS) # 主事件循環(huán)連續(xù)發(fā)射10發(fā) all_particles [] for round_num in range(10): x, y, color launch() new_particles create_firework(x, y, color, countrandom.randint(30, 50)) all_particles.extend(new_particles) # 持續(xù)更新直到所有粒子生命耗盡 while all_particles: alive_particles [] for p in all_particles: if p.update(): alive_particles.append(p) all_particles alive_particles screen.update() time.sleep(0.02) turtle.done()這個版本的亮點在Particle類把粒子的“位置、速度、方向、顏色、生命值”統(tǒng)一封裝更新邏輯集中在update()里主循環(huán)只管“生成粒子 - 更新 - 清理死掉的粒子”。這種寫法本身就是游戲開發(fā)里“實體組件”模型的雛形后面你做復(fù)雜項目也會用到同樣的模式。4. 核心細(xì)節(jié)與美化方向從“能跑”到“好看”代碼能跑只是第一步“好看”才是煙火秀的目標(biāo)。我在調(diào)這個項目的過程中總結(jié)了幾個直接影響觀感的細(xì)節(jié)每一個都是踩過坑才摸索出來的。4.1 參數(shù)調(diào)優(yōu)速度、密度、生命值是有講究的粒子速度決定了爆炸半徑。速度5-12配合生命值40-80在800x600的窗口里能形成直徑大約100-200像素的爆炸團。如果你想要更“猛烈”的視覺效果可以把速度調(diào)到8-15、生命值調(diào)到80-120但這樣粒子會飛得太遠(yuǎn)甚至飛出屏幕邊界。我建議把窗口尺寸和粒子參數(shù)做個“綁定”窗口寬800時速度上限12、生命80是視覺最舒適的范圍。顏色搭配同樣有規(guī)律。純隨機選色容易產(chǎn)生“亂七八糟”的感覺。我發(fā)現(xiàn)三種配色策略效果最好超 ——同色系漸變比如深紅到粉紅適合模擬真實煙花撞色組合青色和橙色、紫色和黃色適合電競風(fēng)格單一亮色加白色粒子混在里面層次感最強。你可以在COLORS列表里只保留同一色系的不同深淺效果立刻提升一個檔次。還有粒子數(shù)量。30個以下顯得寡淡50個以上開始卡頓。不是turtle跑不動而是屏幕刷新和顏色計算的成本上去了。穩(wěn)妥的做法是控制每次爆炸在35-45個粒子之間用“多發(fā)齊射”來營造密集感而不是靠單發(fā)粒子數(shù)硬堆。4.2 結(jié)構(gòu)優(yōu)化封裝成函數(shù)為后續(xù)擴展留好接口我寫這個項目時特別注意把代碼分成三個層次底層是Particle類的運動邏輯中間是create_firework和launch這類具體動作最上面是主循環(huán)控制節(jié)奏。這樣分層的好處是你想加“雙擊爆炸”煙花升到一半再分成幾簇、“寫字煙花”粒子排成文字形狀、“聲音同步”放BGM時跟著節(jié)奏發(fā)射只需要新寫一個函數(shù)完全不用動原有代碼。比如我后來加了個“雙子煙花”效果在一發(fā)煙花爆炸到一半時從兩個邊緣再各爆一次。實現(xiàn)起來就是在主循環(huán)里判斷粒子生命值小于30時調(diào)用兩次create_firework即可。這種擴展能力就是封裝帶來的紅利。4.3 致敬經(jīng)典再聊兩句pygame的進階方案如果你想挑戰(zhàn)更高性能、更多交互效果pygame值得一試。turtle版的核心思想——粒子系統(tǒng)、重力、漸隱——在pygame里完全復(fù)用。區(qū)別在于pygame用Surface畫布和pixel像素點代替了海龜對象可以同時顯示成千上萬個粒子不掉幀pygame有自己的時鐘控制pygame.time.Clock()動畫節(jié)奏更精確pygame還能加載圖片、播放聲音可以做出“煙花音樂”的完整作品。但我不建議一上來就沖pygame。先把這個turtle版調(diào)明白“粒子系統(tǒng)”這個核心概念就會內(nèi)化成你自己的東西。到時候從turtle切pygame你會發(fā)現(xiàn)唯一的門檻只剩pygame的API語法思想早就通了。5. 常見問題與排查技巧實錄寫煙花代碼的過程中你十有八九會遇到下面這些狀況。我把它們整理成速查表附帶排查思路省得你一個個去搜?,F(xiàn)象可能原因排查與解決窗口一閃而過沒看到煙花動畫循環(huán)結(jié)束后窗口被自動關(guān)閉代碼末尾加上turtle.done()它會保持窗口不關(guān)閉畫面卡頓嚴(yán)重、粒子飛得慢粒子數(shù)量太多或使用了大量顏色計算降低粒子數(shù)量優(yōu)化顏色更新邏輯不要每幀都計算顏色粒子拖著一條亂糟糟的線到處跑粒子海龜沒抬筆所有粒子創(chuàng)建后要penup()否則移動時留下軌跡連線煙花彈直接穿過目標(biāo)點沒爆炸while循環(huán)的退出條件沒寫好檢查while rocket.ycor() target_y的邊界條件確保退出循環(huán)后能拿到爆炸點坐標(biāo)顏色漸變時出現(xiàn)紅色警告或顏色異常turtle的顏色值超出了0-1或0-255范圍漸隱計算后檢查r,g,b值用min(1, max(0, value))限制范圍粒子飛出屏幕外不回來沒有邊界檢測在update()里判斷粒子坐標(biāo)是否超出窗口范圍超出則直接清掉程序運行慢動畫不流暢time.sleep()的延遲設(shè)置太大微調(diào)sleep值建議0.01-0.02s。太小會閃爍太大會卡異常報錯“tuple object has no attribute”調(diào)用pencolor()返回值類型判斷錯誤turtle的pencolor()返回的元組可能是整數(shù)或浮點統(tǒng)一轉(zhuǎn)換再計算還有一個新手很容易踩的坑不要在主循環(huán)里創(chuàng)建新的turtle對象之后忘記銷毀它們。每創(chuàng)建一個海龜對象turtle都會把它加入內(nèi)部管理列表數(shù)量過多時即便hideturtle()也依然占用資源。我建議每輪煙花結(jié)束后用clear()hideturtle()del三個組合暴力清理確保下一輪有干凈的環(huán)境。調(diào)試方面我有個習(xí)慣碰到“粒子不動了”這類問題先在粒子更新邏輯里加一行print(p.t.position(), p.life, p.speed)看看坐標(biāo)和生命值的變化規(guī)律??吹綌?shù)據(jù)問題就藏不住了。這比悶頭改代碼效率高十倍。6. 我踩過的一次印象深刻的坑最后分享一個具體的事故。有一次我把煙花從turtle版本往pygame版本遷移運行后粒子飛得特別快根本不像是煙花更像霰彈槍發(fā)射。調(diào)試了半天發(fā)現(xiàn)問題出在時鐘控制上turtle版本的sleep(0.01)是絕對延時而pygame的clock.tick(60)控制的是每幀不短于16毫秒。我把速度參數(shù)直接照搬沒有考慮幀率差異導(dǎo)致粒子每幀位移量比預(yù)期大了一倍多。后來解決辦法很簡單把粒子速度統(tǒng)一換算成“每秒的像素位移”而不是“每幀的像素位移”。也就是在pygame里用delta_time乘以速度值而不是直接用速度值移動。這個教訓(xùn)讓我意識到同樣的邏輯在不同框架下參數(shù)的量綱和含義可能完全不同遷移代碼時一定要重新審視這些單位。類似的細(xì)節(jié)還有不少但我覺得寫代碼本來就是“踩坑-排查-解決-變強”的循環(huán)。煙花這個項目雖然小但它會把動畫、隨機、事件循環(huán)、對象管理這些基礎(chǔ)概念串起來等你把這一整套跑通了再回頭看那些語法書會發(fā)現(xiàn)自己已經(jīng)跨過“能看懂”和“能寫出來”之間那道坎了。寫完之后試著改改參數(shù)、換換顏色、加幾種爆炸模式玩出你自己的版本。代碼這東西不動手永遠(yuǎn)覺得玄動手改上幾版它就是你的了。本文還有配套的精品資源點擊獲取