القائمة الرئيسية

الصفحات

برمجة لعبة سباق مثل Need for Speed باستخدام Flutter وPython

How to Code Racing Game Like Need for Speed ​​in Flutter Python، برمجة لعبة سباق باستخدام فلاتر، تصميم لعبة Need for Speed للموبايل،  لعبة سيارات ثنائية الأبعاد Flutter، خادم Python للعب الجماعي، لعبة سباق واقعية بسيطة للمبتدئين، تعلم Flutter وPython من خلال لعبة سيارات، برمجة لعبة سباق سيارات Flutter، تصميم لعبة مثل Need for Speed للموبايل، Flame engine game development، ألعاب سيارات ثنائية الأبعاد باستخدام فلاتر، تطوير لعبة متعددة اللاعبين بلغة بايثون، أفضل ألعاب مفتوحة المصدر باستخدام Flutter وPython، لعبة سباق Flutter، Game Development باستخدام Python، Car Racing Game، Flutter Flame Engine، Python Socket Game Server، Multiplayer Car Game، سباق سيارات واقعي، Flutter 2D Car Racing، تعلم برمجة الألعاب للمبتدئين، برمجة لعبة، Need for Speed، فلاتر، بايثون، برمجة لعبة سباق مثل Need for Speed باستخدام Flutter وPython، ألعاب سيارات ثنائية الأبعاد باستخدام فلاتر، Flutter 2D Car Racing، أفضل ألعاب مفتوحة المصدر،




برمجة لعبة سباق مثل Need for Speed باستخدام Flutter وPython



في عالم تطوير الألعاب الرقمية، تظل ألعاب الآركيد الكلاسيكية مصدر إلهام
 لا ينضب، وتبرز ألعاب السباقات بفيزيائها المميزة وتفاعلاتها الممتعة.
 إذا كنت تطمح لدخول مجال برمجة الألعاب للويب والهواتف وتتساءل عن نقطة
 انطلاق قوية تجمع بين التحدي والإبداع، فإن بناء لعبة سباق ثنائية الأبعاد (2D)
 باستخدام Flutter لواجهة المستخدم وPython للخادم يمثل فرصة ذهبية. 
سيأخذك هذا الدليل الشامل في رحلة خطوة بخطوة، من تصميم عالمك الافتراضي
 ثلاثي الأبعاد وصولاً إلى إطلاق لعبتك مباشرة في متصفحات الويب أو على الأجهزة 
المحمولة، مستعرضين كل جزء رئيسي من العملية، استعد لإتقان فيزياء
 FlameForge2D، برمجة C#، والنشر عبر WebGL/APK لتقدم تجربتك الخاصة للاعبين حول العالم.


خطوات برمجة لعبة سباق مثل Need for Speed على Flutter وPython


لإنشاء لعبة سباق شبيهة بـ Need for Speed على الهاتف المحمول، 
سنعتمد على تقسيم العمل إلى جزئين رئيسيين:
1- الواجهة الرسومية (Front-end) باستخدام Flutter ومحرك الألعاب Flame 
الذي يوفّر أدوات للتحكم بالحركة، الفيزياء، المؤثرات، والصوتيات.
2- الخلفية (Back-end) باستخدام Python، إما لتطوير ذكاء اصطناعي للمنافسين
 أو لإدارة الاتصالات في اللعب الجماعي.
سنبدأ بإعداد المشروع، ثم نبني منطق التحكم في السيارة، ونضيف مؤثرات صوتية 
وبصرية مثل صوت المحرك، الانزلاق، الدخان، وبعدها نربط اللعبة بخادم Python لتوفير تجربة أكثر تفاعلية.

* الأدوات والتقنيات المستخدمة

- Flutter + Flame Engine + FlameForge2D: لإنشاء واجهة المستخدم،
 الرسوم المتحركة، ومحاكاة الفيزياء 2D.
- Python (مع مكتبة FastAPI و WebSockets): لإنشاء خادم اللعبة الذي يدير التواصل في اللعب الجماعي.
- Assets مجانية: لصور السيارات والخلفيات والمؤثرات الصوتية والبصرية.


3. هيكل المشروع
css

