عباس زنگارکی فراهانی
عباس زنگارکی فراهانی
خواندن ۳۹ دقیقه·۴ سال پیش

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

خب ابتدا اجازه بدین نکاتی رو خدمتتون عرض کنم

اول اینکه سورس کد هایی که قرار داده میشه سینتکسش درست نیس ایندنت ها رعایت نشده لطفا به سایت اصلی گودو مراجعه کنید تا مشکلی نداشته باشین

دوم اینکه انجمن فارسی گودو رو هنوز بعضیا خبری ندارن ازش میتونین در کانال تلگرام انجمن عضو شید @GodotIran

خب بریم سر اصل مطلب


ساخت یک سیستم برای مدیریت انیمیشن ها در ابتدا ما به روشی برای کنترل انیمیشن های در حال تغییر نیاز داریم. Player.tscn را باز کرده و گره AnimationPlayer را انتخاب کنید

(Player -> Rotation_Helper -> Model -> Animation_Player).



یک اسکریپت جدید به نام

AnimationPlayer_Manager.gd

ایجاد کرده و آن را به AnimationPlayer پیوست کنید. کد زیر را به AnimationPlayer_Manager.gd اضافه کنید:


extends AnimationPlayer # Structure -> Animation name :[Connecting Animation states] var states = { "Idle_unarmed":["Knife_equip", "Pistol_equip", "Rifle_equip", "Idle_unarmed"], "Pistol_equip":["Pistol_idle"], "Pistol_fire":["Pistol_idle"], "Pistol_idle":["Pistol_fire", "Pistol_reload", "Pistol_unequip", "Pistol_idle"], "Pistol_reload":["Pistol_idle"], "Pistol_unequip":["Idle_unarmed"], "Rifle_equip":["Rifle_idle"], "Rifle_fire":["Rifle_idle"], "Rifle_idle":["Rifle_fire", "Rifle_reload", "Rifle_unequip", "Rifle_idle"], "Rifle_reload":["Rifle_idle"], "Rifle_unequip":["Idle_unarmed"], "Knife_equip":["Knife_idle"], "Knife_fire":["Knife_idle"], "Knife_idle":["Knife_fire", "Knife_unequip", "Knife_idle"], "Knife_unequip":["Idle_unarmed"], } var animation_speeds = { "Idle_unarmed":1, "Pistol_equip":1.4, "Pistol_fire":1.8, "Pistol_idle":1, "Pistol_reload":1, "Pistol_unequip":1.4, "Rifle_equip":2, "Rifle_fire":6, "Rifle_idle":1, "Rifle_reload":1.45, "Rifle_unequip":2, "Knife_equip":1, "Knife_fire":1.35, "Knife_idle":1, "Knife_unequip":1, } var current_state = null var callback_function = null func _ready(): set_animation("Idle_unarmed") connect("animation_finished", self, "animation_ended") func set_animation(animation_name): if animation_name == current_state: print ("AnimationPlayer_Manager.gd -- WARNING: animation is already ", animation_name) return true if has_animation(animation_name): if current_state != null: var possible_animations = states[current_state] if animation_name in possible_animations: current_state = animation_name play(animation_name, -1, animation_speeds[animation_name]) return true else: print ("AnimationPlayer_Manager.gd -- WARNING: Cannot change to ", animation_name, " from ", current_state) return false else: current_state = animation_name play(animation_name, -1, animation_speeds[animation_name]) return true return false func animation_ended(anim_name): # UNARMED transitions if current_state == "Idle_unarmed": pass # KNIFE transitions elif current_state == "Knife_equip": set_animation("Knife_idle") elif current_state == "Knife_idle": pass elif current_state == "Knife_fire": set_animation("Knife_idle") elif current_state == "Knife_unequip": set_animation("Idle_unarmed") # PISTOL transitions elif current_state == "Pistol_equip": set_animation("Pistol_idle") elif current_state == "Pistol_idle": pass elif current_state == "Pistol_fire": set_animation("Pistol_idle") elif current_state == "Pistol_unequip": set_animation("Idle_unarmed") elif current_state == "Pistol_reload": set_animation("Pistol_idle") # RIFLE transitions elif current_state == "Rifle_equip": set_animation("Rifle_idle") elif current_state == "Rifle_idle": pass; elif current_state == "Rifle_fire": set_animation("Rifle_idle") elif current_state == "Rifle_unequip": set_animation("Idle_unarmed") elif current_state == "Rifle_reload": set_animation("Rifle_idle") func animation_callback(): if callback_function == null: print ("AnimationPlayer_Manager.gd -- WARNING: No callback function for the animation to call!") else: callback_function.call_func()


بیایید بررسی کنیم که این اسکریپت چه کاری انجام می دهد، بیایید با متغیرهای کلاس این اسکریپت شروع کنیم:

state:

اجرای حالت های انیمیشن ما. (توضیحات بیشتر در پایین*)

animation_speeds:

سرعت پخش انیمیشن(توضیح بیشتر در پایین*)

current_state:

متغیری برای نگهداری وضعیت انیمیشنی که در حال حاضر در آن هستیم. (توضیح بیشتر در پایین*)

callback_function:

متغیری برای نگهداری عملکرد callback. (توضیحات بیشتر در زیر) اگر با ماشین های حالت آشنایی دارید ، ممکن است متوجه شده باشید که حالت ها مانند یک ماشین اولیه ساختار دارند. در اینجا تقریباً نحوه تنظیم انها وجود دارد:


state*

تمام انیمیشن ها (حالت هایی) را که می توانیم به آن انتقال دهیم را در خود جای داده است. به عنوان مثال ، اگر در حال حاضر در حالت Idle_unarmed هستیم ، فقط می توانیم به

Knife_equip ، Pistol_ Equip ، Rifle_ Equip و Idle_unarmed

منتقل شویم. اگر بخواهیم به حالتی منتقل شویم که در حالات انتقال احتمالی، قرار نگرفته است ، یک پیام هشدار دریافت می کنیم و انیمیشن تغییر نمی کند. ما همچنین می توانیم به طور خودکار از برخی از حالت ها به حالت های دیگر برویم ، همانطور که در زیر در

animation_ended

توضیح داده خواهد شد


animation_speeds*

سرعت پخش هر انیمیشن است. برخی از انیمیشن ها کمی کند هستند و در تلاشیم تا همه چیز روان به نظر برسد ، باید آنها را با سرعت بیشتری اجرا کنیم.


نکته: توجه داشته باشید که همه انیمیشن های شلیک سریعتر از سرعت نرمال خود هستند. این را به خاطر بسپارید!


current_state*

نام حالت انیمیشنی را که در حال حاضر در آن هستیم نگه خواهد داشت.

سرانجام ، callback_function یک FuncRef خواهد بود که توسط player برای تخم ریزی(spawning ) گلوله در قاب مناسب انیمیشن ارائه می شود.


FuncRef

به ما اجازه می دهد تا یک تابع را به عنوان آرگومان منتقل کنیم ، در واقع به ما اجازه می دهد تا یک تابع را از اسکریپت دیگر فراخوانی کنیم.


اطلاعات بیشتر از FuncRef:


https://docs.godotengine.org/en/stable/classes/class_funcref.html#class-funcref


حالا بیاییدready _ را بررسی کنیم.


ابتدا با استفاده از تابع set_animation ، انیمیشن خود را روی Idle_unarmed تنظیم می کنیم ، بنابراین مطمئناً در آن انیمیشن شروع می کنیم. بعد سیگنال animation_finished را به این اسکریپت متصل می کنیم و آن را به فراخوانی animation_ended اختصاص می دهیم. این بدان معنی است که هر زمان که یک انیمیشن تمام می شود ، animation_ended فراخوانی می شود.


بیایید به set_animation نگاه کنیم.

set_animation

اگر حالت انیمیشنی که در حال حاضر در آن هستیم ، نام حالت انیمیشن گذشته را در state داشته باشد ، سپس به آن انیمیشن تغییر خواهیم کرد.


در مرحله اول ، بررسی می کنیم که آیا نام انیمیشن وارد شده همان نام انیمیشن در حال پخش است یا خیر. اگر آنها یکسان باشند ، ما یک هشدار برای کنسول می نویسیم و trueبرمی گردانیم.

