اموزش ساخت بازی با گودو(گودوت)قسمت اول

نمای کلی قسمتدر این قسمت ما یک بازیکن اول شخص خواهیم ساخت که می تواند در محیط اطراف حرکت کند.با پایان این قسمت ، شما یک شخصیت اول شخص خواهید داشت که می تواند در محیط بازی حرکت کند ، دوی سرعت بزند ، با دوربین اول شخص مبتنی بر ماوس به اطراف نگاه کند ، به هوا بپرد و یک چراغ فلاش را خاموش و روشن کند.گودو را راه اندازی کنید و پروژه( https://docs.godotengine.org/en/stable/_downloads/06eb944abda1cabe2ed92be2d4f42744/Godot_FPS_Starter.zip )موجود درassets را باز کنید.ابتدا تنظیمات پروژه را باز کرده و به برگه "Input Map" بروید. خواهید دید که چندین اقدام قبلاً تعریف شده است. ما از این اقدامات برای بازیکن خود استفاده خواهیم کرد. در صورت تمایل می توانید کلیدهای مربوط به این اقدامات را تغییر دهید.بیایید یک ثانیه وقت بگذاریم تا ببینیم چه چیزی در assets داریم.چندین صحنه در Assets گنجانده شده است. به عنوان مثال ، در res: // ما 14 صحنه داریم که با گذراندن این مجموعه آموزشی ، بیشتر آنها را بازدید خواهیم کرد. در حال حاضر اجازه دهید Player.tscn را باز کنیم.دسته ای از صحنه ها و چند بافت در پوشه Assets وجود دارد. اگر می خواهید می توانید به این موارد نگاه کنید ، اما ما در این مجموعه آموزشی از طریق دارایی ها کاوش نخواهیم کرد. دارایی ها شامل همه مدل های استفاده شده برای هر یک از سطوح و همچنین برخی از بافت ها و مواد است.ایجاد منطق حرکت FPS پس از باز کردن Player.tscn ، بیایید نگاهی سریع به نحوه راه اندازی آن بیندازیم ابتدا به نحوه تنظیم شکلهای برخورد بازیکن توجه کنید. استفاده از کپسول اشاره عمودی(collision shape /capsul shape) به عنوان شکل برخورد(collision) برای بازیکن ، در اکثر بازی های اول شخص کاملاً رایج است.ما یک مربع کوچک(collision shape/box shape) به "پاهای" بازیکن اضافه می کنیم تا بازیکن احساس کند در یک نقطه متعادل استما می خواهیم "پا" کمی بالاتر از پایین کپسول باشد ، بنابراین می توانیم از لبه ها کمی بچرخانیم. محل قرار دادن "پاها" به سطح شما و احساسی که می خواهید بازیکن شما داشته باشد بستگی دارد.در بسیاری از مواقع بازیکن وقتی به لبه می رود و از صفحه خارج می شود ، متوجه دایره ای بودن شکل برخورد(collision shape) می شود. ما مربع کوچکی را در پایین کپسول اضافه می کنیم تا لبه های لغزنده و اطراف آن را کاهش دهیم.نکته دیگری که باید توجه کنید این است که چند گره از فرزندان Rotation_Helper هستند. به این دلیل که Rotation_Helper شامل تمام گره هایی است که می خواهیم در محور X بچرخانیم (بالا و پایین). دلیل این امر این است که می توانیم Player را در محور Y و Rotation_helper را در محور X بچرخانیم.اگر ما از Rotation_helper استفاده نمی کردیم ، احتمالاً موارد چرخش همزمان در هر دو محور X و Y داشتیم که در برخی موارد به طور بالقوه به حالت چرخش در هر سه محور تبدیل می شود. برای اطلاعات بیشتر به ( https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html#doc-using-transforms ) مراجعه کنیدیک اسکریپت جدید به گره Player متصل کنید و آن را Player.gd بنامید. بیایید بازیکن خود را با اضافه کردن توانایی حرکت در اطراف ، نگاه کردن با ماوس و پرش ، برنامه ریزی کنیم. کد زیر را به Player.gd اضافه کنید:extends KinematicBodyconst GRAVITY = -24.8var vel = Vector3()const MAX_SPEED = 20const JUMP_SPEED = 18const ACCEL = 4.5var dir = Vector3()const DEACCEL= 16const MAX_SLOPE_ANGLE = 40 var camera var rotation_helper var MOUSE_SENSITIVITY = 0.05func _ready():camera = $Rotation_Helper/Camerarotation_helper = $Rotation_Helper Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)func _physics_process(delta): process_input(delta) process_movement(delta)func process_input(delta): # ---------------------------------- # Walking dir = Vector3()var cam_xform = camera.get_global_transform() var input_movement_vector = Vector2()if Input.is_action_pressed(&quotmovement_forward&quot): input_movement_vector.y += 1if Input.is_action_pressed(&quotmovement_backward&quot): input_movement_vector.y -= 1if Input.is_action_pressed(&quotmovement_left&quot): input_movement_vector.x -= 1if Input.is_action_pressed(&quotmovement_right&quot): input_movement_vector.x += 1 input_movement_vector = input_movement_vector.normalized()# Basis vectors are already normalized. dir += -cam_xform.basis.z * input_movement_vector.y dir += cam_xform.basis.x * input_movement_vector.x # ---------------------------------- # ---------------------------------- # Jumping if is_on_floor():if Input.is_action_just_pressed(&quotmovement_jump&quot):vel.y = JUMP_SPEED # ---------------------------------- # ---------------------------------- # Capturing/Freeing the cursorif Input.is_action_just_pressed(&quotui_cancel&quot):if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)else: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) # ----------------------------------func process_movement(delta): dir.y = 0dir = dir.normalized()vel.y += delta * GRAVITYvar hvel = velhvel.y = 0var target = dirtarget *= MAX_SPEEDvar accel if dir.dot(hvel) > 0:accel = ACCELelse: accel = DEACCELhvel = hvel.linear_interpolate(target, accel * delta)vel.x = hvel.xvel.z = hvel.zvel = move_and_slide(vel, Vector3(0, 1, 0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE)) func _input(event): if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: rotation_helper.rotate_x(deg2rad(event.relative.y * MOUSE_SENSITIVITY)) self.rotate_y(deg2rad(event.relative.x * MOUSE_SENSITIVITY * -1))var camera_rot = rotation_helper.rotation_degreescamera_rot.x = clamp(camera_rot.x, -70, 70) rotation_helper.rotation_degrees = camera_rotاین کد زیادی است ، بنابراین اجازه دهید آن را با تابع تقسیم کنیم:ابتدا ، برخی متغیرهای کلاس را تعریف می کنیم تا نحوه حرکت بازیکن ما در جهان را تعیین کنیم.در طول این آموزش ، متغیرهای تعریف شده در خارج از توابع به عنوان "متغیرهای کلاس" معرفی می شوند. این بدان دلیل است که می توانیم از هر مکان اسکریپت به هر یک از این متغیرها دسترسی پیدا کنیم.بیایید هر یک از متغیرهای کلاس را مرور کنیم:Gravity: چقدر نیروی گرانش ما را به سمت پایین می کشاند. vel: سرعت KinematicBody ما. MAX_SPEED: سریعترین سرعتی که می توانیم بدست آوریم. همین که به این سرعت رسیدیم ، سریعتر پیش نمی رویم. JUMP_SPEED: چقدر می توانیم پرش کنیم. ACCEL: چقدر سریع شتاب می گیریم. هرچه مقدار بالاتر باشد ، ما زودتر به حداکثر سرعت می رسیم. DEACCEL: با چه سرعتی سرعت را کاهش می دهیم. هرچه مقدار بالاتر باشد ، زودتر متوقف خواهیم شد. MAX_SLOPE_ANGLE: شدیدترین زاویه ای که KinematicBody ما به عنوان "کف" در نظر خواهد گرفت.camera: گره دوربین. rotation_helper: گره فضایی که همه چیزهایی را که می خواهیم در محور X بچرخانیم (بالا و پایین) دارد. MOUSE_SENSITIVITY: حساسیت ماوس چقدر است. به نظر من مقداری از 0.05 برای موس من خوب کار می کند ، اما ممکن است لازم باشد آن را براساس حساسیت موشواره(موس) خود تغییر دهید.می توانید بسیاری از این متغیرها را اصلاح کنید تا نتایج متفاوتی بدست آورید. به عنوان مثال ، با کاهش GRAVITY و یا افزایش JUMP_SPEED می توانید شخصیت احساس "شناور" تری پیدا کنید. خیالتان راحت باشد!شاید متوجه شده باشید که MOUSE_SENSITIVITY مانند سایر ثابتها در همه حروف نوشته شده است ، اما MOUSE_SENSITIVITY یک ثابت نیست. دلیل این امر این است که ما می خواهیم مانند یک متغیر ثابت (متغیری که نمی تواند تغییر کند) در کل اسکریپت با آن رفتار کنیم ، اما می خواهیم بعداً وقتی تنظیمات قابل تنظیم را اضافه می کنیم ، مقدار آن را تغییر دهیم. بنابراین ، برای اینکه به خود یادآوری کنیم که مانند یک ثابت با آن رفتار کنیم ، این نام در همه کلاه ها ذکر شده استحالا بیایید به عملکرد _ready نگاه کنیم: ابتدا گره های دوربین و rotation_helper را بدست آورده و در متغیرهای خود ذخیره می کنیم.سپس باید حالت موس را روی capture تنظیم کنیم ، بنابراین ماوس نمی تواند از پنجره بازی خارج شود.با این کار ماوس پنهان می شود و در مرکز صفحه نگهداری می شود. این کار را به دو دلیل انجام می دهیم: دلیل اول این است که نمی خواهیم بازیکن هنگام پخش نشانگر ماوس خود را ببیند.دلیل دوم این است که ما نمی خواهیم مکان نما از پنجره بازی خارج شود. اگر مکان نما از پنجره بازی خارج شود ، ممکن است مواردی وجود داشته باشد که بازیکن خارج از پنجره کلیک کند ، و سپس تمرکز بازی از بین می رود. برای اطمینان از اینکه هیچ یک از این موارد اتفاق نمی افتد ، مکان نما را پنهان میکنیمبه ( https://docs.godotengine.org/en/stable/classes/class_input.html#class-input ) برای حالت های مختلف ماوس مراجعه کنید. ما فقط در این مجموعه آموزشی از MOUSE_MODE_CAPTURED و MOUSE_MODE_VISIBLE استفاده خواهیم کرد.بعد بیایید نگاهی به physics_process بیندازیم: تمام کاری که ما در physics_process انجام می دهیم فراخوانی دو عملکرد است: process_input و process_movementprocess_input جایی خواهد بود که همه کدهای مربوط به ورودی player را ذخیره می کنیم. ما می خواهیم قبل از هر چیز دیگری آن را صدا کنیم ، بنابراین ورودی جدید player برای کار داریم.process_movement جایی است که ما تمام داده های لازم را به KinematicBody ارسال می کنیم تا بتواند در دنیای بازی حرکت کند.بیایید روند_input بعدی را بررسی کنیم: ابتدا dir را بر روی یک Vector3 خالی تنظیم می کنیم. dir برای ذخیره مسیری که بازیکن قصد دارد به سمت آن حرکت کند استفاده خواهد شد. از آنجا که نمی خواهیم ورودی قبلی player فراتر از یک تماس process_movement باشد ، player را تنظیم می کنیم.در مرحله بعدی ، تغییر شکل جهانی دوربین را دریافت می کنیم و آن را نیز در متغیر cam_xform ذخیره می کنیم. دلیل اینکه ما به تبدیل جهانی دوربین نیاز داریم این است که بتوانیم از بردارهای جهت دار آن استفاده کنیم. بسیاری از کاربران بردارهای جهت دهنده را گیج کننده دانسته اند ، بنابراین بیایید یک ثانیه وقت بگذاریم و نحوه کار آنها را توضیح دهیم:فضای جهان را می توان چنین تعریف کرد: فضایی که همه اشیا در آن قرار می گیرند ، نسبت به یک نقطه مبدا ثابت. هر جسم ، مهم نیست که دو بعدی باشد یا سه بعدی ، در فضای جهان جایگاهی دارد. به بیان دیگر: فضای جهان فضایی در جهان است که موقعیت ، چرخش و مقیاس هر جسم را می توان با یک نقطه ثابت ، شناخته شده و ثابت به نام مبدا اندازه گیری کرد.در گودو ، مبدا در موقعیت (0 ، 0 ، 0) با چرخش (0 ، 0 ، 0) و مقیاس (1 ، 1 ، 1) است.هنگامی که ویرایشگر Godot را باز می کنید و یک گره Spatial based انتخاب می کنید ، یک gizmo ظاهر می شود. هر یک از فلش ها به طور پیش فرض با استفاده از جهت های فضای جهان نشان داده می شوند.اگر می خواهید با استفاده از بردارهای جهت دار فضایی جهان حرکت کنید ، مانند این کار می کنید:if Input.is_action_pressed(&quotmovement_forward&quot): node.translate(Vector3(0, 0, 1))if Input.is_action_pressed(&quotmovement_backward&quot): node.translate(Vector3(0, 0, -1)) if Input.is_action_pressed(&quotmovement_left&quot): node.translate(Vector3(1, 0, 0))if Input.is_action_pressed(&quotmovement_right&quot): node.translate(Vector3(-1, 0, 0))توجه کنید که برای بدست آوردن بردارهای جهت دار فضای جهانی نیازی به انجام هیچ گونه محاسبه ای نداریم. ما می توانیم چند متغیر Vector3 تعریف کنیم و مقادیر نشان داده شده در هر جهت را وارد کنیم.بسیار خوب ، بازگشت به process_input: بعد یک متغیر جدید به نام input_movement_vector درست می کنیم و آن را به یک Vector2 خالی اختصاص می دهیم. ما از این برای ایجاد یک محور مجازی، برای ترسیم ورودی بازیکن به حرکت استفاده خواهیم کرد.این ممکن است فقط برای صفحه کلید بیش از حد به نظر برسد ، اما بعداً وقتی ورودی joypad را اضافه می کنیم ، منطقی خواهد بود.بر اساس اینکه عمل حرکت جهت دار فشرده می شود(Input.is_action_pressed) ، input_movement_vector را اضافه یا از آن کم می کنیم. بعد از اینکه هر یک از اقدامات حرکت جهت دار را بررسی کردیم ، input_movement_vector را عادی می کنیم. این امر باعث می شود که مقادیر input_movement_vector در یک دایره واحد 1 شعاع باشد.بعد ما بردار Z محلی دوربین را برمبنای (input_movement_vector.y) به dir اضافه می کنیم. این امر به این صورت است که هنگام جلو یا عقب رفتن، ما محور Z محلی دوربین را اضافه می کنیم تا playerنسبت به دوربین به جلو یا عقب حرکت کند.از آنجا که دوربین با 180 درجه چرخانده می شود ، ما باید بردار Z را چرخانیم. به طور معمول جلو محور Z مثبت است ، بنابراین استفاده از basic.z.normalized () کارساز است ، اما ما از -basis.z.normalized () استفاده می کنیم زیرا محور Z دوربین ما در مقایسه با, بازیکن به سمت عقب است.ما برای وکتور X محلی دوربین همین کار را می کنیم و به جای استفاده از input_movement_vector.y در عوض از input_movement_vector.x استفاده می کنیم. این امر باعث می شود جایی که player به چپ / راست فشار می دهد ، نسبت به دوربین حرکت می کند. در مرحله بعدی با استفاده از عملکرد is_on_floor بررسی می کنیم که آیا بازیکن روی زمین است یا خیر. اگر اینگونه باشد ، بررسی می کنیم که آیا عمل "move_jump" فشرده شده است یا خیر. اگر اینگونه باشد ، سرعت Y بازیکن را روی JUMP_SPEED تنظیم می کنیم.از آنجا که ما در حال تنظیم سرعت Y هستیم ، player به هوا می پرد. سپس عملکرد ui_cancel را بررسی می کنیم. این امر بدین ترتیب است که می توان با فشار دادن دکمه ESCAPE، مکان نما را آزاد و ضبط کرد. ما این کار را انجام می دهیم زیرا در غیر این صورت هیچ راهی برای آزاد کردن مکان نما نداریمبرای آزاد کردن / گرفتن مکان نما ، بررسی می کنیم که آیا ماوس قابل مشاهده است یا خیر. اگر باشد ، آن را ضبط می کنیم و اگر نباشد ، آن را قابل مشاهده می کنیم . این تمام کاری است که ما در حال حاضر برای process_input انجام می دهیم. ما چندین بار به این عملکرد باز خواهیم گشت زیرا پیچیدگی های بیشتری به player خود اضافه می کنیم.حالا بیایید به process_movement نگاه کنیم: ابتدا با صفر قرار دادن مقدار Y آن اطمینان حاصل می کنیم که dir هیچ حرکتی در محور Y ندارد. بعد dir را عادی می کنیم تا اطمینان حاصل کنیم که در یک دایره واحد 1 (شعاع) قرار داریم. صرف نظر از اینکه پخش کننده مستقیم حرکت می کند یا مورب ، این امر باعث می شود که ما با سرعت ثابت حرکت کنیم. اگر ما نرمال نمی شدیم ، بازیکن سریعتر از مورب حرکت می کرد تا وقتی که مستقیم می رود.بعد با اضافه کردن GRAVITY * delta به سرعت Y بازیکن ، گرانش را به بازیکن اضافه می کنیم. پس از آن سرعت player را به یک متغیر جدید (به نام hvel) اختصاص می دهیم و هر حرکتی را در محور Y حذف می کنیم. بعد یک متغیر جدید (target) را برای بردار جهت player تنظیم می کنیم. سپس آن را در حداکثر سرعت پخش ضرب می کنیم ، بنابراین می دانیم که بازیکن در مسیری که توسط dir فراهم می شود ، تا کجا حرکت خواهد کرد.بعد از آن یک متغیر جدید برای شتاب ایجاد می کنیم ، به نام accel. سپس محصول نقطه hvel را می گیریم تا ببینیم آیا player مطابق hvel حرکت می کند یا خیر. به یاد داشته باشید ، hvel هیچ سرعت Y ندارد ، به این معنی که ما فقط در حال حرکت بازیکن به جلو ، عقب ، چپ یا راست هستیم. اگر بازیکن طبق hvel حرکت می کند ، ما accel را روی ACCEL ثابت قرار می دهیم تا بازیکن شتاب بگیرد ، در غیر این صورت accel را روی ثابت DEACCEL قرار می دهیم تا بازیکن کند شود.سپس سرعت افقی را بین یکدیگر قرار می دهیم ، سرعت X و Z بازیکن را روی سرعت افقی تنظیم می کنیم و move_and_slide را فراخوانی می کنیم تا KinematicBody بتواند بازیکن را از طریق دنیای فیزیک حرکت دهد. نکته تمام کدهای موجود در process_movement دقیقاً همان کد حرکت از نسخه ی نمایشی کاراکتر Kinematic هست! عملکرد نهایی ما تابع _input است و خوشبختانه نسبتاً کوتاه است:ابتدا اطمینان حاصل می کنیم که رویدادی که با آن روبرو هستیم یک رویداد InputEventMouseMotion است. ما همچنین می خواهیم مکان یاب را بگیریم یا خیر ، اگر نمی خواهیم چرخش کنیم. اگر رویداد واقعاً یک رویداد حرکت ماوس باشد و مکان نما گرفته شود ، ما بر اساس حرکت نسبی ماوس که توسط InputEventMouseMotion ارائه شده است می چرخیم. ابتدا گره rotation_helper را در محور X می چرخانیم ، با استفاده از مقدار Y حرکت ماوس ، که توسط InputEventMouseMotion ارائه شده است.سپس کل KinematicBody را در محور Y با مقدار X حرکت نسبی ماوس می چرخانیم.در آخر ، چرخش X rotation_helper را گیره می کنیم تا بین -70 و 70 درجه باشد تا بازیکن نتواند خودش را وارونه بچرخاند. برای تست کد ، اگر هنوز صحنه ای با نام Testing_Area.tscn باز نشده است ، آن را باز کنید. با مرور قسمتهای بعدی آموزش ، از این صحنه استفاده خواهیم کرد ، بنابراین حتماً آن را در یکی از برگه های صحنه خود باز نگه دارید.دادن یک چراغ قوه به بازیکن و گزینه دویدن سریع: قبل از شروع کار سلاح ها ، باید چند مورد دیگر اضافه کنیم. بسیاری از بازی های FPS دارای گزینه دوی سرعت و چراغ قوه هستند. ما به راحتی می توانیم این موارد را به بازیکن خود اضافه کنیم ، پس اجازه دهید این کار را انجام دهیم!ابتدا به چند متغیر کلاس دیگر در اسکریپت player خود نیاز داریم:const MAX_SPRINT_SPEED = 30 const SPRINT_ACCEL = 18var is_sprinting = false var flashlightتمام متغیرهای دوی سرعت دقیقاً مشابه متغیرهای غیر دوی سرعت با نام های مشابه کار می کنند. is_sprinting بولینی است برای پیگیری اینکه آیا بازیکن در حال دویدن است یا خیر ، و چراغ قوه متغیری است که ما برای نگه داشتن گره نور فلش پخش کننده از آن استفاده خواهیم کرد. اکنون باید چند خط کد اضافه کنیم ، از _readyشروع می شود. موارد زیر را به _ready اضافه کنید:flashlight = $Rotation_Helper/Flashlightاین گره Flashlight را می گیرد و آن را به متغیر چراغ قوه اختصاص می دهد. حالا باید مقداری از کد را در process_input تغییر دهیم. موارد زیر را در قسمت process_input اضافه کنید:# ---------------------------------- # Sprinting if Input.is_action_pressed(&quotmovement_sprint&quot): is_sprinting = true else: is_sprinting = false # ---------------------------------- # ---------------------------------- # Turning the flashlight on/off if Input.is_action_just_pressed(&quotflashlight&quot): if flashlight.is_visible_in_tree(): flashlight.hide() else: flashlight.show() # ----------------------------------بیایید موارد اضافی را مرور کنیم: ما is_sprint به true تنظیم میکنیم هنگامی که پخش کننده عمل move_sprint را نگه می دارد ، و false هنگام انتشار action move_sprint نادرست است. در process_movement ما کدی را اضافه می کنیم که باعث می شود player سرعت بیشتری داشته باشد. در اینجا در process_input ما فقط قصد تغییر متغیر is_sprinting را داریم.ما کاری شبیه آزاد کردن / گرفتن مکان نما برای کار با چراغ قوه انجام می دهیم. ابتدا بررسی می کنیم که آیا عمل چراغ قوه فشرده شده است یا خیر. اگر چنین بود ، سپس بررسی می کنیم که آیا چراغ قوه در درخت صحنه قابل مشاهده است. اگر اینگونه باشد ، آنرا پنهان می کنیم و اگر اینگونه نباشد ، آنرا نشان می دهیم. اکنون ما باید در process_movement دو مورد را تغییر دهیم. ابتدا target * = MAX_SPEED را با موارد زیر جایگزین کنید:if is_sprinting: target *= MAX_SPRINT_SPEEDelse: target *= MAX_SPEEDاکنون به جای اینکه همیشه هدف را در MAX_SPEED ضرب کنیم ، ابتدا بررسی می کنیم که آیا player در حال دویدن است یا نه. اگرplayer در حال دویدن است ، ما در عوض هدف را در MAX_SPRINT_SPEED ضرب می کنیم. اکنون تنها چیزی که باقی مانده تغییر شتاب هنگام دوی سرعت است. accel = ACCEL را به موارد زیر تغییر دهید:if is_sprinting: accel = SPRINT_ACCELelse: accel = ACCELاکنون ، هنگامی که بازیکن در حال دویدن سریع است ، ما به جای ACCEL از SPRINT_ACCEL استفاده خواهیم کرد ، که سرعت بازیکن را سریعتر می کند. اکنون اگر Shift را فشار دهید باید بتوانید دوی سرعت بزنید و با فشار دادن F می توانید چراغ فلش را خاموش و روشن کنید! برو امتحان کن شما می توانید متغیرهای کلاس مربوط به سرعت را تغییر دهید تا پخش کننده هنگام دویدن سریعتر یا کندتر شود!اینم پروژه ساخته شده تا اینجای کارhttps://docs.godotengine.org/en/stable/_downloads/6e5beb2c329d95c6efbfd2bd57bd8067/Godot_FPS_Part_1.zipقسمت دومhttps://vrgl.ir/AVt3b