nfs_game_flutter_python/
├── flutter_app/
│   ├── lib/
│   │   ├── main.dart
│   │   ├── car_game.dart
│   │   └── multiplayer_client.dart (جديد)
│   ├── pubspec.yaml
│   └── assets/
│       ├── images/
│       │   ├── car.png
│       │   ├── road.png
│       │   ├── smoke_1.png
│       │   └── smoke_2.png
│       └── audio/
│           └── engine_loop.mp3
└── python_server/
    └── server.py
--

4. الجزء الأول: إعداد مشروع Flutter + Flame
* تثبيت Flame
تثبيت Flame ومكتباته الإضافية
افتح سطر الأوامر (Terminal/CMD) وانتقل إلى المجلد الذي تريد إنشاء 
مشروعك فيه، ثم اتبع الخطوات:

# إنشاء مشروع Flutter جديد
flutter create flutter_app

# الانتقال إلى مجلد المشروع
cd flutter_app

# إضافة مكتبات Flame الأساسية، الصوتيات، و FlameForge2D للفيزياء
flutter pub add flame
flutter pub add flame_audio
flutter pub add flutter_svg # مفيد للرسوميات المتجهة (اختياري)
flutter pub add flame_forge2d # لإضافة محرك الفيزياء Box2D
flutter pub add web_socket_channel # للاتصال بخادم الويب سوكيت في Python

--

* main.dart - واجهة اللعبة
dart

import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'car_game.dart'; // استيراد ملف منطق اللعبة الرئيسي

void main() {
  runApp(
    GameWidget(game: CarGame()), // تشغيل اللعبة باستخدام GameWidget
  );
}
--

* تصميم طاولة السباق ومكوناتها
سنقوم ببناء المسار والجدران كأجسام فيزيائية باستخدام
 FlameForge2D لضمان تفاعل واقعي مع السيارة.
* car_game.dart - منطق اللعبة
هذا الملف سيحتوي على معظم منطق اللعبة، بما في ذلك السيارة، المسار، التحكم، والفيزياء.

dart





import 'package:flame/game.dart';
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; // استيراد مكتبة الفيزياء
import 'package:flame_audio/flame_audio.dart'; // لاستيراد الصوتيات
import 'dart:math'; // لدوال الرياضيات مثل cos و sin

// ملاحظة: لاستخدام KeyboardEvents، يجب أن يكون التطبيق قابلاً للتركيز (Focusable)
// في Flutter Web أو Desktop. للتحكم باللمس على الموبايل، سيتم إضافة جزء لاحق.

class CarGame extends Forge2DGame with KeyboardEvents, HasGameRef { // استخدام Forge2DGame للفيزياء
  late CarBody playerCar; // سيارة اللاعب
  double carSpeed = 0;
  double carAngle = 0;

  @override
  Future<void> onLoad() async {
    await super.onLoad(); // استدعاء onLoad الأساسي لـ Forge2DGame

    // تحميل صورة السيارة وإضافتها كـ SpriteComponent داخل CarBody
    // سنقوم بإنشاء CarBody وإضافتها كجسم فيزيائي
    playerCar = CarBody(position: size / 2);
    add(playerCar);

    // إضافة الجدران/حدود المسار كأجسام فيزيائية ثابتة
    add(Ground(gameRef.screenToWorld(Vector2(size.x / 2, size.y)))); // أسفل الشاشة
    add(Ground(gameRef.screenToWorld(Vector2(size.x / 2, 0))));    // أعلى الشاشة
    add(Ground(gameRef.screenToWorld(Vector2(0, size.y / 2)), isVertical: true)); // يسار الشاشة
    add(Ground(gameRef.screenToWorld(Vector2(size.x, size.y / 2)), isVertical: true)); // يمين الشاشة

    // يمكنك استبدال الجدران البسيطة بـ SpriteComponent للطريق نفسه وجدران مرئية.

    // تحميل وتشغيل صوت المحرك
    FlameAudio.audioCache.loadAll(['engine_loop.mp3']);
    FlameAudio.loop('engine_loop.mp3'); // يبدأ صوت المحرك بالتكرار
  }

  // كلاس يمثل جسم السيارة الفيزيائي
  class CarBody extends BodyComponent {
    late SpriteComponent carSprite;
    final Vector2 initialPosition;

    CarBody({required this.initialPosition}) : super(renderBody: false); // لا نرسم الجسم الفيزيائي

