متوسط · ۱ ساعت

فیزیک و کنترل بازیکن در ۲D

Rigidbody، Collider و ورودی کاربر.

تیم Nova Lab · · آموزش متوسط

قدم اول: آماده‌سازی Player و Ground

از آموزش قبل، Scene با Player (Sprite + Transform) و Ground (Sprite + Box Collider 2D) آماده است. اگر Collider ندارید، الان اضافه کنید: روی Ground، Add Component > Physics 2D > Box Collider 2D. Ground نباید Rigidbody داشته باشد — بدون Rigidbody، Collider static است و در برابر نیروی Player ثابت می‌ماند. این تفاوت static و dynamic بنیاد فیزیک 2D در Unity است.

روی Player، Add Component > Physics 2D > Rigidbody 2D. Body Type را Dynamic بگذارید تا تحت گرانش و نیرو حرکت کند. Gravity Scale را روی ۳ تا ۵ تنظیم کنید — مقدار پیش‌فرض 1 برای پلتفرمر کند است؛ 4 حس responsiveتری می‌دهد. Mass را روی 1 نگه دارید مگر طراحی خاص داشته باشید. Linear Drag را 0 نگه دارید؛ Drag بالا حرکت را سنگین می‌کند.

در Rigidbody 2D، Constraints > Freeze Rotation Z را فعال کنید. بدون این، Player هنگام برخورد به زمین می‌چرخد — در بازی 2D side-view تقریباً هرگز مطلوب نیست. Collision Detection را Continuous کنید تا در سرعت بالا از Collider عبور نکند (tunneling). Interpolate را Interpolate بگذارید — حرکت در Game View بین فریم‌های فیزیک نرم‌تر به نظر می‌رسد.

Physics Material 2D برای کنترل اصطکاک و bounce: در Project راست‌کلیک > Create > 2D > Physics Material 2D. Friction را 0.4 و Bounciness را 0 برای زمین معمولی. Material را روی Box Collider 2D Ground بکشید. Material جدا برای Player (Friction 0) لغزش کنترل‌شده‌تر ایجاد می‌کند — بازیکن بعد از رها کردن دکمه حرکت سریع‌تر متوقف می‌شود.

Tag و Layer را تنظیم کنید: Player را انتخاب کنید، Tag را Player بگذارید (اگر نیست از Edit > Project Settings > Tags and Layers بسازید). Ground را Layer Ground اختصاص دهید. Layerها بعداً در ground check و Collision Matrix استفاده می‌شوند — از همان ابتدا setup کنید تا refactor وسط پروژه لازم نشود.

قبل از نوشتن کد، یک بار Play بزنید: Player باید روی Ground بیفتد و بایستد. اگر از زمین عبور کرد، Continuous collision و ضخامت Collider را بررسی کنید. اگر نمی‌افتد، Gravity Scale و وجود Collider روی Ground را چک کنید. Console نباید خطای physics نشان دهد — هر warning زرد را یادداشت کنید.

قدم دوم: اسکریپت حرکت افقی

در Assets/Scripts، راست‌کلیک > Create > C# Script به نام PlayerController بسازید. Double-click فایل را در IDE پیش‌فرض باز می‌کند — Visual Studio، Rider یا VS Code بسته به External Script Editor در Preferences. اسکریپت را به GameObject Player بکشید؛ نام class باید دقیقاً با نام فایل یکسان باشد وگرنه Unity خطای compile می‌دهد.

ساختار پایه MonoBehaviour: فیلد SerializeField برای moveSpeed با مقدار پیش‌فرض 8f و یک reference خصوصی به Rigidbody2D. در متد Awake بنویسید rb = GetComponent(). Awake قبل از Start و قبل از اولین FixedUpdate فراخوانی می‌شود — بهترین جا برای cache کردن Componentهای هم‌GameObject است.

ورودی افقی را با Input.GetAxisRaw("Horizontal") بخوانید. خروجی -1 برای چپ، 0 برای بی‌حرکت و +1 برای راست است. GetAxisRaw بدون smoothing برمی‌گردد؛ GetAxis با ramp نرم برای بازی top-down گاهی بهتر است، اما پلتفرمر side-scrolling به پاسخ فوری کیبورد نیاز دارد تا بازیکن احساس تأخیر نکند.

حرکت را حتماً در FixedUpdate اعمال کنید، نه Update. موتور فیزیک Unity در timestep ثابت (پیش‌فرض 0.02 ثانیه، یعنی 50 بار در ثانیه) در FixedUpdate اجرا می‌شود. با set کردن rb.velocity برابر new Vector2(moveInput * moveSpeed, rb.velocity.y) سرعت عمودی (سقوط یا پرش) حفظ می‌شود و فقط مؤلفه افقی تغییر می‌کند.

الگوی حرفه‌ای: مقدار moveInput را در Update بخوانید و در متغیر private ذخیره کنید؛ در FixedUpdate همان مقدار ذخیره‌شده را روی velocity اعمال کنید. این از desync بین frame rate نمایش (60 یا 144 fps) و timestep فیزیک جلوگیری می‌کند. برای شروع tutorial، خواندن مستقیم در FixedUpdate هم قابل قبول است.