ثانیا ، می بینیم که AnimationPlayer دارای انیمیشن با نام animation_name با استفاده از has_animation است. اگر اینگونه نباشد ، false برمیگردانیم. ثالثاً ، بررسی می کنیم که آیا current_state تنظیم شده است یا خیر. اگر ما حالتی در state_state داشته باشیم ، تمام حالت های ممکن را که می توانیم به آنها انتقال دهیم بدست می آوریم. اگر نام انیمیشن در لیست انتقالهای احتمالی باشد ، ما current_state را روی انیمیشن

pass (animation_name)

تنظیم می کنیم ، به AnimationPlayer می گوییم که انیمیشن را با زمان ترکیبی1- با سرعت تعیین شده در animation_speed پخش کند و true برگردد.(به علت اینکه مطالب یکم نامفهومه سعی کنید کد جلوتون باشه و با توجه بهش پیش برین)


Blend time

مدت زمانی است که می توان دو انیمیشن را با هم مخلوط کرد. با قرار دادن مقدار 1 ، انیمیشن جدید فوراً پخش می شود و بر هر انیمیشنی که قبلاً پخش شده است ، غلبه می کند. اگر مقدار 1 را قرار دهید ، برای یک ثانیه انیمیشن جدید با افزایش قدرت پخش می شود و قبل از پخش انیمیشن جدید ، دو انیمیشن را برای یک ثانیه با هم مخلوط می کند. این منجر به ،انتقال روان بین انیمیشن ها می شود ، که وقتی از یک انیمیشن متحرک به یک انیمیشن در حال اجرا تغییر می کنید عالی به نظر می رسد. ما زمان ترکیبی را روی 1- تنظیم می کنیم زیرا می خواهیم فوراً انیمیشن ها را تغییر دهیم.


حالا اجازه دهید به animation_ended نگاه کنیم. animation_ended تابعی است که با اجرای انیمیشن توسط AnimationPlayer فراخوانی می شود. برای برخی از حالت های انیمیشن ، ممکن است لازم باشد که به پایان برسیم و به حالت دیگری برویم. برای مقابله با این مسئله ، هر حالت انیمیشن احتمالی را بررسی می کنیم. در صورت نیاز ، به حالت دیگری خواهیم رفت.


هشدار

اگر از مدل های متحرک خود استفاده می کنید ، مطمئن شوید که هیچ یک از انیمیشن ها حلقه ای تنظیم نشده باشند. وقتی انیمیشن ها به انتهای انیمیشن می رسند و در آستانه حلقه قرار دارند ، انیمیشن های تمام شده انیمیشن را ارسال نمی کنند توجه داشته باشید انتقال در animation_ended در حالت ایده آل بخشی از داده ها در state است ، اما در تلاش برای درک آسان تر آموزش ، ما کد انتقال هر حالت را در animation_ended سخت خواهیم کرد.


سرانجام ،

animation_callback

این عملکرد با استفاده از اهنگ روش تماس( call method track ) در انیمیشن های ما فراخوانی می شود. اگر یک FuncRef به callback_function اختصاص داده شده باشد ، آنرا فراخوانی کرده ایم. اگر FuncRef به callback_function اختصاص داده نشده باشد ، یک هشدار را برای کنسول چاپ می کنیم.


نکته:برای اطمینان از عدم بروز مشکل در زمان اجرا ، Testing_Area.tscn را اجرا کنید. وقتی بازی اجرا شود به نظر نمی رسد چیزی تغییر کرده باشد ، همه چیز به درستی کار می کند.


آماده شدن انیمیشن ها

اکنون که یک Animation manager فعال داریم ، باید آن را از اسکریپت player خود فراخوانی کنیم. هرچند قبل از آن ، ما باید برخی از آهنگ های برگشت تماس انیمیشن ( animation callback tracks )را در انیمیشن های شلیک شده خود تنظیم کنیم(we need to set some animation callback tracks in our firing animations).

اگر Player.tscn را ندارید ، آن را باز کنید و به گره AnimationPlayer بروید

(Player -> Rotation_Helper -> Model -> Animation_Player).


ما باید یک آهنگ روش تماس( call method track to three of our animations: The firing ) را به سه انیمیشن خود ضمیمه کنیم:

انیمیشن شلیک برای تپانچه ، تفنگ و چاقو. بیایید با تپانچه شروع کنیم. روی لیست کشویی انیمیشن کلیک کنید و "Pistol_fire" را انتخاب کنید. اکنون به پایین لیست آهنگ های انیمیشن بروید. مورد آخر این لیست باید Armature / Skeleton: Left_UpperPointer باشد. اکنون بالای لیست ، روی دکمه "افزودن آهنگ" ، در سمت چپ خط زمان کلیک کنید

با این کار با پنجره ای با چند انتخاب روبرو می شوید. ما می خواهیم آهنگ روش فراخوانی اضافه کنیم ، بنابراین روی گزینه ای که روی " call method track " نوشته شده است کلیک کنید. با این کار پنجره ای باز می شود که کل درخت گره را نشان می دهد. به گره AnimationPlayer بروید ، آن را انتخاب کنید و OK را فشار دهید.

اکنون در پایین لیست آهنگ های انیمیشن ، یک قطعه سبز خواهید داشت که روی آن "AnimationPlayer" نوشته شده است. حال باید نقطه ای را اضافه کنیم که می خواهیم عملکرد پاسخگویی خود را فراخوانی کنیم. جدول زمانی را جابه جا کنید تا به نقطه ای برسید که گلوله شروع به شلیک کند. توجه داشته باشید جدول زمانی پنجره ای است که تمام نقاط انیمیشن ما در آن ذخیره شده است. هر یک از نقاط کوچک نشان دهنده یک نقطه از داده های انیمیشن است. برای پیش نمایش انیمیشن "Pistol_fire" ، گره دوربین زیر Rotator Helper را انتخاب کرده و کادر "Preview" را در گوشه بالا سمت چپ زیر چشم انداز علامت بزنید. اسکراب کردن جدول زمانی به معنای حرکت خودمان از طریق انیمیشن است. بنابراین وقتی می گوییم "جدول زمانی را جابه جاکنید تا به یک نقطه برسید" ، منظور ما حرکت از طریق پنجره انیمیشن تا رسیدن به نقطه روی جدول زمانی است. همچنین ، فلاش پوزه بند ، تابشی از نور است که با شلیک گلوله از پوزه خارج می شود. از پوزه نیز گاهی به عنوان لوله تفنگ یاد می شود( Also, the muzzle of a gun is the end point where the bullet comes out. The muzzle flash is the flash of light that escapes the muzzle when a bullet is fired. The muzzle is also sometimes referred to as the barrel of the gun).

نکته برای کنترل دقیق تر هنگام جابه جایی جدول زمانی ، Ctrl را فشار دهید و برای بزرگنمایی با چرخ ماوس به جلو بروید. پیمایش به عقب بزرگنمایی می شود همچنین می توانید با تغییر مقدار در مرحله (ها) به مقدار پایین / بالاتر ، چگونگی ضربه محکم و ناگهانی جدول زمانی را تغییر دهید. پس از رسیدن به نقطه ای که دوست دارید ، روی ردیف "Animation Player" کلیک راست کرده و Insert Key را فشار دهید. در قسمت نام خالی ، animation_callback را وارد کرده و Enter را فشار دهید.

اکنون وقتی در حال پخش این انیمیشن هستیم ، آهنگ روش تماس در آن نقطه خاص از انیمیشن فعال می شود. بیایید روند انیمیشن های شلیک با تفنگ و چاقو را تکرار کنیم! توجه داشته باشید از آنجا که فرآیند دقیقاً همان تپانچه است ، روند کار باید در عمق کمی کمتر توضیح داده شود. اگر گم شدید مراحل را از بالا دنبال کنید! دقیقاً یکسان است ، فقط در یک انیمیشن متفاوت. از لیست کشویی انیمیشن به انیمیشن "Rifle_fire" بروید. پس از رسیدن به انتهای لیست آهنگ انیمیشن ، با کلیک روی دکمه "افزودن آهنگ" در بالای لیست ، مسیر روش تماس را اضافه کنید. نقطه ای را که پوزه بند شروع به چشمک زدن می کند پیدا کنید و کلیک راست کنید و Insert Key را فشار دهید تا یک نقطه رهگیری روش تماس در آن موقعیت در مسیر اضافه شود. "animation_callback" را در قسمت نام پنجره بازشو تایپ کنید و Enter را فشار دهید. اکنون باید مسیر روش تماس را روی انیمیشن چاقو اعمال کنیم. انیمیشن "Knife_fire" را انتخاب کنید و به پایین قسمت های انیمیشن بروید. روی دکمه "افزودن آهنگ" در بالای لیست کلیک کنید و یک مسیر روش اضافه کنید. سپس یک نقطه در حدود یک سوم اول انیمیشن پیدا کنید تا نقطه روش تماس انیمیشن را در آن قرار دهید.