    @override
    Future<void> onLoad() async {
      await super.onLoad();
      carSprite = SpriteComponent()
        ..sprite = await gameRef.loadSprite('car.png')
        ..size = Vector2(64, 128) // حجم السيارة
        ..anchor = Anchor.center; // مرسى الـ sprite في المنتصف ليتوافق مع الجسم الفيزيائي
      add(carSprite);
    }

    @override
    Body createBody() {
      final bodyDef = BodyDef(
        position: initialPosition,
        type: BodyType.dynamic, // السيارة جسم ديناميكي (يتحرك ويتفاعل)
        linearDamping: 0.8, // مقاومة حركة خطية لتقليل السرعة تدريجياً
        angularDamping: 0.8, // مقاومة حركة دورانية
      );

      final shape = PolygonShape()..setAsBox(carSprite.size.x / 2 / worldToScreen, carSprite.size.y / 2 / worldToScreen);

      final fixtureDef = FixtureDef(shape, density: 1.0, friction: 0.8, restitution: 0.1); // الكثافة، الاحتكاك، الارتداد

      return world.createBody(bodyDef)..createFixture(fixtureDef);
    }

    @override
    void update(double dt) {
      super.update(dt);
      carSprite.position = body.position * worldToScreen; // تحديث موقع الـ sprite ليتبع الجسم الفيزيائي
      carSprite.angle = body.angle; // تحديث زاوية الـ sprite لتتبع الجسم الفيزيائي
    }
  }

  // كلاس يمثل الأرض أو الجدران الثابتة
  class Ground extends BodyComponent {
    final Vector2 _position;
    final bool isVertical;

    Ground(this._position, {this.isVertical = false});

    @override
    Body createBody() {
      final bodyDef = BodyDef(
        position: _position,
        type: BodyType.static, // الجسم ثابت لا يتحرك بفعل الفيزياء
      );

      Shape shape;
      if (isVertical) {
        shape = PolygonShape()..setAsBox(0.1, 1000); // جدار رأسي رفيع وطويل
      } else {
        shape = PolygonShape()..setAsBox(1000, 0.1); // جدار أفقي رفيع وطويل
      }

      return world.createBody(bodyDef)..createFixture(FixtureDef(shape));
    }
  }

  // التحكم بالسيارة (سلوك قياسي)
  @override
  KeyEventResult onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    final movementForce = 200.0;
    final turningTorque = 50.0;

    if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) {
      playerCar.body.applyForceToCenter(playerCar.body.linearVelocity.normalized() * movementForce);
      // أو استخدام قوة في اتجاه الأمام
      // playerCar.body.applyLinearImpulse(playerCar.body.getTransform().q.asVector2() * movementForce * dt);
    }
    if (keysPressed.contains(LogicalKeyboardKey.arrowDown)) {
      playerCar.body.applyForceToCenter(-playerCar.body.linearVelocity.normalized() * movementForce / 2);
    }
    if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
      playerCar.body.applyTorque(turningTorque); // تطبيق عزم دوران
    }
    if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
      playerCar.body.applyTorque(-turningTorque);
    }

    // لتشغيل الدخان فقط عند التسارع أو الانزلاق
    if (keysPressed.contains(LogicalKeyboardKey.arrowUp) && playerCar.body.linearVelocity.length > 5) {
      // add a smoke effect here
    }

    return KeyEventResult.handled;
  }
}


--

5. الجزء الثاني: إضافة خادم Python للتحكم بالذكاء الاصطناعي أو اللعب الجماعي




* server.py – خادم بسيط باستخدام socket
هذا الخادم سيتلقى تحديثات حالة اللاعبين ويبثها لجميع المتصلين.

python