Play بزنید و با کلیدهای A/D یا فلش‌های جهت‌دار حرکت را تست کنید. Player باید روی Ground راه برود بدون لغزش نامحدود. اگر روی یخ می‌لغزد، Friction Material زمین را بالا ببرید؛ اگر چسبیده است، Friction Player را پایین بیاورید. moveSpeed را بین 6 تا 12 امتحان کنید و مقداری را یادداشت کنید که با jumpForce آینده هماهنگ باشد.

قدم سوم: پیاده‌سازی پرش

فیلدهای jumpForce با مقدار 14f و متغیر private bool isGrounded به PlayerController اضافه کنید. در FixedUpdate و بعد از اعمال حرکت افقی، شرط پرش را بنویسید: فقط وقتی isGrounded true است و Input.GetButtonDown("Jump") فشرده شده، velocity.y را برابر jumpForce قرار دهید. GetButtonDown فقط یک فریم true است — برای پرش تک‌بار ideal است. Space به‌صورت پیش‌فرض به محور Jump نگاشت شده.

تشخیص زمین (ground check): یک GameObject خالی به‌عنوان child Player با نام GroundCheck در پایین sprite بسازید — جایی که «کف پا» قرار دارد. فیلد SerializeField از نوع LayerMask برای groundLayer تعریف کنید و در Inspector فقط Layer Ground را تیک بزنید. در FixedUpdate با Physics2D.OverlapCircle روی groundCheck.position، شعاع 0.2f و groundLayer بررسی کنید آیا زیر پاها زمین هست.

برای دیباگ بصری، متد OnDrawGizmosSelected را اضافه کنید: رنگ Gizmo سبز وقتی isGrounded و قرمز وقتی در هوا؛ DrawWireSphere موقعیت و شعاع check را در Scene View نشان می‌دهد. شعاع 0.15 تا 0.25 برای sprite انسان‌وار متوسط مناسب است — خیلی بزرگ باعث پرش در هوا، خیلی کوچک باعث flicker روی لبه می‌شود.

Coyote Time یعنی 0.1 تا 0.15 ثانیه بعد از ترک لبه، هنوز اجازه پرش دارید — مثل بازی‌های حرفه‌ای Celeste. Jump Buffer یعنی اگر 0.1 ثانیه قبل از فرود Space زده شود، در اولین فریم grounded پرش اجرا شود. این دو تکنیک بازی indie را responsive می‌کنند؛ در Nova Lab stretch goal هستند اما ارزش پیاده‌سازی دارند.

Double jump با شمارنده jumpCount و maxJumps = 2: هر پرش jumpCount را افزایش دهید؛ وقتی isGrounded شد jumpCount را صفر کنید. شرط پرش: isGrounded یا jumpCount کمتر از maxJumps. velocity.y را مستقیم set کنید نه AddForce — برای پلتفرمر predictableتر و tune کردن jumpForce ساده‌تر است.

تست نهایی پرش: Player روی لبه Ground فقط وقتی واقعاً grounded است بپرد. اگر در هوا پرش می‌کند، groundLayer یا موقعیت GroundCheck را اصلاح کنید. jumpForce را با Gravity Scale هماهنگ tune کنید — نسبت این دو «ارتفاع پرش» و «سرعت سقوط» را تعیین می‌کند و حس arcade در مقابل realistic را shape می‌زند.

قدم چهارم: برخورد و Collider دقیق

Box Collider 2D برای Player معمولاً کافی است. با Edit Collider نقاط را جابه‌جا کنید — Collider را کمی کوچک‌تر از sprite بگذارید تا گوشه‌های پا در لبه platform گیر نکنند. برای Tilemap بزرگ، Composite Collider 2D به‌همراه Rigidbody2D از نوع Kinematic روی Tilemap برخورد یکپارچه و بهینه می‌سازد.

OnCollisionEnter2D وقتی دو Collider solid (Is Trigger خاموش) برخورد می‌کنند — برای دیوار bounce، آسیب از enemy، یا صدای land. OnTriggerEnter2D وقتی Is Trigger روشن است — برای سکه، checkpoint، kill zone. Player باید Rigidbody2D و Collider solid داشته باشد؛ Coin فقط Trigger.

CompareTag("Player") و CompareTag("Enemy") — Tagها case-sensitive هستند. Layer Collision Matrix در Edit > Project Settings > Physics 2D مشخص می‌کند کدام لایه‌ها با هم collide می‌کنند. اگر Coin فقط Trigger است، می‌توانید برخورد Player-Coin را در matrix غیرفعال کنید — بهینه‌سازی جزئی.

جلوگیری از فشار در دیوار: raycast یا capsule cast افقی کوتاه در جهت moveInput قبل از set velocity — اگر obstacle، velocity.x = 0. بدون این، Player در گوشه‌ها jitter می‌کند و physics solver ناپایدار به نظر می‌رسد.