در واقع ما چاقو را شلیک نخواهیم کرد و انیمیشن به جای شلیک ، یک انیمیشن خیره کننده است( We will not actually be firing the knife, and the animation is a stabbing animation rather than a firing one).

برای این آموزش ما از منطق شلیک اسلحه برای چاقو خود استفاده مجدد می کنیم ، بنابراین انیمیشن به سبک سازگار با سایر انیمیشن ها نامگذاری شده است. از آنجا بر روی جدول زمانی کلیک راست کرده و "Insert Key" را کلیک کنید. "animation_callback" را در قسمت نام قرار داده و Enter را فشار دهید. نکته حتماً کار خود را ذخیره کنید!

با انجام این کار ، ما تقریباً آماده هستیم تا توانایی شلیک کردن به اسکریپت player اضافه کنیم!

ما باید یک صحنه بسازیم:

صحنه برای گلوله ما.


ایجاد صحنه گلوله روش های مختلفی برای کنترل گلوله های اسلحه در بازی های ویدیویی وجود دارد. در این مجموعه آموزشی ، ما به بررسی دو روش رایج خواهیم پرداخت:

اشیا، و برنامه های raycast


این شی ای خواهد بود که در جهان سفر می کند و کد برخورد خود را کنترل می کند. در این روش ما یک شی گلوله را در جهتی که اسلحه ما روبرو است ایجاد و تخم ریزی می کنیم و سپس به جلو حرکت می کند. این روش چندین مزیت دارد. اولین مورد لازم نیست که گلوله ها را در دستگاه پخش خود ذخیره کنیم ، و گلوله به خودی خود با ارسال سیگنال ها وقتی که به جسم برخورد میکند از بین میرود


مزیت دیگر این است که ما می توانیم حرکت گلوله پیچیده تری داشته باشیم. اگر می خواهیم با گذشت زمان گلوله اندکی سقوط کند ، می توانیم اسکریپت کنترل کننده گلوله را به آرامی به سمت زمین فشار دهیم.


استفاده از یک شی همچنین باعث می شود گلوله برای رسیدن به هدف خود طول بکشد ، هر لحظه که به آن اشاره شود فوراً برخورد نمی کند. این احساس واقع بینانه تر است زیرا هیچ چیز در زندگی واقعی بلافاصله از یک نقطه به نقطه دیگر حرکت نمی کند. یکی از معایب بزرگ عملکرد است. در حالی که داشتن هر گلوله مسیرهای خود را محاسبه می کند و برخورد خود را انجام می دهد ، امکان انعطاف پذیری زیادی را فراهم می کند ، اما به عملکرد آن بستگی دارد. با استفاده از این روش ما در هر مرحله حرکت هر گلوله را محاسبه می کنیم و اگرچه این ممکن است برای چند ده گلوله مشکلی ایجاد نکند ، اما در صورت داشتن بالقوه چند صد گلوله می تواند به یک مشکل بزرگ تبدیل شود. اندازه تیرها نمونه برجسته ای هستند زیرا در بسیاری از تیراندازان اول شخص ، تیر ها در موقعیت هدف خود بلافاصله منفجر نمی شوند. شما همچنین می توانید گلوله ها را بارها و بارها مثل نارنجک به عنوان اشیا پیدا کنید زیرا به طور کلی قبل از منفجر شدن در سراسر جهان پرش می کنند.


یکی دیگر از معایب استفاده از گلوله ها شبکه سازی است. اشیا باید موقعیت ها (حداقل) را با تمام کلاینت هایی که به سرور متصل هستند همگام سازی کنند. در حالی که ما هیچ نوع شبکه ای را اجرا نمی کنیم، این نکته ای است که باید هنگام ایجاد تیرانداز اول شخص خود به خاطر داشته باشید ، مخصوصاً اگر قصد دارید به نوعی شبکه در آینده اضافه کنید .



راه دیگر برای رسیدگی به برخوردهای گلوله ای که ما در آن بررسی خواهیم کرد ، پخش مستقیم از شبکه است. این روش در اسلحه هایی که گلوله های متحرک سریعی دارند که به مرور زمان مسیر حرکت آنها به ندرت تغییر می کند بسیار رایج است. به جای ایجاد یک جسم گلوله و ارسال آن از طریق فضا ، ما در عوض یک پرتو را از لوله تفنگ به جلو می فرستیم. منشا raycast را روی موقعیت شروع گلوله قرار می دهیم و براساس طول می توانیم میزان برد گلوله را در فضا تنظیم کنیم.

یک مزیت بزرگ این روش سبک بودن عملکرد آن است. محاسبه ارسال چند صد اشعه از طریق فضا برای کامپیوتر بسیار آسان تر از ارسال چند صد گلوله است. مزیت دیگر این است که ما می توانیم بلافاصله بفهمیم که آیا وقتی چیزی را خواسته ایم به آن ضربه زده ایم یا نه. برای شبکه سازی این مهم است زیرا ما نیازی به همگام سازی حرکات گلوله از طریق اینترنت نداریم ، فقط لازم است که raycast را بزنید یا خیر. هرچند پخش تلویزیونی معایبی دارد. یک نقطه ضعف مهم این است که ما نمی توانیم به راحتی یک پرتو را در چیزی غیر از یک خط قرار دهیم. این بدان معناست که ما فقط می توانیم در یک خط مستقیم شلیک کنیم هرچقدر طول پرتوی ما طولانی باشد. شما می توانید با ریختن چندین اشعه در موقعیت های مختلف ، توهم حرکت گلوله را ایجاد کنید ، اما نه تنها اجرای آن در کد دشوار است ، بلکه از نظر عملکرد نیز سنگین تر است. نقطه ضعف دیگر این است که ما نمی توانیم گلوله را ببینیم. با استفاده از اشیا گلوله در صورت مشاهده ، می توانیم حرکت گلوله در فضا را ببینیم ، اما چون انفجارها بلافاصله اتفاق می افتد ، روش مناسبی برای نشان دادن گلوله ها نداریم. می توانید از مبدا raycast تا نقطه برخورد یک خط بکشید ، و این یکی از روش های محبوب نمایش raycast است. روش دیگر به راحتی کشیدن raycast نیست ، زیرا از نظر تئوری گلوله ها خیلی سریع حرکت می کنند ، چشم ما به هر حال نمی تواند آن را ببیند.


بیایید شی گلوله را تنظیم کنیم.

Bullet_Scene.tscn

را باز کنید. صحنه شامل گره Spatialبه نام گلوله است،یک MeshInstance و یکCollisionShape.

یک اسکریپت جدید به نام Bullet_script.gd ایجاد کنید و آن را به Bullet Spatial پیوست کنید. ما می خواهیم کل شی گلوله را در منتقل کنیم. ما از منطقه(Area) برای بررسی اینکه آیا با چیزی برخورد کرده ایم یا نه استفاده خواهیم کرد


توجه داشته باشید چرا ما از Area استفاده می کنیم و از RigidBody استفاده نمی کنیم؟

دلیل اصلی استفاده نکردن از RigidBody این است که نمی خواهیم گلوله با گره های دیگر RigidBody تعامل داشته باشد. با استفاده از Area اطمینان حاصل می کنیم که هیچ یک از گره ها، از جمله گلوله های دیگر ، مشکل نخواهند داشت. دلیل دیگر صرفاً سهولت در تشخیص برخورد با یک منطقه است! این اسکریپتی است که گلوله ما را کنترل خواهد کرد:


extends Spatial var

BULLET_SPEED = 70

var BULLET_DAMAGE = 15

const KILL_TIMER = 4

var timer = 0

var hit_something = false

func _ready(): $Area.connect("body_entered", self, "collided")

func _physics_process(delta):

var forward_dir = global_transform.basis.z.normalized() global_translate(forward_dir * BULLET_SPEED * delta) timer += delta