from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List, Dict
import json
import asyncio # لاستخدام async/await في Python

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []
        self.player_states: Dict[str, Dict] = {} # لتخزين حالة كل لاعب (مثلاً: {"player_id": {"x": ..., "y": ..., "angle": ...}})
        self.next_player_id = 0 # لتوليد معرفات فريدة للاعبين

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        player_id = f"player_{self.next_player_id}"
        self.next_player_id += 1
        self.active_connections.append(websocket)
        self.player_states[player_id] = {"id": player_id, "x": 0, "y": 0, "angle": 0} # حالة ابتدائية

        # إرسال ID اللاعب الخاص به إلى العميل
        await websocket.send_text(json.dumps({"type": "player_id", "id": player_id}))
        # بث الحالة الأولية لجميع اللاعبين الجدد
        await self.broadcast_game_state()
        print(f"[NEW CONNECTION] Player {player_id} connected. Total: {len(self.active_connections)}")

    def disconnect(self, websocket: WebSocket):
        player_id_to_remove = None
        for pid, conn in zip(self.player_states.keys(), self.active_connections):
            if conn == websocket:
                player_id_to_remove = pid
                break
        if player_id_to_remove:
            self.active_connections.remove(websocket)
            self.player_states.pop(player_id_to_remove, None)
            print(f"[DISCONNECT] Player {player_id_to_remove} disconnected. Total: {len(self.active_connections)}")
            asyncio.create_task(self.broadcast_game_state()) # بث حالة اللعبة بعد قطع الاتصال

    async def send_personal_message(self, message: str, websocket: WebSocket):
        await websocket.send_text(message)

    async def broadcast_game_state(self):
        # يرسل حالة جميع اللاعبين إلى جميع العملاء
        message = json.dumps({"type": "game_state", "players": list(self.player_states.values())})
        for connection in self.active_connections:
            await connection.send_text(message)

manager = ConnectionManager()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    player_id = None # لتخزين معرف اللاعب بعد الاتصال
    try:
        # البحث عن معرف اللاعب المرتبط بهذا الـ websocket
        for pid, conn in zip(manager.player_states.keys(), manager.active_connections):
            if conn == websocket:
                player_id = pid
                break

        while True:
            data = await websocket.receive_text()
            if player_id:
                player_update = json.loads(data)
                # تحديث حالة اللاعب في الخادم
                manager.player_states[player_id].update(player_update)
                # بث الحالة الجديدة لجميع اللاعبين
                await manager.broadcast_game_state()
    except WebSocketDisconnect:
        manager.disconnect(websocket)
    except Exception as e:
        print(f"WebSocket Error: {e}")
        manager.disconnect(websocket)

# لتشغيل الخادم:
# 1. تأكد من تثبيت FastAPI و uvicorn و websockets:
#    pip install fastapi uvicorn websockets
# 2. شغل الخادم من مجلد 'python_server' باستخدام:
#    uvicorn server:app --reload --ws websockets


--

6. دمج الشبكة في Flutter

 دمج الشبكة في Flutter (اللعب الجماعي)
الآن سنربط تطبيق Flutter بخادم Python عبر WebSockets.
multiplayer_client.dart - كلاس لإدارة الاتصال
هذا الملف سيتعامل مع كل ما يخص الاتصال بالخادم.
dart




import 'package:web_socket_channel/web_socket_channel.dart';
import 'dart:convert';
import 'package:flutter/material.dart'; // لاستخدام DebugPrint

class MultiplayerClient {
  WebSocketChannel? _channel;
  Function(Map<String, dynamic>)? onGameStateUpdate; // Callback لتحديث حالة اللعبة

  String? playerId; // لتخزين معرف اللاعب الخاص بنا

  Future<void> connect(String url) async {
    try {
      _channel = WebSocketChannel.connect(Uri.parse(url));
      debugPrint('WebSocket connected to $url');

      _channel!.stream.listen(
        (message) {
          final data = jsonDecode(message);
          if (data['type'] == 'player_id') {
            playerId = data['id'];
            debugPrint('Received Player ID: $playerId');
          } else if (data['type'] == 'game_state' && onGameStateUpdate != null) {
            onGameStateUpdate!(data); // تمرير حالة اللعبة المحدثة إلى callback
          }
        },
        onError: (error) => debugPrint('WebSocket Error: $error'),
        onDone: () => debugPrint('WebSocket Disconnected'),
      );
    } catch (e) {
      debugPrint('Failed to connect to WebSocket: $e');
    }
  }

  void sendPlayerState(double x, double y, double angle, double speed) {
    if (_channel != null && _channel!.sink != null && playerId != null) {
      final playerState = {
        'id': playerId,
        'x': x,
        'y': y,
        'angle': angle,
        'speed': speed
      };
      _channel!.sink.add(jsonEncode(playerState));
    }
  }

  void disconnect() {
    _channel?.sink.close();
  }
}



--

* دمج العميل في car_game.dart
سنضيف منطق الاتصال وإدارة اللاعبين الآخرين إلى ملف اللعبة الرئيسي.
Dart

