Player-ის არქიტექტურა
რატომ მუშაობს Romergo Player ჩვეულებრივ DOM-ზე
ვიზუალური ნოველის ძრავა, ზოგადად, ისეთი რთული არ არის, როგორიც სამგანზომილებიანი შუთერის ან რეალურ დროში სტრატეგიის ძრავა. მას ყოველ წამს არ სჭირდება დიდი სამყაროს ფიზიკის, ასობით ობიექტის ქცევისა და რთული განათების გამოთვლა. თუმცა ეს არ ნიშნავს, რომ მხოლოდ დაწკაპუნებით სურათების შეცვლა საკმარისია.
ვიზუალური ნოველის ჩვეულებრივი სცენა საკმაოდ გასაგები ელემენტებისგან შედგება:
- ერთი ან რამდენიმე ფონი;
- პერსონაჟები და მათი პოზიციები;
- რეპლიკა, მოსაუბრის სახელი და პორტრეტი;
- ვიზუალური ეფექტები და გადასვლები;
- მუსიკა და ხმები;
- არჩევანი ან მოთამაშის სხვა მოქმედება.
მთელი მაგია ამ პარამეტრების დროში გადართვით ჩნდება. მნიშვნელოვანია არა მხოლოდ რა ვაჩვენოთ, არამედ როდის, რა დავტოვოთ უცვლელი, როგორ გავაგრძელოთ მუსიკა სცენებს შორის, რომელი ენა ავირჩიოთ და სად გადავიდეთ მოთამაშის მოქმედების შემდეგ.
დიდხანს ვეძებდი ძრავას და ბოლოს ბრაუზერი ავირჩიე
სცენის აწყობა Godot-ზე, PixiJS-სა და Three.js-ზე ვცადე. სხვადასხვა წარმატებით ყველაფერი მუშაობდა და თავად ძრავები არაფერში იყვნენ დამნაშავე. მაგრამ quest-ების შექმნის ჩემს მეთოდში ისინი დამატებით ფენას ქმნიდნენ, რომელთანაც ცალკე შეთანხმება მჭირდებოდა.
მე მხოლოდ მზა სათამაშო ეკრანი არ მჭირდებოდა. ჩემთვის მნიშვნელოვანი იყო Builder-ში ნებისმიერი ცვლილების მყისიერად ნახვა, მთელი თამაშის აწყობის გარეშე ერთი სცენის გაშვება, ელემენტების პირდაპირ preview-ში მონიშვნა და გადაადგილება, შემდეგ კი იგივე ქცევის მიღება გამოქვეყნებულ Player-ში.
საბოლოოდ ყველაზე ეფექტური ყველაზე მარტივი გადაწყვეტა აღმოჩნდა: ბრაუზერის ჩვეულებრივი DOM.
ფონი, პერსონაჟები და დიალოგი ბრაუზერის ნაცნობ ფენებად რჩება. კომპოზიციისა და ეფექტების უმეტესობა CSS-ით იქმნება. Canvas 2D ცალკეული ვიზუალური ელემენტებისთვის გამოიყენება, ხოლო WebGL canvas მხოლოდ იქ ერთვება, სადაც სურათებს შორის shader-გადასვლები ნამდვილად საჭიროა.
ხმა ბრაუზერის HTMLAudioElement-ის საშუალებით მუშაობს. Runtime აქტიური კლიპებისთვის აუდიოს ქმნის, ხმის დონესა და გამეორებას მართავს და გადასვლისას იმავე მუსიკას ინარჩუნებს, თუ მისი asset არ შეცვლილა.
ეს ნებისმიერი თამაშისთვის უნივერსალური რეცეპტი არ არის. მაგრამ ვიზუალური ნოველისთვის DOM კომპრომისი კი არა, ძალიან ზუსტი ინსტრუმენტი აღმოჩნდა.
მარტივი სცენა და ჭკვიანი runtime
Player-ს წარმოსახვით ორ ნაწილად ვყოფ:
- „სულელი“ სცენა აჩვენებს და უკრავს იმას, რასაც გადასცემენ;
- „ჭკვიანი“ runtime ინახავს ისტორიის მდგომარეობას და წყვეტს, რა უნდა მოხდეს შემდეგ.
სცენამ არ უნდა იცოდეს, რატომ გამოჩნდა კონკრეტული პერსონაჟი, რომელმა პირობამ გახსნა პასუხის ვარიანტი ან რომელი თავი იქნება შემდეგი. ის იღებს კლიპებს, მიმდინარე დროს, ენას, ეკრანის რეჟიმსა და მედიის ბმულებს. შემდეგ შედეგს ასახავს და მოთამაშის მოქმედებებს აბრუნებს.
Runtime იღებს quest-ის აღწერასა და გავლის მდგომარეობას. მან იცის მიმდინარე თავი, კვანძი და სცენა, ცვლადების მნიშვნელობები, გაკეთებული არჩევანი, საკონტროლო წერტილები და დასრულებული თავები. როცა მოთამაშე სცენას დააჭერს ან პასუხს აირჩევს, runtime ეფექტებს იყენებს, შემდეგ ნაბიჯს პოულობს და სცენას კვლავ მზა მდგომარეობას გადასცემს.
თუ ძლიერ გავამარტივებთ, ციკლი ასე გამოიყურება:
Quest JSON + შენახული პროგრესი
↓
runtime განსაზღვრავს მიმდინარე ნაბიჯს
↓
სცენა აჩვენებს კლიპებსა და მედიას
↓
მოთამაშის მოქმედება runtime-ში ბრუნდება
↓
შემდეგი ნაბიჯი — ციკლი ფინალამდე მეორდება
გამოქვეყნებულ თამაშში Player მთელი quest-ის დაფიქსირებულ runtime-სურათს ტვირთავს. Builder-ში preview მიმდინარე მონახაზიდან თავსებად runtime-ს ქმნის. ამიტომ ეს არ არის ორი მსგავსი player, რომლებიც დროთა განმავლობაში განსხვავებულად იწყებენ მუშაობას, არამედ ერთი საერთო RuntimePlayer, SceneStage და viewport სხვადასხვა გარსში.
როგორ ინარჩუნებს სისტემა უწყვეტობას სცენებს შორის
გადასვლისას runtime ახალი სცენის მდგომარეობას თავიდან ითვლის. განმეორებადი მუსიკა asset ID-ით ამოიცნობა და ახალი დაწყებისა და განმეორებითი fade-in-ის გარეშე გრძელდება. სურათებს ბრაუზერი ტვირთავს და cache-ში ინახავს, ამიტომ იგივე ფონის გამოყენება ქსელიდან ფაილის ხელახლა ჩამოტვირთვას არ ნიშნავს.
ანუ ოპტიმიზაცია ერთ დიდ პირობაზე — „არაფერი გადავცეთ“ — არ დგას. ის სწორ დონეებზეა განაწილებული: runtime დაკვრის უწყვეტობას ინარჩუნებს, resolver სტაბილურ რესურსებს აბრუნებს, ხოლო ბრაუზერის cache უკვე ცნობილ მედიას თავიდან აღარ ტვირთავს.
ერთი სცენის ორი ეკრანი
ჩემთვის ყველაზე უსიამოვნო ამოცანა თავად rendering კი არა, ტელეფონზე ადაპტაცია იყო. ჰორიზონტალური სცენის უბრალო შემცირება პერსონაჟებს ზედმეტად პატარას ხდის, დიალოგს ავიწროებს და ფონის მნიშვნელოვან დეტალებს მარტივად ტოვებს კადრს მიღმა.
Romergo-ში სცენას ორი ფიქსირებული ვირტუალური viewport აქვს: ჰორიზონტალური 1280 × 720 და ვერტიკალური 390 × 693. Player მოწყობილობისა და ორიენტაციის მიხედვით შესაბამის რეჟიმს ირჩევს, შემდეგ კი მზა სცენას მთლიანად მასშტაბირებს.
Desktop განლაგება ძირითადად რჩება. ტელეფონისთვის პერსონაჟების ცალკე პოზიციებისა და მასშტაბის დაყენება შეიძლება; თუ override არ არსებობს, დამატებით შემცირებული desktop ვარიანტი გამოიყენება. ამიტომ ავტორი ორ დამოუკიდებელ სცენას კი არ არედაქტირებს, არამედ ერთ სცენას — მიზნობრივი მობილური შესწორებებით.
ფონისთვის bgX, bgY და მასშტაბი ორივე რეჟიმში საერთოა, ვერტიკალური viewport კი იმავე სურათს თავისებურად ჭრის. ამიტომ გამოშვებამდე კადრირება ორივე ფორმატში უნდა შემოწმდეს და საერთო ფოკუსი შეირჩეს. ფონის ცალკე მობილური პარამეტრები runtime-კონტრაქტში არ შედის.
პერსონაჟების ერთგვაროვანი შესწორებები MCP-ის საშუალებით AI აგენტს შეიძლება გადაეცეს, შემდეგ კი ორივე viewport-ის ნამდვილ preview-ში შემოწმდეს. სწორედ ამიტომ ვუწოდებ ადაპტაციას ნახევრად ავტომატურს და არა სრულად ავტომატურს.
როგორ არ ვაჩვენოთ მოთამაშეს შავი ეკრანი
მარტივი სცენა ვერ გვიშველის, თუ საჭირო სურათი ქსელიდან ჯერ არ მოსულა. ამიტომ loading-იც runtime-კონტრაქტის ნაწილი გახდა.
პირველ rendering-მდე Player საწყისი სცენის აქტიურ მედიას აგროვებს და მის ჩატვირთვას ელოდება. ამ დროს მოთამაშე ცარიელი ფონის ნაცვლად ნორმალურ პროგრესის ინდიკატორს ხედავს. გაშვების შემდეგ runtime მცირე დაყოვნებით მიმდინარე სცენისა და გრაფის მომდევნო ორ ნაბიჯში მისაწვდომი სცენების რესურსებს წინასწარ ათბობს. ნაგულისხმევად სიღრმე ორი გადასვლაა წინ, ორივე ნაბიჯზე არსებული ყველა შესაძლო განშტოების ჩათვლით.
წინასწარი ჩატვირთვა გადასვლების გრაფს მიჰყვება და მისი სიღრმე ცალკე შეიძლება შეიცვალოს. აქ სცენების ფიქსირებული რაოდენობა არ გამოიყენება: ხაზოვანი მიმდევრობა და განშტოება განსხვავებულ დატვირთვას ქმნის, მაშინაც კი, როცა წინ ფორმალურად ერთნაირი რაოდენობის ნაბიჯია.
Offline თამაშისთვის სხვა რეჟიმიც არსებობს: მომხმარებელს შეუძლია მთელი quest წინასწარ ჩამოტვირთოს. შემდეგ runtime-სურათი, მედია და PWA გარსი ბრაუზერის cache-ში რჩება და ქსელის გარეშე იხსნება. შემდეგი სცენის წინასწარი ჩატვირთვა გლუვ online გავლას უზრუნველყოფს, სრული ჩამოტვირთვა კი ნამდვილ offline რეჟიმს.
Telegram როგორც გარსი, Discord როგორც ცალკე სისტემა
ბრაუზერების სიყვარული განსაკუთრებით გამოგვადგა Player-ის Telegram-სა და Discord-ში ჩაშენებისას. ორივე შემთხვევაში პლატფორმის შიგნით იგივე web runtime იხსნება, ამიტომ სცენისა და გავლის წესების ახალი სათამაშო ძრავისთვის გადაწერა არ გახდა საჭირო.
Telegram-ს ძირითადად პლატფორმის სესია, quest-ის გაშვება, ლოკალური პროგრესი და Mini App-სა და ჩვეულებრივ Player-ს შორის გადასვლა დასჭირდა. თავად თამაში იგივე დარჩა.
Discord უფრო რთულია, რადგან რამდენიმე ადამიანმა ისტორიის ერთი და იგივე მომენტი უნდა დაინახოს. თითოეულ მონაწილეს ნამდვილად აქვს საკუთარი ლოკალური runtime, მაგრამ host სიმართლის წყაროდ ითვლება. Host-ის Player მდგომარეობას ცვლილებებისას და დაახლოებით ყოველ 750 მილიწამში აგზავნის. Realtime ოთახი ბრძანებას WebSocket-ით იღებს, snapshot-ს ინახავს და მონაწილეებს უგზავნის. მაყურებლების runtime დისტანციურ მდგომარეობას იყენებს, სცენას აღადგენს და სინქრონიზაციებს შორის დროს ლოკალურად აგრძელებს.
ასეთმა დაყოფამ სასარგებლო შედეგი მოგვცა: ისტორიის მდგომარეობა საერთოა, მაგრამ ენა და ეკრანის ზომა ლოკალური რჩება. ერთ მონაწილეს შეუძლია სცენას ტელეფონზე ქართულად უყუროს, მეორეს — დიდ ეკრანზე ინგლისურად, და ორივე host-თან სინქრონულად დარჩება. მაყურებლებს პასუხის ვარიანტებზე ხმის მიცემაც შეუძლიათ ისე, რომ მეორე host არ გახდნენ.
ერთი საფუძველი ყველა რეჟიმისთვის
Player დიდი ხანია გასცდა საბაზისო ნაკრებს — „ფონი, პერსონაჟები და რეპლიკა“. მასში გაჩნდა განშტოებები, ცვლადები, საკონტროლო წერტილები, უკან გადახვევა, ინტერაქტიული ინტერფეისის სცენები, თარგმანები, offline და სინქრონული გავლა.
მაგრამ საბაზისო გადაწყვეტილებამ ზრდას გაუძლო. სცენა კვლავ გამოსახულებასა და ხმას ემსახურება. Runtime კვლავ მდგომარეობასა და გადასვლებს მართავს. Browser API ფარავს rendering-ს, მედიას, cache-სა და ჩაშენებას, სპეციალური ფენები კი მხოლოდ იქ ემატება, სადაც მათ გარეშე ნამდვილად შეუძლებელია.
ამის წყალობით იგივე Player-ის მხარდაჭერა Builder preview-ში, ტესტებში, ჩვეულებრივ ბრაუზერულ თამაშში, Telegram-სა და Discord-ში შეიძლება. ჩემთვის ეს კარგი მაგალითია იმისა, როგორ შეიძლება ყველაზე მარტივი და ოდნავ უხეში გადაწყვეტა ყველაზე მოქნილი აღმოჩნდეს — თუ პასუხისმგებლობის საზღვარს სწორად გავავლებთ.