if timer >= KILL_TIMER:

queue_free()

func collided(body):

if hit_something == false:

if body.has_method("bullet_hit"): body.bullet_hit(BULLET_DAMAGE, global_transform)

hit_something = true

queue_free()


ابتدا چند متغیر کلاس تعریف می کنیم:

BULLET_SPEED:

سرعتی که گلوله حرکت می کند.

BULLET_DAMAGE:

خسارتی که گلوله به هر چیزی که با آن برخورد کند ، وارد خواهد کرد.

KILL_TIMER:

چه مدت گلوله می تواند بدون اصابت به چیزی دوام بیاورد.

timer:

برای ردیابی مدت زنده بودن گلوله.

hit_something:

بولینی برای ردیابی اینکه آیا چیزی را زده ایم یا نه.

به استثنای timer و hit_something

همه این متغیرها نحوه تعامل گلوله با جهان را تغییر می دهند. توجه داشته باشید دلیل استفاده ما از تایمر کشتن این است که موردی نداریم که گلوله ای برای همیشه سفر کند. با استفاده از یک تایمر kill می توان اطمینان حاصل کرد که هیچ گلوله ای برای همیشه سفر نمی کند و منابع را مصرف نمی کند.


نکته همانطور که در قسمت 1 ، همه متغیرهای کلاس بزرگ را چند زوج داریم. دلیل این امر همان دلیلی است که در قسمت 1 آورده شده است: ما می خواهیم با این متغیرها مانند ثابت رفتار کنیم ، اما می خواهیم بتوانیم آنها را تغییر دهیم. در این صورت بعداً باید آسیب و سرعت این گلوله ها را تغییر دهیم ، بنابراین باید متغیر باشند و ثابت نباشند.

در _ready قبلاً سیگنال body_entered منطقه را روی خود تنظیم کردیم تا هنگام ورود بدن به ناحیه ، عملکرد برخورد را فراخوانی کند.

_physics_proces ،

محور Z محلی گلوله را بدست می آورد. اگر به صحنه در حالت محلی نگاه کنید ، متوجه خواهید شد که گلوله به سمت محور محلی مثبت Z رو به رو است. در مرحله بعدی ، ما کل گلوله را با توجه به جهت رو به جلو هدایت می کنیم ، در سرعت و زمان دلتا را ضرب می کنیم. بعد از آن زمان دلتا را به تایمر خود اضافه می کنیم و بررسی می کنیم که آیا تایمر به مقداری بزرگتر از ثابت KILL_TIME رسیده است. اگر داشته باشد ، از queue_free برای آزاد کردن گلوله استفاده می کنیم. در برخورد ما بررسی می کنیم که آیا هنوز چیزی را زده ایم یا نه. بخاطر داشته باشید که برخورد فقط زمانی وارد می شود که شی ای وارد گره Area شود. اگر گلوله قبلاً با چیزی برخورد نکرده باشد ، سپس بررسی می کنیم که بدنی که گلوله با آن برخورد کرده عملکردی به نام bullet_hit دارد یا خیر. اگر اینگونه باشد ، ما آن را صدا می کنیم و آسیب گلوله و تغییر شکل گلوله را تاثیر می دهیم تا بتوانیم چرخش و موقعیت گلوله را بدست آوریم.


در برخورد ، بدن می تواند یک StaticBody ، RigidBody یا KinematicBody باشد ما متغیر hit_something Bullet را روی true قرار می دهیم زیرا صرف نظر از اینکه بدنی که گلوله با آن برخورد کرده است عملکرد bullet_hit را دارد یا خیر ، به چیزی برخورد کرده است و بنابراین باید مطمئن شویم که گلوله به چیز دیگری برخورد نمی کند. سپس گلوله را با استفاده از queue_free حذف می کنیم.

نکته شاید از خود بپرسید که چرا گلوله را با استفاده از queue_free به محض برخورد به چیزی حذف می کنیم ، حتی یک متغیر hit_something داریم. دلیل اینکه ما باید ردیابی کنیم که آیا چیزی را مورد اصابت قرار داده ایم یا نه این است که queue_free بلافاصله گره را حذف نمی کند ، بنابراین گلوله می تواند با جسم دیگری برخورد کند قبل از اینکه گودو فرصتی برای آزاد شدن آن پیدا کند. با ردیابی اینکه گلوله به چیزی اصابت کرده است ، می توانیم مطمئن شویم که گلوله فقط به یک جسم برخورد خواهد کرد. قبل از اینکه دوباره برنامه نویسی player را شروع کنیم ، بیایید به Player.tscn بیندازیم. دوباره Player.tscn را باز کنید. Rotation_Helper را گسترش دهید میبینید که دو گره وجود دارد:

Gun_Fire_Points و Gun_Aim_Point.



Gun_aim_point

نقطه ای است که گلوله ها به سمت آن نشانه می روند. توجه کنید که چگونه با مرکز صفحه صف کشیده شده و یک فاصله به جلو در محور Z کشیده شده است. Gun_aim_point به عنوان نقطه ای عمل می کند که گلوله ها به طور حتم با آن برخورد می کنند.


یک نمونه مش نامرئی برای اشکال زدایی وجود دارد. مش یک کره کوچک است که نشان می دهد گلوله ها به کدام هدف برخورد میکنند.

Gun_Fire_Points

را باز کنید و سه گره spatial دیگر پیدا خواهید کرد ، یکی برای هر سلاح. Rifle_Point را باز کنید و یک گره Raycast پیدا خواهید کرد. این جایی است که ما ماشین های انفجاری گلوله های تفنگ خود را ارسال خواهیم کرد. طول raycast تعیین می کند که گلوله های ما تا کجا مسافت را طی کنند. ما از گره Raycast برای کنترل گلوله تفنگ استفاده می کنیم زیرا می خواهیم تعداد زیادی گلوله را سریع شلیک کنیم. اگر از اشیا گلوله استفاده کنیم ، کاملاً ممکن است در ماشین های قدیمی به مشکلات عملکردی برخورد کنیم. توجه داشته باشید اگر فکر می کنید موقعیت نقاط از کجا آمده است ، موقعیت های خشن انتهای هر سلاح است.

با رفتن به AnimationPlayer ، انتخاب یکی از انیمیشن های شلیک شده و مرور زمان بندی ، می توانید این موضوع را مشاهده کنید.

نکته raycast برای هر سلاح باید بیشتر در انتهای هر سلاح قرار بگیرد.

Knife_Point

را باز کنید و یک گره Area پیدا خواهید کرد. ما از یک چاقو برای استفاده از یک ناحیه استفاده می کنیم زیرا فقط از تمام اجسام نزدیک خود مراقبت می کنیم و چاقوی ما به فضا شلیک نمیشود. اگر ما در حال ساخت چاقوی پرتاب کننده بودیم ، به احتمال زیاد یک شی گلوله را تخم ریزی می کنیم که شبیه چاقو است. در آخر ، ما Pistol_Point داریم. این همان نقطه ای است که ما اشیا گلوله خود را ایجاد می کنیم. ما در اینجا به هیچ گره اضافی نیاز نداریم ، زیرا گلوله تمام تشخیص برخورد خود را کنترل می کند. اکنون که دیدیم چگونه دیگر سلاح های خود را کنترل خواهیم کرد و گلوله ها را در کجا تخم ریزی خواهیم کرد ، بیایید کار کردن آنها را شروع کنیم.


در صورت تمایل می توانید به گره های HUD نیز نگاه کنید. هیچ چیز فانتزی در آنجا وجود ندارد و غیر از استفاده از یک برچسب ، ما هیچ یک از این گره ها را لمس نخواهیم کرد. رابط های طراحی را با گره های کنترلControl برای آموزش استفاده از گره های GUI بررسی کنید. ایجاد اولین سلاح بیایید کد مربوط به هر یک از سلاح های خود را بنویسیم ، با شروع از تپانچه.

Pistol_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Pistol_Point)

را انتخاب کرده و اسکریپت جدیدی به نام

Weapon_Pistol.gd

ایجاد کنید. کد زیر را به

Weapon_Pistol.gd

اضافه کنید:


extends Spatial

const DAMAGE = 15

const IDLE_ANIM_NAME = "Pistol_idle"

const FIRE_ANIM_NAME = "Pistol_fire"

var is_weapon_enabled = false

var bullet_scene = preload("Bullet_Scene.tscn")

