تثبيت المكتبات المطلوبة
قبل أن نبدأ في كتابة الكود، نحتاج إلى التأكد من توفر المكتبات اللازمة. افتح موجه الأوامر (Terminal) وقم بتثبيت `numpy` و `matplotlib`.
pip install numpy matplotlib pillow
استيراد المكتبات وإعداد الشاشة
في البداية، نستورد المكتبات ونقوم بإعداد مساحة الرسم (Figure) وإخفاء المحاور لكي يبدو المشهد كفيلم.
import numpy as np # استيراد مكتبة العمليات الرياضية (التي تحوي دوال الجيب)
import matplotlib.pyplot as plt # استيراد مكتبة الرسم البياني لإنشاء الأشكال
import matplotlib.animation as animation # استيراد مكتبة التحريك لإنشاء الأنيميشن
# إعداد الرسم
fig, ax = plt.subplots(figsize=(8, 5)) # إنشاء نافذة الرسم بمقاس 8x5 بوصة
ax.set_xlim(0, 10) # تحديد حدود المحور الأفقي من 0 إلى 10
ax.set_ylim(0, 10) # تحديد حدود المحور الرأسي من 0 إلى 10
ax.set_aspect('equal') # جعل نسبة الطول والعرض متساوية حتى لا تتشوه الأشكال
ax.axis('off') # إخفاء المحاور والأرقام الجانبية لتبدو كشاشة سينمائية
fig.patch.set_facecolor('white') # جعل لون خلفية النافذة أبيض
تجهيز المقاتلين (الأسلاك)
سنقوم برسم المقاتل الأزرق (يسار) والمقاتل الأصفر (يمين) باستخدام خطوط ودوائر بسيطة. ننشئ متغيرات فارغة سنقوم بتحديثها لاحقاً في كل إطار (frame).
# المقاتل الأزرق (يسار)
head_b, = ax.plot([], [], 'o', color='#1f77b4', markersize=25) # إنشاء الرأس كدائرة زرقاء بحجم 25
body_b, = ax.plot([], [], '-', color='#1f77b4', lw=5) # إنشاء الجسم كخط أزرق بسمك 5
arms_b, = ax.plot([], [], '-', color='#1f77b4', lw=5) # إنشاء الذراعين كخط أزرق بسمك 5
legs_b, = ax.plot([], [], '-', color='#1f77b4', lw=5) # إنشاء الساقين كخط أزرق بسمك 5
# المقاتل الأصفر (يمين)
head_y, = ax.plot([], [], 'o', color='#ff7f0e', markersize=25) # إنشاء الرأس كدائرة صفراء بحجم 25
body_y, = ax.plot([], [], '-', color='#ff7f0e', lw=5) # إنشاء الجسم كخط أصفر بسمك 5
arms_y, = ax.plot([], [], '-', color='#ff7f0e', lw=5) # إنشاء الذراعين كخط أصفر بسمك 5
legs_y, = ax.plot([], [], '-', color='#ff7f0e', lw=5) # إنشاء الساقين كخط أصفر بسمك 5
برمجة دالة التهيئة (Init)
هذه الدالة تقوم بتفريغ البيانات في البداية قبل أن تبدأ الحركة.
def init(): # دالة التهيئة التي تعمل في بداية الأنيميشن فقط
head_b.set_data([], []) # تفريغ إحداثيات رأس الأزرق
body_b.set_data([], []) # تفريغ إحداثيات جسم الأزرق
arms_b.set_data([], []) # تفريغ إحداثيات أذرع الأزرق
legs_b.set_data([], []) # تفريغ إحداثيات سيقان الأزرق
head_y.set_data([], []) # تفريغ إحداثيات رأس الأصفر
body_y.set_data([], []) # تفريغ إحداثيات جسم الأصفر
arms_y.set_data([], []) # تفريغ إحداثيات أذرع الأصفر
legs_y.set_data([], []) # تفريغ إحداثيات سيقان الأصفر
# إرجاع جميع الأجزاء لكي يتم رسمها كإطار فارغ أولي
return head_b, body_b, arms_b, legs_b, head_y, body_y, arms_y, legs_y
محرك الحركة (Update Function)
هذا هو القلب النابض للمشروع. تقوم هذه الدالة بتحديث الإحداثيات لكل مقاتل في كل جزء من الثانية (frame). وتستخدم الدوال الجيبية (sin/cos) لحساب المسافات والركلات لتكوين حركات ناعمة.
def update(frame): # دالة التحديث التي يتم استدعاؤها في كل إطار من الإطارات
# حساب الإحداثي السيني (X) للاقتراب والابتعاد باستخدام دالة الجيب (sin)
base_x_b = 2.5 + np.sin(frame * 0.1) * 1.5 # تحريك المقاتل الأزرق للأمام والخلف
base_x_y = 7.5 - np.sin(frame * 0.1) * 1.5 # تحريك المقاتل الأصفر عكس الأزرق تماماً
# التحقق من المسافة: إذا كانت أقل من 3.5، يصبح المتغير 1 (بدء القتال)، وإلا 0
fighting = 1 if abs(base_x_y - base_x_b) < 3.5 else 0
# تحديث إحداثيات الرأس (الرأس يظل ثابتاً على ارتفاع 6.5 في المحور الصادي)
head_b.set_data([base_x_b], [6.5])
head_y.set_data([base_x_y], [6.5])
# تحديث إحداثيات الجسد (رسم خط عمودي يمتد من النقطة 6 إلى 4)
body_b.set_data([base_x_b, base_x_b], [6, 4])
body_y.set_data([base_x_y, base_x_y], [6, 4])
# حساب قوة الركلات والضربات باستخدام الدوال المثلثية، وتفعيلها فقط وقت القتال (fighting)
kick_b = max(0, np.sin(frame * 0.8)) * fighting # ركلة الأزرق
kick_y = max(0, np.cos(frame * 0.8)) * fighting # ركلة الأصفر، نستخدم cos ليكون هناك تناوب
# تحديث إحداثيات الذراعين مع إضافة تأثير الركل والضرب عند نقطة النهاية للذراع
arms_b.set_data([base_x_b-0.8, base_x_b, base_x_b+0.8 + kick_b*1.5], [5, 5.5, 5.5 + kick_b*0.5])
arms_y.set_data([base_x_y-0.8 - kick_y*1.5, base_x_y, base_x_y+0.8], [5.5 + kick_y*0.5, 5.5, 5])
# تحديث حركة الساقين للأزرق
if kick_b > 0.5: # إذا كانت قيمة الركلة عالية
legs_b.set_data([base_x_b-0.5, base_x_b, base_x_b+1.5], [2, 4, 4.5]) # رفع الساق للركل للأمام
else:
legs_b.set_data([base_x_b-0.5, base_x_b, base_x_b+0.5], [2, 4, 2]) # الوقوف العادي (القدمين على الأرض)
# تحديث حركة الساقين للأصفر
if kick_y > 0.5: # إذا كانت قيمة الركلة عالية
legs_y.set_data([base_x_y-1.5, base_x_y, base_x_y+0.5], [4.5, 4, 2]) # رفع الساق للركل للأمام
else:
legs_y.set_data([base_x_y-0.5, base_x_y, base_x_y+0.5], [2, 4, 2]) # الوقوف العادي (القدمين على الأرض)
# إرجاع العناصر المحدثة لكي يقوم Matplotlib برسم الإطار الجديد
return head_b, body_b, arms_b, legs_b, head_y, body_y, arms_y, legs_y
تشغيل الأنيميشن وحفظه كـ GIF
أخيراً، نقوم بدمج كل شيء باستخدام FuncAnimation ونقوم بتصدير النتيجة النهائية إلى ملف
GIF متحرك يوثق المعركة.
# ربط كل شيء معاً لإنشاء الأنيميشن (المجسم، دالة التحديث، عدد الإطارات 120، دالة التهيئة، وتحديث التغييرات فقط بـ blit)
ani = animation.FuncAnimation(fig, update, frames=120, init_func=init, blit=True, interval=50)
# تحديد اسم الملف النهائي
output_file = 'karate_fight.gif'
# حفظ الأنيميشن في ملف بصيغة GIF باستخدام مكتبة pillow بسرعة 20 إطار في الثانية
ani.save(output_file, writer='pillow', fps=20)
# طباعة رسالة في الكونسول تفيد بنجاح العملية
print(f"تم حفظ الفيديو بنجاح في ملف: {output_file}")