// أضف هذه الاستيرادات
import 'package:flutter/foundation.dart'; // لاستخدام kIsWeb
import 'multiplayer_client.dart'; // استيراد كلاس العميل

// ... (بقية الكود من الأعلى)

class CarGame extends Forge2DGame with KeyboardEvents, HasGameRef {
  // ... (المتغيرات الموجودة سابقاً)

  MultiplayerClient? multiplayerClient;
  Map<String, OtherCarBody> otherPlayers = {}; // لتتبع سيارات اللاعبين الآخرين

  @override
  Future<void> onLoad() async {
    await super.onLoad();

    // ... (كود تحميل السيارة والجدران)

    // تهيئة عميل اللعب الجماعي والاتصال بالخادم
    if (kIsWeb || defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS) {
      multiplayerClient = MultiplayerClient();
      multiplayerClient!.onGameStateUpdate = (gameState) {
        // تحديث مواضع اللاعبين الآخرين
        final List<dynamic> playersData = gameState['players'];
        for (var playerData in playersData) {
          final id = playerData['id'];
          if (id != multiplayerClient!.playerId) { // لا تحدث سيارة اللاعب نفسه
            if (otherPlayers.containsKey(id)) {
              // تحديث سيارة موجودة
              otherPlayers[id]!.body.position = Vector2(playerData['x'], playerData['y']);
              otherPlayers[id]!.body.angle = playerData['angle'];
            } else {
              // إضافة سيارة لاعب جديد
              final newOpponent = OtherCarBody(position: Vector2(playerData['x'], playerData['y']));
              add(newOpponent);
              otherPlayers[id] = newOpponent;
            }
          }
        }
        // إزالة اللاعبين الذين غادروا
        final currentServerPlayers = playersData.map((p) => p['id']).toSet();
        final localPlayers = otherPlayers.keys.toSet();
        final playersToRemove = localPlayers.difference(currentServerPlayers);
        for (var id in playersToRemove) {
          otherPlayers[id]?.removeFromParent();
          otherPlayers.remove(id);
        }
      };

      // ملاحظة: تأكد أن عنوان الخادم صحيح
      // إذا كنت تشغل الخادم محلياً، فاستخدم IP جهازك على الشبكة المحلية للاتصال من الهاتف
      // أو استخدم ngrok إذا كنت تريد الوصول إليه من خارج الشبكة المحلية (انظر قسم النشر)
      multiplayerClient!.connect('ws://localhost:8000/ws');
    }
  }

  @override
  void update(double dt) {
    super.update(dt);
    // ... (كود تحديث السرعة والزاوية)

    // إرسال حالة اللاعب إلى الخادم بشكل مستمر
    multiplayerClient?.sendPlayerState(
      playerCar.body.position.x,
      playerCar.body.position.y,
      playerCar.body.angle,
      playerCar.body.linearVelocity.length,
    );
  }

  // كلاس لتمثيل سيارة لاعب آخر (بدون تحكم مباشر)
  class OtherCarBody extends BodyComponent {
    late SpriteComponent carSprite;
    final Vector2 initialPosition;

    OtherCarBody({required this.initialPosition}) : super(renderBody: false);

    @override
    Future<void> onLoad() async {
      await super.onLoad();
      carSprite = SpriteComponent()
        ..sprite = await gameRef.loadSprite('car.png')
        ..size = Vector2(64, 128)
        ..anchor = Anchor.center;
      add(carSprite);
    }

    @override
    Body createBody() {
      // جسم ثابت مؤقتاً أو يمكن تحديثه من الخادم
      final bodyDef = BodyDef(
        position: initialPosition,
        type: BodyType.kinematic, // جسم يتحرك بواسطة الكود، لا يتأثر بالفيزياء (للسيارات الأخرى)
      );
      final shape = PolygonShape()..setAsBox(carSprite.size.x / 2 / worldToScreen, carSprite.size.y / 2 / worldToScreen);
      return world.createBody(bodyDef)..createFixture(FixtureDef(shape));
    }

    @override
    void update(double dt) {
      super.update(dt);
      carSprite.position = body.position * worldToScreen;
      carSprite.angle = body.angle;
    }
  }
}

--

7. تحسين الرسوميات والمؤثرات
- استخدم مكتبة flame_audio لإضافة صوت المحرك.
- طبّق تأثيرات متقدمة مثل الانزلاق أو الدخان.
- يمكنك تحميل assets من مواقع مثل Kenney.nl أو Itch.io :