var player_node = null

func _ready():

pass

func fire_weapon():

var clone = bullet_scene.instance()

var scene_root = get_tree().root.get_children()[0] scene_root.add_child(clone) clone.global_transform = self.global_transform

clone.scale = Vector3(4, 4, 4)

clone.BULLET_DAMAGE = DAMAGE

func equip_weapon():

if player_node.animation_manager.current_state == IDLE_ANIM_NAME:

is_weapon_enabled = true return true

if player_node.animation_manager.current_state == "Idle_unarmed":

player_node.animation_manager.set_animation("Pistol_equip") return false

func unequip_weapon():

if player_node.animation_manager.current_state == IDLE_ANIM_NAME:

if player_node.animation_manager.current_state != "Pistol_unequip":

player_node.animation_manager.set_animation("Pistol_unequip")

if player_node.animation_manager.current_state == "Idle_unarmed":

is_weapon_enabled = false return true

else:

return false


ابتدا برخی متغیرهای کلاس را که در اسکریپت به آنها نیاز داریم تعریف می کنیم:

DAMAGE:

میزان خسارت یک گلوله.

IDLE_ANIM_NAME:

نام انیمیشن بیکار تپانچه است.

FIRE_ANIM_NAME:

نام انیمیشن آتش تپانچه است.

is_weapon_enabled:

متغیری برای بررسی استفاده یا فعال بودن این سلاح.

bullet_scene:

صحنه گلوله ای که قبلاً روی آن کار کردیم.

player_node:

متغیری برای نگه داشتن Player.gd.

دلیل اینکه بیشتر این متغیرها را تعریف می کنیم این است که می توانیم از آنها در Player.gd استفاده کنیم. هر کدام از سلاح هایی که خواهیم ساخت ، همه این متغیرها را خواهد داشت (منهای bullet_scene) بنابراین ما یک رابط ثابت برای تعامل در Player.gd داریم. با استفاده از متغیرها / توابع مشابه در هر سلاح ، می توانیم بدون نیاز به دانستن اینکه از کدام سلاح استفاده می کنیم ، با آنها تعامل داشته باشیم ، این باعث می شود کد ما بسیار مدولارتر شود زیرا ما می توانیم اسلحه را بدون نیاز به تغییر بسیاری از کد در Player.gd اضافه کنیم. و کار خواهد کرد. ما می توانیم همه کدها را در Player.gd بنویسیم ، اما پس از آن با افزودن سلاح ، مدیریت Player.gd دشوارتر می شود. با استفاده از یک طراحی مدولار با یک رابط سازگار ، می توانیم Player.gd را زیبا و مرتب نگه داریم ، در حالی که افزودن / حذف / اصلاح سلاح را نیز آسان تر می کنیم.


در _ ready به سادگی از آن عبور می کنیم. یک نکته قابل توجه است ، فرضیه ای که در برخی موارد Player.gd را پر خواهیم کرد. ما می خواهیم فرض کنیم که Player.gd قبل از فراخوانی هر یک از توابع موجود در Weapon_Pistol.gd ، خود را از دست خواهد داد. اگرچه این می تواند به موقعیت هایی منجر شود که بازیکن خود را رد نمی کند (زیرا ما فراموش می کنیم) ، برای بازیابی بازیکن باید یک رشته طولانی از تماسهای get_parent داشته باشیم تا درخت صحنه را رد کنیم. این خیلی زیبا به نظر نمی رسد

(get_parent (). get_parent (). get_parent ())

و غیره و فرض

اینکه بخاطر بسپاریم که خود را به هر سلاحی در Player.gd منتقل می کنیم نسبتاً ایمن است. بعد بیایید به fire_weapon نگاه کنیم:


اولین کاری که ما انجام می دهیم این است که صحنه گلوله ای را که قبلاً ساخته بودیم ، نمونه کنیم. نکته با استفاده از صحنه ، ما یک گره جدید ایجاد می کنیم که تمام گره (های) صحنه را که در آن صحنه نصب کرده ایم نگه می دارد و به طور موثر آن صحنه را شبیه سازی می کند. سپس ما یک کلون به اولین گره کودک ریشه صحنه ای که هم اکنون در آن هستیم ، اضافه می کنیم. با این کار ، ما آن را فرزند گره اصلی صحنه بارگیری شده فعلی می کنیم. به عبارت دیگر ، ما در حال بارگذاری کلون به عنوان فرزند گره اول (هرچه در بالای درخت صحنه است) در صحنه بارگذاری شده / باز شده در حال حاضر هستیم. اگر صحنه بارگیری شده / باز شده در حال آزمایش Testing_Area.tscn است ، ما می توانیم کلون خود را به عنوان فرزند Testing_Area ، گره اصلی در آن صحنه ، اضافه کنیم.


در مرحله بعدی ، تغییر شکل کلی کلون را به شکل تبدیل جهانی Pistol_Point قرار می دهیم. دلیل این کار ما این است که گلوله در انتهای تپانچه تخم ریزی می شود. با کلیک روی AnimationPlayer و پیمایش در Pistol_fire می توانید ببینید که Pistol_Point در انتهای تپانچه قرار گرفته است. بعد آن را با ضریب 4 مقیاس بندی می کنیم زیرا صحنه گلوله به طور پیش فرض کمی کوچک است. سپس میزان خسارت گلوله (BULLET_DAMAGE) را روی میزان خسارت یک گلوله تپانچه تنظیم می کنیم (DAMAGE). حالا بیایید به equip_weapon نگاهی بیندازیم:

اولین کاری که ما انجام می دهیم بررسی این است که آیا مدیر انیمیشن در انیمیشن بیکار تپانچه idle قرار دارد یا خیر. اگر در انیمیشن بیکار تپانچه هستیم ، تنظیم می کنیم

is_weapon_enabled

به true زیرا اسلحه با موفقیت تجهیز شده است. از آنجا که می دانیم انیمیشن تجهیز تپانچه unequip ما به طور خودکار به انیمیشن بیکار تپانچهidle منتقل می شود ، اگر در انیمیشن بیکار تپانچه هستیم ، تپانچه باید اجرای انیمیشن تجهیز را تمام کرده باشد.


ما می دانیم که این انیمیشن ها انتقال می یابند برای همین ما کد را برای انتقال آنها در Animation_Manager.gd نوشتیم بعد بررسی می کنیم که آیا پخش کننده در وضعیت انیمیشن Idle_unarmed است. از آنجا که تمام انیمیشن های تکنیکی به این حالت می روند و از آنجا که می توان هر اسلحه ای را از این حالت مجهز کرد ، اگر بازیکن در حالت Idle_unarmed باشد ، ما انیمیشن ها را به Pistol_ Equip تغییر می دهیم. از آنجا که می دانیم Pistol_ Equip به Pistol_idle منتقل می شود ، دیگر نیازی به پردازش اضافی برای تجهیز سلاح نداریم ، اما چون هنوز قادر به تجهیز تپانچه نبودیم ، false بر میگردانیم. در آخر ، بیایید به unequip_weapon نگاهی بیندازیم:

unequip_weapon

مانند equip_weapon است ، اما در عوض ما برعکس چیزها را بررسی می کنیم. ابتدا بررسی می کنیم که آیا player در وضعیت انیمیشن بیکارidle است یا خیر. سپس بررسی می کنیم که player در انیمیشن Pistol_unequip نیست. اگر player در انیمیشن Pistol_unequip نیست ، می خواهیم انیمیشن pistol_unequip را اجرا کنیم. توجه داشته باشید ممکن است از خود بپرسید که چرا ما بررسی می کنیم که آیا بازیکن در انیمیشن بیکار تپانچه حضور دارد یا خیر ، و سپس مطمئن می شویم که بازیکن بلافاصله از تکنیک خارج نیست. دلیل بررسی اضافی این است که قبل از اینکه فرصتی برای پردازش

set_animation

داشته باشیم ، می توانیم در موارد نادر دوبار با unequip_weapon تماس بگیریم ، بنابراین برای اطمینان از پخش انیمیشن unquip ، این چک اضافی را اضافه می کنیم. در مرحله بعدی بررسی می کنیم که آیا پخش کننده در Idle_unarmed قرار دارد یا خیر ، این همان وضعیت انیمیشنی است که از Pistol_unequip به آن منتقل خواهیم شد. اگر بازیکن در Idle_unarmed باشد ، از آنجا که دیگر از این سلاح استفاده نمی کنیم ، is_weapon_enabled را روی false تنظیم می کنیم و به دلیل اینکه تپانچه را با موفقیت از عوض کردیم ، true بر میگردانیم. اگر بازیکن در Idle_unarmed نباشد ، ما false برمیگردانیم زیرا هنوز با موفقیت تپانچه را مجهز نکرده ایم.