Platform متحرک: Rigidbody2D Kinematic روی platform و حرکت با transform در FixedUpdate یا Animation clip. Player را child نکنید — اصطکاک Player را با platform حمل می‌کند. Platform Effector 2D با گزینه One Way برای platformی که از پایین jump-through می‌شود.

تمرین: Wall با Box Collider 2D، KillZone Trigger زیر Ground. Player به Wall نفوذ نکند؛ در KillZone یا reset position یا در tutorial UI با GameManager.GameOver. این patternها پایه level design و loop بازی هستند.

قدم پنجم: ورودی جدید Input System (اختیاری)

Unity Input System (package جدید) جایگزین Input Manager کلاسیک است. Window > Package Manager > Unity Registry > Input System > Install. پس از نصب، dialog می‌پرسد Active Input Handling — For now Both انتخاب کنید تا tutorial با Input.GetAxisRaw هم کار کند.

در Project: Create > Input Actions. Asset جدید PlayerInputActions.inputactions. Action Map «Player» با Action Move (Type: Value, Control Type: Vector2) و Jump (Type: Button). Bindings: WASD composite به Move، Arrow keys، Space به Jump.

در Inspector فایل inputactions، Generate C# Class را فعال و Apply. کلاس PlayerInputActions ساخته می‌شود. در PlayerController: actions = new PlayerInputActions(); actions.Player.Enable(); moveInput = actions.Player.Move.ReadValue().x;

Jump با actions.Player.Jump.triggered در Update چک شود. Input System rebind runtime، gamepad و touch را unified می‌کند — برای publish روی console و mobile ارزش دارد. Nova Lab در سطح متوسط Input کلاسیک را teach می‌کند؛ Input System را برای پروژه portfolio پیشنهاد می‌کند.

Player Input component (Add Component > Player Input) behavior را ساده می‌کند — Send Messages یا Invoke Unity Events. برای کنترل کامل، generated C# class manual در اسکریپت بهتر است.

هشدار: اگر Active Input Handling فقط Input System Package باشد، Input.GetAxisRaw کار نمی‌کند. Both برای transition؛ در production یکی را انتخاب کنید تا confusion کم شود.

قدم ششم: polish و تست

Animator Controller: Window > Animation > Animator. Parameters: speed (Float), isGrounded (Bool), yVelocity (Float). States: Idle، Run (blend با speed)، Jump، Fall. Transitionها با condition — مثلاً isGrounded false و yVelocity > 0 به Jump.

Sprite flip: if (moveInput > 0) transform.localScale = new Vector3(1,1,1); else if (moveInput < 0) transform.localScale = new Vector3(-1,1,1); — ساده‌ترین flip افقی بدون دو sprite. یا SpriteRenderer.flipX.

Audio: AudioSource روی Player. Clip jump و land کوتاه. در پرش PlayOneShot(jumpClip); در OnCollisionEnter2D با Ground، landClip. Volume و pitch کمی random برای تنوع.

Particle: child GameObject با Particle System — burst کوچک هنگام land. Play On Awake خاموش؛ در code particle.Play().

Camera follow: اسکریپت ساده روی Camera — Vector3 target = player.position + offset; transform.position = Vector3.Lerp(transform.position, target, smoothSpeed * Time.deltaTime); — offset Z = -10 حفظ شود. SmoothDamp برای feel بهتر.

Playtest checklist: حرکت responsive، پرش فقط روی ground، بدون tunneling، flip درست، صدا و particle. moveSpeed، jumpForce، Gravity Scale را یادداشت کنید — در tutorial UI امتیاز و جان اضافه می‌کنیم؛ PlayerController نهایی را Prefab Apply کنید.

سوالات متداول

FixedUpdate یا Update برای حرکت؟

هر تغییر مستقیم rb.velocity یا AddForce باید در FixedUpdate باشد چون با physics engine هم‌گام است. Update frame rate وابسته است (60 یا 144 fps). input reading می‌تواند Update باشد و مقدار در FixedUpdate مصرف شود — pattern استاندارد.

چرا Player از زمین عبور می‌کند؟

Collision Detection = Continuous، Fixed Timestep در Time settings (0.02 default)، Collider نازک‌تر از یک pixel نباشد، velocity.y در یک فریم extreme نباشد. اگر Tilemap دارید Composite Collider و thickness کافی داشته باشد.

Kinematic vs Dynamic Rigidbody2D؟

Dynamic تحت گرانش و force کامل — برای Player و objectهای فیزیکی آزاد. Kinematic با transform/اسکریپت حرکت می‌کند، به Dynamic واکنش نشان می‌دهد — برای platform متحرک، درب، elevator. Static = بدون Rigidbody، برای Ground و Wall.

OverlapCircle یا Raycast برای ground check؟

OverlapCircle روی شیب و لبه پایدارتر است — sphere زیر پا overlap با Ground layer. Raycast کوتاه (0.1 unit) ساده‌تر اما روی edge گاهی flicker می‌کند. radius 0.2 و layer mask دقیق برای پلتفرمر توصیه Nova Lab.