* 1. إضافة صوت المحرك باستخدام flame_audio
إضافة صوت المحرك باستخدام flame_audio
الملف موجود بالفعل في car_game.dart ضمن onLoad():
Dart

// ... داخل Future<void> onLoad() async {
    FlameAudio.audioCache.loadAll(['engine_loop.mp3']);
    FlameAudio.loop('engine_loop.mp3'); // يبدأ صوت المحرك بالتكرار
// ...
--




* تأكد من وجود ملف engine_loop.mp3 في flutter_app/assets/audio/. 
يجب أيضاً إضافة المسار في pubspec.yaml:
YAML

# ...
flutter:
  uses-material-design: true
  assets:
    - assets/images/
    - assets/audio/
# ...}
--

* 2. تطبيق مؤثر الانزلاق (Drift Effect)
* الانزلاق يعتمد على السرعة والاتجاه.
 في Box2D، يتم التحكم بالاحتكاك والانزلاق عبر friction وlinearDamping. 
هنا مثال مبسط:
dart

double speed = 0;
double angle = 0;

@override
void update(double dt) {
  super.update(dt);
  
  // تحديث السرعة حسب الضغط
  if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) {
    speed += 100 * dt;
  } else {
    speed *= 0.98; // تخفيف السرعة (احتكاك)
  }

  // دوران السيارة
  if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
    angle -= 2 * dt;
  }
  if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
    angle += 2 * dt;
  }

  // تطبيق الانزلاق
  car.angle = angle;
  car.position += Vector2(speed * dt * cos(angle), speed * dt * sin(angle));
}
--




* 3. إضافة مؤثر الدخان عند الانزلاق أو التسارع
* أضف صور الدخان smoke_1.png, smoke_2.png داخل assets/images
* داخل car_game.dart
dart




// في بداية ملف car_game.dart، تأكد من استيراد هذه المكتبات:
import 'package:flame/game.dart';
import 'package:flame/components.dart';
import 'package:flutter/services.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_audio/flame_audio.dart';
import 'dart:math';
import 'package:flame/particles.dart'; // لاستيراد نظام الجسيمات
import 'package:flame/effects.dart'; // لتأثيرات حركة الجسيمات
import 'package:flutter/material.dart'; // لاستخدام الألوان في Paint

// ... (بقية كود CarGame class والمتغيرات مثل playerCar)

class CarGame extends Forge2DGame with KeyboardEvents, HasGameRef {
  late CarBody playerCar; // سيارة اللاعب
  // ... (بقية المتغيرات مثل multiplayerClient و otherPlayers)

  // لا نحتاج لـ `late TimerComponent smokeTimer;` هنا في هذا الدمج
  // لأننا سنتعامل مع إضافة وإزالة تأثيرات الدخان مباشرة عند الحاجة.

  @override
  Future<void> onLoad() async {
    await super.onLoad();

    // تحميل صورة السيارة وإضافتها كـ SpriteComponent داخل CarBody
    playerCar = CarBody(position: gameRef.screenToWorld(size / 2)); // استخدام gameRef.screenToWorld لتحويل من بكسل إلى وحدة فيزياء
    add(playerCar);

    // إضافة الجدران/حدود المسار كأجسام فيزيائية ثابتة
    add(Ground(gameRef.screenToWorld(Vector2(size.x / 2, size.y))));
    add(Ground(gameRef.screenToWorld(Vector2(size.x / 2, 0))));
    add(Ground(gameRef.screenToWorld(Vector2(0, size.y / 2)), isVertical: true));
    add(Ground(gameRef.screenToWorld(Vector2(size.x, size.y / 2)), isVertical: true));

    // تحميل وتشغيل صوت المحرك
    await FlameAudio.audioCache.loadAll(['engine_loop.mp3', 'smoke_1.png']); // تحميل صورة الدخان أيضاً
    FlameAudio.loop('engine_loop.mp3'); // يبدأ صوت المحرك بالتكرار

    // ... (بقية كود onLoad مثل تهيئة MultiplayerClient)
  }