ایجاد دو سلاح دیگر اکنون که همه کدی را که برای تپانچه مورد نیاز است در اختیار ما قرار داد ، بیایید کد اسلحه و چاقو را در قسمت بعدی اضافه کنیم

Rifle_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Rifle_Point)

را انتخاب کنید و یک اسکریپت جدید بنام Weapon_Rifle.gd ایجاد کنید ، سپس موارد زیر را اضافه کنید:


extends Spatial

const DAMAGE = 4

const IDLE_ANIM_NAME = "Rifle_idle"

const FIRE_ANIM_NAME = "Rifle_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():

pass

func fire_weapon():

var ray = $Ray_Cast

ray.force_raycast_update()

if ray.is_colliding():

var body = ray.get_collider()

if body == player_node:

pass

elif body.has_method("bullet_hit"): body.bullet_hit(DAMAGE, ray.global_transform)

func equip_weapon():

if player_node.animation_manager.current_state == IDLE_ANIM_NAME:

is_weapon_enabled = true

return true

if player_node.animation_manager.current_state == "Idle_unarmed":

player_node.animation_manager.set_animation("Rifle_equip") return false

func unequip_weapon():

if player_node.animation_manager.current_state == IDLE_ANIM_NAME:

if player_node.animation_manager.current_state != "Rifle_unequip":

player_node.animation_manager.set_animation("Rifle_unequip")

if player_node.animation_manager.current_state == "Idle_unarmed":

is_weapon_enabled = false

return true

return false


بیشتر این موارد دقیقاً همان Weapon_Pistol.gd است ، بنابراین ما فقط قصد داریم به موارد تغییر یافته نگاه کنیم:

fire_weapon

اولین کاری که ما می کنیم این است که گره Raycast را بدست آوریم ، که فرزند Rifle_Point است. بعد Raycast را مجبور می کنیم با استفاده از force_raycast_update به روز شود. این کار Raycast را مجبور می کند وقتی که آن را فراخوانی می کنیم ، برخورد را تشخیص دهد ، به این معنی که با جهان فیزیک سه بعدی یک بررسی تصادف کامل قاب دریافت می کنیم. سپس بررسی می کنیم که آیا Raycast با چیزی برخورد کرده است یا خیر. اگر Raycast با چیزی برخورد کرده باشد ، ابتدا بدنه برخوردی را که با آن برخورد کرده است بدست می آوریم. این می تواند StaticBody ، RigidBody یا KinematicBody باشد. در مرحله بعدی می خواهیم اطمینان حاصل کنیم که بدنی که با آن برخورد کرده ایم بازیکن نیست ، زیرا ما (احتمالاً) نمی خواهیم به بازیکن توانایی شلیک به پا را بدهیم. اگر بدن player نیست ، سپس بررسی می کنیم که آیا عملکردی به نام bullet_hit دارد یا خیر. اگر این اتفاق بیفتد ، ما آن را صدا می کنیم و میزان خسارت این گلوله را وارد می کنیم (DAMAGE) ، و تغییر شکل کلی Raycast ، بنابراین می توانیم تشخیص دهیم که گلوله از کدام جهت به وجود آمده است. اکنون تنها کاری که باید انجام دهیم نوشتن کد چاقو است.

Knife_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Knife_Point)

را انتخاب کرده و اسکریپت جدیدی به نام Weapon_Knife.gd ایجاد کرده و موارد زیر را اضافه کنید:


extends Spatial

const DAMAGE = 40

const IDLE_ANIM_NAME = "Knife_idle"

const FIRE_ANIM_NAME = "Knife_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():

pass

func fire_weapon():

var area = $Area

var bodies = area.get_overlapping_bodies()

for body in bodies:

if body == player_node:

continue

if body.has_method("bullet_hit"): body.bullet_hit(DAMAGE, area.global_transform)

func equip_weapon():

if player_node.animation_manager.current_state == IDLE_ANIM_NAME:

is_weapon_enabled = true

return true

if player_node.animation_manager.current_state == "Idle_unarmed":

player_node.animation_manager.set_animation("Knife_equip")

return false

func unequip_weapon():

if player_node.animation_manager.current_state == IDLE_ANIM_NAME:

player_node.animation_manager.set_animation("Knife_unequip")

if player_node.animation_manager.current_state == "Idle_unarmed":

is_weapon_enabled = false

return true

return false


همانند Weapon_Rifle.gd ، تنها تفاوت ها در fire_weapon است ، بنابراین بیایید به آن نگاه کنیم:

اولین کاری که ما می کنیم این است که گره کودک Area از Knife_Point را بدست آوریم. بعد می خواهیم تمام بدنه های برخورد را با استفاده از get_overlapping_bodies در داخل منطقه Area بدست آوریم. با این کار لیستی از هر جسمی که منطقه Area را لمس می کند باز می گردد. ما می خواهیم از طریق هر یک از آن اجساد عبور کنیم. ابتدا بررسی می کنیم که بدن بازیکن نیست ، زیرا نمی خواهیم به بازیکن اجازه دهیم خود را خنجر بزند. اگر بدن بازیکن است ، ما از ادامه استفاده می کنیم بنابراین می پریم و بدن بعدی را نگاه می کنیم. اگر به بدن بعدی پرش نکرده ایم ، سپس بررسی می کنیم که آیا بدن عملکرد bullet_hit را دارد یا خیر. اگر این اتفاق بیفتد ، ما می توانیم مقدار خسارتی را که یک ضربه چاقو متحمل می شود (DAMAGE) و تحول جهانی منطقه وارد کنیم. توجه داشته باشید در حالی که می توانستیم مکانی خشن برای محل اصابت دقیق چاقو محاسبه کنیم ، اما ما نمی خواهیم این کار را انجام دهیم زیرا استفاده از موقعیت منطقهArea به اندازه کافی خوب کار می کند و زمان اضافی لازم برای محاسبه موقعیت خشن برای هر بدن ارزش تلاش را ندارد.


کار کردن سلاح ها بیایید شروع به کار کردن سلاح ها در Player.gd کنیم.

ابتدا بیایید متغیرهای کلاس مورد نیاز برای سلاح ها را اضافه کنیم:


# Place before _ready

var animation_manager

var current_weapon_name = "UNARMED"

var weapons = {"UNARMED":null, "KNIFE":null, "PISTOL":null, "RIFLE":null}

const WEAPON_NUMBER_TO_NAME = {0:"UNARMED", 1:"KNIFE", 2:"PISTOL", 3:"RIFLE"}

const WEAPON_NAME_TO_NUMBER = {"UNARMED":0, "KNIFE":1, "PISTOL":2, "RIFLE":3}

var changing_weapon = false

var changing_weapon_name = "UNARMED"

var health = 100

var UI_status_label


اجازه دهید بررسی کنیم که این متغیرهای جدید چه کاری انجام می دهند:

animation_manager:

این گره AnimationPlayer و اسکریپت آن را که قبلاً نوشتیم در خود نگه می دارد.

current_weapon_name:

نام سلاحی که ما در حال حاضر از آن استفاده می کنیم. این چهار مقدار ممکن دارد: UNARMED ، KNIFE ، PISTOL ، و RIFLE

arms:

دیکشنری که تمام گره های سلاح را در خود جای می دهد.

WEAPON_NUMBER_TO_NAME:

ما از این برای تغییر سلاح استفاده خواهیم کرد.

WEAPON_NAME_TO_NUMBER:

به ما امکان می دهد نام اسلحه را به شماره آن تبدیل کنیم. ما از این برای تغییر سلاح استفاده خواهیم کرد.

change_weapon:

بولینی برای ردیابی اینکه آیا ما اسلحه / سلاح را تغییر می دهیم یا نه.

change_weapon_name:

نام سلاحی است که می خواهیم به آن تغییر دهیم.

health:

بازیکن ما چقدر سلامتی دارد. در این قسمت از آموزش ما از آن استفاده نخواهیم کرد.

UI_status_label:

برچسبی برای نشان دادن میزان سلامتی ما ، و مقدار مهمات. در مرحله بعدی باید چند مورد را در _ready اضافه کنیم:

func _ready():

camera = $Rotation_Helper/Camera

rotation_helper = $Rotation_Helper

animation_manager = $Rotation_Helper/Model/Animation_Player

animation_manager.callback_function = funcref(self, "fire_bullet")

Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

weapons["KNIFE"] = $Rotation_Helper/Gun_Fire_Points/Knife_Point

weapons["PISTOL"] = $Rotation_Helper/Gun_Fire_Points/Pistol_Point

weapons["RIFLE"] = $Rotation_Helper/Gun_Fire_Points/Rifle_Point

var gun_aim_point_pos = $Rotation_Helper/

Gun_Aim_Point.global_transform.origin

for weapon in weapons:

var weapon_node = weapons[weapon]

if weapon_node != null:

weapon_node.player_node = self

weapon_node.look_at(gun_aim_point_pos, Vector3(0, 1, 0)) weapon_node.rotate_object_local(Vector3(0, 1, 0), deg2rad(180))

current_weapon_name = "UNARMED"

changing_weapon_name = "UNARMED"

UI_status_label = $HUD/Panel/Gun_label

flashlight = $Rotation_Helper/Flashlight


بیایید آنچه تغییر کرده است را مرور کنیم. ابتدا گره AnimationPlayer را دریافت کرده و آن را به متغیر animation_manager اختصاص می دهیم. سپس عملکرد callback را روی FuncRef تنظیم می کنیم که عملکرد fire_bullet بازیکن را فراخوانی کند. در حال حاضر ما تابع fire_bullet را ننوشته ایم ، اما به زودی به آنجا خواهیم رسید. بعد همه گره های سلاح را می گیریم و آنها را به سلاح ها اختصاص می دهیم. این به ما امکان می دهد فقط به نام گره های سلاح (KNIFE ، PISTOL یا RIFLE) دسترسی پیدا کنیم. سپس موقعیت جهانی Gun_Aim_Point را بدست می آوریم تا بتوانیم اسلحه های بازیکن را بچرخانیم. سپس ما از طریق هر سلاح در اسلحه عبور می کنیم. ما ابتدا گره سلاح را بدست می آوریم. اگر گره اسلحه تهی نباشد ، سپس متغیر player_node آن را روی این اسکریپت (Player.gd) تنظیم می کنیم. سپس ما باید آن را با استفاده از تابع look_at در gun_aim_point_pos نگاه کنیم و سپس آن را 180 درجه در محور Y بچرخانیم. توجه داشته باشید ما تمام آن نقاط سلاح را 180 درجه روی محور Y آنها می چرخانیم زیرا دوربین ما به سمت عقب است. اگر همه این نقاط سلاح را 180 درجه نچرخانیم ، همه سلاح ها به عقب شلیک می شوند. سپس نام current_weapon_name و change_weapon_name را به UNARMED تنظیم می کنیم. سرانجام ، ما برچسب UI را از HUD خود دریافت می کنیم. بیایید فراخوانی عملکرد جدیدی را به _physics_proces اضافه کنیم تا بتوانیم اسلحه ها را تغییر دهیم. این کد جدید است:


func _physics_process(delta):

process_input(delta)

process_movement(delta)

process_changing_weapons(delta)


حالا ما اسلحه های process_changing_ را فراخوانی می کنیم. حالا بیایید تمام کد ورودی player را برای سلاح های موجود در process_input اضافه کنیم. کد زیر را اضافه کنید:


# ---------------------------------- # Changing weapons.

var weapon_change_number = WEAPON_NAME_TO_NUMBER[current_weapon_name]

if Input.is_key_pressed(KEY_1):

weapon_change_number = 0

if Input.is_key_pressed(KEY_2):

weapon_change_number = 1

if Input.is_key_pressed(KEY_3):

weapon_change_number = 2

if Input.is_key_pressed(KEY_4):

weapon_change_number = 3

if Input.is_action_just_pressed("shift_weapon_positive"):

weapon_change_number += 1

if Input.is_action_just_pressed("shift_weapon_negative"):

weapon_change_number -= 1

weapon_change_number = clamp(weapon_change_number, 0, WEAPON_NUMBER_TO_NAME.size() - 1)

if changing_weapon == false:

if WEAPON_NUMBER_TO_NAME[weapon_change_number] !=

current_weapon_name:

changing_weapon_name = WEAPON_NUMBER_TO_NAME[weapon_change_number]

changing_weapon = true # ---------------------------------- # ---------------------------------- # Firing the weapons

if Input.is_action_pressed("fire"):

if changing_weapon == false:

var current_weapon = weapons[current_weapon_name]

if current_weapon != null:

if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:

animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)


بیایید با اضافه کردن چگونگی تغییر سلاح ، موارد اضافی را مرور کنیم. ابتدا شماره سلاح فعلی را دریافت می کنیم و آن را به weapon_change_number اختصاص می دهیم. سپس بررسی می کنیم که آیا یکی از کلیدهای عددی (کلیدهای 1-4) فشرده شده است یا خیر. در صورت وجود ، weapon_change_number را بر روی مقدار تعیین شده در آن کلید تنظیم می کنیم. توجه داشته باشید دلیل اینکه کلید 1 به 0 نگاشت می شود این است که اولین عنصر در یک لیست نگاشته می شود ، نه یک. بیشتر اکسسوری های لیست / آرایه ها در اکثر زبان های برنامه نویسی به جای 1 از 0 شروع می شوند. برای اطلاعات بیشتر به

https://en.wikipedia.org/wiki/

Nero-base-number

مراجعه کنید

بعد بررسی می کنیم که shift_weapon_positive یا shift_weapon_negative فشار داده شده است. اگر یکی از آنها باشد ، عدد 1 را از gun_change_num جمع و کم می کنیم. از آنجا که ممکن است بازیکن weapon_change_number را به خارج از تعداد اسلحه ای که بازیکن در اختیار دارد منتقل کرده باشد ، ما آن را گیره می کنیم بنابراین نمی تواند از حداکثر تعداد اسلحه ای که بازیکن دارد بیشتر شود و عدد 0 یا weapon_change_number را تغییر می دهد.


سپس بررسی می کنیم که آیا بازیکن در حال تغییر اسلحه نیست. اگر نیست ، سپس بررسی می کنیم که آیا سلاحی که بازیکن می خواهد به آن تغییر دهد سلاح جدیدی است یا نه اگر سلاحی که بازیکن می خواهد به آن تغییر دهد سلاح جدیدی است ، پس از آن change_weapon_name را به اسلحه در gun_change_number تنظیم کرده و change_weapon را روی true قرار می دهیم. برای شلیک سلاح ابتدا بررسی می کنیم که آیا عمل آتش فشرده شده است یا خیر. سپس بررسی می کنیم که بازیکن تغییر سلاح نمی دهد. بعد گره سلاح را برای سلاح فعلی دریافت می کنیم. اگر گره سلاح فعلی برابر با null نباشد و پخش کننده در وضعیت IDLE_ANIM_NAME خود باشد ، ما انیمیشن پخش کننده را روی FIRE_ANIM_NAME اسلحه فعلی تنظیم می کنیم. بیایید کد زیر را به process_changing_weapons اضافه کنید:


func process_changing_weapons(delta):

if changing_weapon == true:

var weapon_unequipped = false var current_weapon = weapons[current_weapon_name]

if current_weapon == null: weapon_unequipped = true else:

if current_weapon.is_weapon_enabled == true:

weapon_unequipped = current_weapon.unequip_weapon()

else:

weapon_unequipped = true

if weapon_unequipped == true:

var weapon_equipped = false

var weapon_to_equip = weapons[changing_weapon_name]

if weapon_to_equip == null:

weapon_equipped = true

else:

if weapon_to_equip.is_weapon_enabled == false:

weapon_equipped = weapon_to_equip.equip_weapon()

else: weapon_equipped = true

if weapon_equipped == true:

changing_weapon = false

current_weapon_name = changing_weapon_name

changing_weapon_name = ""


بیایید مرور کنیم آنچه در اینجا اتفاق می افتد:

اولین کاری که ما انجام می دهیم اطمینان از دریافت ورودی برای تغییر سلاح است. این کار را با اطمینان از صحت changing_weapons انجام می دهیم. در مرحله بعد ما یک متغیر (arms_unequipped) تعریف می کنیم ، بنابراین می توانیم بررسی کنیم که آیا سلاح فعلی با موفقیت مجهز شده است یا خیر. اگر سلاح فعلی پوچ نیست ، پس باید بررسی کنیم که آیا سلاح فعال است یا خیر. اگر سلاح فعال باشد ، ما عملکرد unequip_weapon آن را فراخوانی می کنیم تا انیمیشن unquip را شروع کند. اگر اسلحه فعال نباشد ، arms_unequipped را به true تنظیم می کنیم زیرا سلاح با موفقیت تجهیز نشده است. اگر سلاح فعلی خنثیnull است ، پس می توانیم به سادگی weapon_unequipped را به true تنظیم کنیم. دلیل این که ما این بررسی را انجام می دهیم این است که هیچ اسکریپت / گره اسلحه ای برای UNARMED وجود ندارد ، اما همچنین هیچ انیمیشنی برای UNARMED وجود ندارد ، بنابراین ما می توانیم تجهیز سلاحی را که بازیکن می خواهد تغییر دهد ، شروع کنیم.


اگر بازیکن اسلحه فعلی را با موفقیت مجهز نکرده است

arms_unequipped == true

ما باید سلاح جدید را تجهیز کنیم. ابتدا یک متغیر جدید (arms_equipped) برای پیگیری اینکه آیا بازیکن با موفقیت سلاح جدید را مجهز کرده است یا خیر ، تعریف می کنیم. سپس سلاحی را دریافت می کنیم که بازیکن می خواهد به آن تغییر دهد. اگر سلاحی که بازیکن می خواهد به آن تغییر دهد پوچ نیست ، سپس بررسی می کنیم که آیا فعال است یا خیر. اگر فعال نباشد ، ما تابع equip_weapon آن را فراخوانی می کنیم تا شروع به تجهیز سلاح کند. اگر اسلحه فعال باشد ، arms_equipped را به true تنظیم می کنیم. اگر اسلحه ای که بازیکن می خواهد به آن تغییر دهد خالی از سکنه است ، ما به سادگی arms_equipped را به true تنظیم می کنیم زیرا هیچ گره / اسکریپتی برای UNARMED نداریم و همچنین هیچ انیمیشن نداریم. در آخر ، بررسی می کنیم که آیا بازیکن سلاح جدید را با موفقیت مجهز کرده است یا خیر. اگر او این کار را انجام داده باشد ، ما change_weapon را false قرار می دهیم زیرا بازیکن دیگر سلاح را تغییر نمی دهد. ما از زمانی که سلاح فعلی تغییر کرده است ، current_weapon_name را بر روی changing_weapon_name قرار می دهیم و سپس changing_weapon_name را به یک رشته خالی تنظیم می کنیم.


حالا ، ما باید یک عملکرد دیگر به بازیکن اضافه کنیم ، و سپس بازیکن آماده شلیک اسلحه است! ما باید fire_bullet را اضافه کنیم که توسط AnimationPlayer در آن نقاطی که قبلاً در مسیر عملکرد AnimationPlayer تنظیم کردیم فراخوانی می شود:


func fire_bullet():

if changing_weapon == true:

return

weapons[current_weapon_name].fire_weapon()


اجازه دهید بررسی کنیم که این عملکرد چه کاری انجام می دهد:

ابتدا بررسی می کنیم که آیا بازیکن در حال تغییر اسلحه است یا خیر. اگر بازیکن در حال تغییر سلاح است ، ما شلیک نمی خواهیم ، بنابراین return برمیگردانیم.


نکته return تماس فراخوانی بقیه عملکرد را متوقف می کند. در این حالت ، ما یک متغیر را برنمی گردانیم زیرا فقط علاقه مند نیستیم که بقیه کد را اجرا کنیم و همچنین وقتی این تابع را فراخوانی می کنیم بدنبال متغیر برگشتی نیستیم. سپس با فراخوانی عملکرد به fire_weapon سلاح فعلی که بازیکن برای شلیک استفاده می کند ، میگوییم.

نکته یادتان هست که چگونه اشاره کردیم که سرعت انیمیشن ها برای شلیک سریعتر از سایر انیمیشن ها بود؟ با تغییر سرعت انیمیشن شلیک ، می توانید سرعت شلیک گلوله را تغییر دهید! قبل از اینکه آماده شویم تا سلاح های جدید خود را آزمایش کنیم ، هنوز کمی کار برای انجام دادن داریم.


ایجاد برخی از اجسام آزمایشی


با رفتن به پنجره برنامه نویسی ، کلیک روی "file" یک اسکریپت جدید ایجاد کنید. این اسکریپت را RigidBody_hit_test نامگذاری کرده و مطمئن شوید که RigidBody گسترش یافته است. اکنون باید این کد را اضافه کنیم:


extends RigidBody

const BASE_BULLET_BOOST = 9;

func _ready():

pass

func bullet_hit(damage, bullet_global_trans):

var direction_vect = bullet_global_trans.basis.z.normalized() * BASE_BULLET_BOOST

apply_impulse((bullet_global_trans.origin - global_transform.origin).normalized(), direction_vect * damage)


بیایید نحوه کار bullet_hit را بررسی کنیم:

ابتدا بردار جهت دار گلوله را بدست می آوریم. به این ترتیب است که می توان فهمید گلوله از کدام جهت به RigidBody برخورد می کند. ما برای شلیک کردن به RigidBody در همان جهتی که از گلوله استفاده می شود استفاده خواهیم کرد. توجه داشته باشید ما باید بردار جهت را توسط BASE_BULLET_BOOST تقویت کنیم تا گلوله ها کمی بیشتر از یک مشت جمع شوند و گره های RigidBody را به روشی قابل مشاهده حرکت دهیم. اگر در هنگام برخورد گلوله ها با RigidBody ، واکنش کم یا بیشتری می خواهید ، می توانید BASE_BULLET_BOOST را روی مقادیر پایین یا بالاتر تنظیم کنید. سپس ما یک ضربه را با استفاده از apply_impulse اعمال می کنیم. ابتدا باید موقعیت را برای ضربه محاسبه کنیم. از آنجا که apply_impulse یک بردار را نسبت به RigidBody می گیرد ، ما باید فاصله RigidBody تا گلوله را محاسبه کنیم. ما این کار را با کم کردن منشا / موقعیت جهانی RigidBody از مبدا / موقعیت جهانی گلوله انجام می دهیم. این فاصله از RigidBody تا گلوله را به ما می رساند. ما این بردار را عادی می کنیم تا اندازه برخورد کننده تاثیری بر میزان حرکت گلوله ها از RigidBody نداشته باشد. سرانجام ، ما باید نیروی ضربه را محاسبه کنیم. برای این منظور ، از جهتی که گلوله رو به آن است استفاده می کنیم و آن را در آسیب گلوله ضرب می کنیم. این نتیجه خوبی می دهد و برای گلوله های قوی تر ، نتیجه قوی تری می گیریم. اکنون باید این اسکریپت را به همه گره های RigidBody که می خواهیم تحت تأثیر قرار بگیریم ، ضمیمه کنیم. Testing_Area.tscn را باز کرده و تمام مکعب هایی را که در گره Cubes قرار دارند انتخاب کنید.


نکته اگر مکعب بالا را انتخاب کنید ، و سپس Shift را فشار دهید و آخرین مکعب را انتخاب کنید ، Godot تمام مکعب های بین را انتخاب می کند! هنگامی که همه مکعب ها را انتخاب کردید ، در inspectore پایین بروید تا به قسمت "scripts" برسید. روی کشویی کلیک کنید و "load" را انتخاب کنید. اسکریپت جدید ایجاد شده RigidBody_hit_test.gd خود را باز کنید.

یادداشت های نهایی

در قسمت 3 ، ما مهمات را به سلاح ها و همچنین برخی از صداها اضافه خواهیم کرد!حتماً بار دیگر کد را بخوانید! شما می توانید پروژه تمام شده این قسمت را از اینجا بارگیری کنید:

https://docs.godotengine.org/en/stable/_downloads/62938c307343ee71b4fb22939a9a780f/Godot_FPS_Part_2.zip

https://vrgl.ir/KNFOGقسمت اول

شاید از این پست‌ها خوشتان بیاید