  @override
  KeyEventResult onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    // ... (كود التحكم بالسيارة باستخدام applyForceToCenter أو applyTorque)
    return KeyEventResult.handled;
  }

  @override
  void update(double dt) {
    super.update(dt);
    // ... (كود إرسال حالة اللاعب إلى الخادم)

    // تفعيل الدخان عند التسارع الشديد أو الانزلاق
    // يمكن ضبط القيم (linearVelocity.length > 10) حسب الحاجة
    if (keysPressed.contains(LogicalKeyboardKey.arrowUp) && playerCar.body.linearVelocity.length > 5) {
      // إضافة تأثير الدخان خلف السيارة
      // يتم تحديد موقع الدخان بحيث يظهر من خلف السيارة (بناءً على اتجاهها)
      final smokePosition = playerCar.body.position * worldToScreen -
                            (playerCar.carSprite.size.y / 2) * playerCar.body.getTransform().q.asVector2();

      add(SmokeParticleEffect(position: smokePosition));
    }
  }

  // كلاس لتمثيل جسم السيارة الفيزيائي
  class CarBody extends BodyComponent {
    late SpriteComponent carSprite;
    final Vector2 initialPosition;

    CarBody({required this.initialPosition}) : super(renderBody: false);

    @override
    Future<void> onLoad() async {
      await super.onLoad();
      carSprite = SpriteComponent()
        ..sprite = await gameRef.loadSprite('car.png')
        ..size = Vector2(64, 128)
        ..anchor = Anchor.center;
      add(carSprite);
    }

    @override
    Body createBody() {
      // ... (كود createBody لـ CarBody)
      final bodyDef = BodyDef(
        position: initialPosition,
        type: BodyType.dynamic,
        linearDamping: 0.8,
        angularDamping: 0.8,
      );

      final shape = PolygonShape()..setAsBox(carSprite.size.x / 2 / worldToScreen, carSprite.size.y / 2 / worldToScreen);

      final fixtureDef = FixtureDef(shape, density: 1.0, friction: 0.8, restitution: 0.1);

      return world.createBody(bodyDef)..createFixture(fixtureDef);
    }

    @override
    void update(double dt) {
      super.update(dt);
      carSprite.position = body.position * worldToScreen;
      carSprite.angle = body.angle;
    }
  }

  // كلاس لتمثيل الأرض أو الجدران الثابتة
  class Ground extends BodyComponent {
    // ... (كود Ground class)
    final Vector2 _position;
    final bool isVertical;

    Ground(this._position, {this.isVertical = false});

    @override
    Body createBody() {
      final bodyDef = BodyDef(
        position: _position,
        type: BodyType.static,
      );

      Shape shape;
      if (isVertical) {
        shape = PolygonShape()..setAsBox(0.1, gameRef.screenToWorld(gameRef.size).y / 2);
      } else {
        shape = PolygonShape()..setAsBox(gameRef.screenToWorld(gameRef.size).x / 2, 0.1);
      }

      return world.createBody(bodyDef)..createFixture(FixtureDef(shape));
    }
  }

  // كلاس لتمثيل سيارة لاعب آخر
  class OtherCarBody extends BodyComponent {
    // ... (كود OtherCarBody class)
    late SpriteComponent carSprite;
    final Vector2 initialPosition;

    OtherCarBody({required this.initialPosition}) : super(renderBody: false);

    @override
    Future<void> onLoad() async {
      await super.onLoad();
      carSprite = SpriteComponent()
        ..sprite = await gameRef.loadSprite('car.png')
        ..size = Vector2(64, 128)
        ..anchor = Anchor.center;
      add(carSprite);
    }

    @override
    Body createBody() {
      final bodyDef = BodyDef(
        position: initialPosition,
        type: BodyType.kinematic,
      );
      final shape = PolygonShape()..setAsBox(carSprite.size.x / 2 / worldToScreen, carSprite.size.y / 2 / worldToScreen);
      return world.createBody(bodyDef)..createFixture(FixtureDef(shape));
    }

    @override
    void update(double dt) {
      super.update(dt);
      carSprite.position = body.position * worldToScreen;
      carSprite.angle = body.angle;
    }
  }

  // كلاس تأثير الدخان (يتم تعريفه خارج كلاس CarGame)
  class SmokeParticleEffect extends ParticleComponent {
    SmokeParticleEffect({required Vector2 position})
        : super(
            position: position,
            // استخدام Particle.generate لإنشاء دفعة من الجسيمات
            particle: Particle.generate(
              count: 3, // عدد أقل للجسيمات في كل دفعة لجعلها خفيفة
              lifespan: 0.8, // عمر الجسيمات أقصر
              generator: (i) => AcceleratedParticle(
                position: Vector2.zero(), // تبدأ من مركز المكون
                speed: Vector2.random() * 30 - Vector2(15, 15), // سرعة عشوائية بسيطة
                acceleration: Vector2(0, -30), // تتصاعد للأعلى ببطء (Y-axis هو الأعلى في Flame)
                child: SpriteParticle(
                  sprite: Flame.images.fromCache('smoke_1.png'), // استخدام الصورة المحملة مسبقًا
                  size: Vector2(15, 15), // حجم أصغر للجسيمات
                  paint: Paint()..color = Colors.white.withOpacity(0.4), // لون الدخان وشفافيته
                ),
              ),
            ),
          );
  }
}


--

* ملاحظة: لتفعيل الدخان فقط عند التسارع
dart
 
if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) {
  if (!smokeTimer.timer.isRunning()) {
    smokeTimer.timer.start();
  }
} else {
  smokeTimer.timer.stop();
}
--

* 4. تحميل Assets مجانية
* موقع Kenney.nl يقدم:
سيارات 2D، مؤثرات طرق، دخان، صوت، حواجز .
* موقع Itch.io:
ابحث عن car racing 2D assets أو top down racer .

8. اختبار ونشر اللعبة
- استخدم flutter build apk لبناء اللعبة.
- استخدم ngrok لاختبار الاتصال بخادم Python على الهاتف.
- اختبر الأداء على أجهزة ضعيفة.
- اجعل السيارة تستجيب للمستشعرات (Accelerometer) إن أردت ذلك.
1* بناء اللعبة لمنصات مختلفة
* لأجهزة Android/iOS:
flutter build apk --release # لبناء ملف APK للاندرويد
flutter build ios --release # لبناء تطبيق iOS (يتطلب جهاز Mac)
--

* للويب (WebGL):
flutter build web --release # لبناء اللعبة كـ WebGL
--

بعد البناء، ستجد ملفات اللعبة في مجلد build/web. يمكنك رفع محتويات
 هذا المجلد إلى خادم ويب (مثل GitHub Pages، Netlify، أو
 أي استضافة ويب) ليتمكن الآخرون من لعبها في المتصفح.
2* اختبار الاتصال بخادم Python على الهاتف/الويب
إذا كان خادم Python يعمل على جهازك المحلي، فلن تتمكن الهواتف أو
 أجهزة الكمبيوتر الأخرى على الشبكة من الوصول إليه مباشرة باستخدام localhost.
للاختبار المحلي من الهاتف: شغل الخادم على جهاز الكمبيوتر، ثم قم بتحديث
 عنوان IP في multiplayer_client.dart من localhost إلى عنوان IP لجهاز 
الكمبيوتر الخاص بك على الشبكة المحلية (مثلاً: ws://192.168.1.5:8000/ws).
للوصول من أي مكان (اختبار عام): استخدم أداة مثل ngrok. شغل ngrok ووجهه إلى
 منفذ خادم Python (عادةً 8000). سيعطيك ngrok عنوان URL عاماً يمكنك استخدامه 
في multiplayer_client.dart للاتصال بالخادم من أي مكان.

بعد اتباع هذا الدليل، أصبحت تملك نموذجًا أوليًا فعالًا لـ لعبة سباق سيارات شبيهة
 بـ Need for Speed باستخدام Flutter وPython، لقد تعلمت: كيفية التحكم 
بحركة السيارة والانزلاق ، كيفية دمج مؤثرات مثل الدخان وصوت المحرك باستخدام
 flame_audio، كيفية ربط اللعبة بخادم Python باستخدام Sockets، خطوات
 تحسين الأداء والرسوميات، من أين تحصل على Assets مجانية عالية الجودة (مثل سيارات، طرق، مؤثرات).
بإمكانك تطوير هذه اللعبة أكثر بإضافة خصائص مثل السباق الليلي، المستويات،
 التحديات، اللعب الجماعي، وحتى الذكاء الاصطناعي. هذه التجربة ليست فقط ممتعة، 
بل توسّع مهاراتك في تطوير الألعاب باستخدام أدوات قوية وعصرية.

 
جدول المحتويات