Panel Logowania

Ekrany wczytywania

napisał : lukaszfito
14
marca
2014
Dowiedz się jak stworzyć ekran wczytywania i zaopatrzyć go w animacje, licznik procentowy lub mini-grę.
tagi : loading screen ekran wczytywania interaktywny statyczny animowany minigra licznik procentowy blender pasek progresu

Wstęp

W tym poradniku dowiemy się jak stworzyć ekran wczytywania. Zapoznamy się z podstawowymi metodami wczytywania i generowania elementów gry. Po czym przestudiujemy 4 rodzaje ekranów wczytywania: statyczny, animowany, procentowy i interaktywny (tworząc minigrę).

edit: Żeby plik "ekran_wczytywania.blend" działał pod Blenderem 2.71, wystarczy do każdej ścieżki w funkcji LibLoad() dodać "//" na początku ścieżki - przykłady: LibLoad( "//"+"stałe\\trawa.blend", "Mesh") lub LibLoad( "//stałe\\trawa.blend", "Mesh")

Schemat

Na pewno sprawdzaliście czasem co znajduje się w folderze docelowym gry: mały plik .exe, pełno pobocznych plików (z dziwnymi rozszerzeniami) i parę folderów (z kolejnymi plikami). W większości przypadków, właśnie taką strukturę będziemy musieli stworzyć, by móc w ogóle myśleć o dodaniu ekranu wczytywania do swojej gry.

W skrócie potrzebujemy pliku rozruchowego gry i plików, które chcemy wczytać. Co można wczytać podczas widocznego ekranu wczytywania? Wszystko! Dźwięki, grafikę, teksty, zmienne, a nawet skrypty. Istnieje wiele metod wczytywania różnego rodzaju kontentu, przedstawię parę sposobów, dających na prawdę duże możliwości, ale najpierw zajrzyjmy jak to wygląda w projekcie poradnika.

Co, gdzie i po co?

Teraz ściągnijmy projekt:

http://troman.pl/files/users/217/ekrany_wczytywania.zip 
http://troman.pl/files/users/217/ekran_wczytywania(2.71).zip 

Folder skompresowany "zip" należy najpierw wypakować, by działał poprawnie. Mamy tutaj plik rozruchowy "ekran_wczytywania.blend", plik tekstowy "plik.txt" (który niedługo nam się przyda), obrazek "ekran wczytywania.jpg" (który pojawi się podczas wczytywania) oraz dwa foldery. Zacznijmy od folderu "stałe", plik "land.blend" to nasza mapa, którą wczytamy (posiada tylko podłoże, bo resztę wczytamy z drugiego folderu), obok znajduje się plik "trawa.blend", dzięki któremu wygenerujemy całą łąkę. W folderze "do wczytania" znajduje się więcej plików: "chmura.blend", "drzewo.blend" i "góra.blend" to obiekty, które wczytujemy na mapę, więc jeśli chcemy mieć więcej drzew, albo chmur, to musimy skopiować te pliki kilkukrotnie - projekt wykryje wszystkie pliki i po kolei będzie je wczytywać (nie musimy zważać na nazwy plików - ale pozostawmy rozszerzenia ".blend"). W folderze znajduje się jeszcze plik "tekst.txt", który zostanie wczytany cały do pamięci podczas wczytywania (również możemy go skopiować - co spowolni troszkę wczytywanie).

Popatrzmy na plik "ekran_wczytywania.blend", mamy w nim 5 scen. Naszym menu jest "wybierz ekran", gdzie możemy wybrać jedną z czterech metod wczytywania (statyczną, animowaną, procentową oraz interaktywną minigrę), na scenie znajdują się 4 przyciski, jedna kamera, światło i tło. Przyciski przy pomocy kostek logiki przenoszą nas na wybraną przez nas scenę, gdzie zaczyna się proces wczytywania.

W późniejszych podpunktach opiszę trochę dokładniej wszystkie pliki i to co się w nich znajduje, teraz jednak skupmy się na wczytywaniu.

Wczytywanie zewnętrznych plików

Wczytywanie tekstu

Zacznijmy od najprostszego - wczytanie tekstu. Wczytywanie plików ".txt" jest bardzo przydatne, gdy gra zawiera dużo tekstów, lub ma więcej wersji językowych. Rzadziej tę metodę stosuje się do wczytywania zmiennych i generowania planszy gry (ale to też jest możliwe). Tej metody warto użyć, gdy chcemy wczytać opcje profilu gracza (rozdzielczość, głośność, poziom trudności..) z pliku konfiguracji.

Ten podpunkt nie jest szczególnie ważny, więc możemy przeskoczyć do Wczytywanie plików ".blend".

Oto nasza sytuacja. Mamy projekt "ekran_wczytywania.blend" i plik tekstowy "plik.txt" zamieszczone w jednym folderze, chcemy wczytać przytoczone zmienne do projektu. Zastosujemy skrypt odporny na wrzucanie przypadkowych spacji i enterów, który rozpoznaje dwa rodzaje zmiennych - tekstu ( string ) i liczb całkowitych ( int ). Wszystkie linijki tekstu bez znaku równości ( "=" ) zostaną potraktowane jako komentarz pliku.

Zawartość "plik.txt" :

Rozdzielczość ekranu:

Szerokość okna= 1280
Wysokość okna =720


Opcje:

Głośność = 70 

Pozostałe opcje:

   Poziom trudności=Prawdziwy twardziel 


Żeby wczytać ten plik musimy skorzystać ze skryptu ( w projekcie znajduje się pod nazwą "metody wczytywania", ale jest tam tylko dla przykładu ).

Skrypt metody wczytywania " (cz 1.) :

# pobieramy z bge tylko to co potrzebne
from bge import logic

# tworzymy dziennik wpisany w logic (dzięki temu będzie działać we wszystkich scenach gry)
logic.dziennik = {}

# otwieramy plik "plik.txt" znajdującego się obok projektu
plik = open("plik.txt", "r")

# wczytujemy zmienne do listy (linijkami)
zmienne = plik.readlines()

# zamykamy plik
plik.close()

# rozpoczynamy pętlę wczytującą zmienne
for i in zmienne:
    
    # sprawdzamy, czy w linijce znajduje się znak rowności
    if "=" in i:
        # dzielimy linijkę w miejscu równości
        j = i.split("=")
        
        # usuwamy białe znaki z początku i końca nazwy zmiennej
        j[1] = j[1].lstrip().rstrip()
        
        # usuwamy białe znaki z początku i końca wartości
        j[0] = j[0].lstrip().rstrip()
        
        # sprawdzamy, czy wartość składa się z samych liczb
        if j[1].isdigit():
            
            # tworzymy zmienną liczbową typu "int"
            logic.dziennik[j[0]] = int(j[1])
            
        # jeśli wartość nie składa się z samych liczb
        else:
            
            # tworzymy zmienną typu "string"
            logic.dziennik[j[0]] = j[1]

# wypisujemy cały dziennik
print(logic.dziennik)

# wypisujemy zmienną "Głośność" z dziennika
print(logic.dziennik["Głośność"])

Linijki:
43. Wypisujemy wczytane zmienne, otrzymujemy informację: {'Szerokość okna': 1280, 'Głośność': 70, 'Wysokość okna': 720, 'Poziom trudności': 'Prawdziwy twardziel'}
46. Wypisujemy zmienną "Głośność" (pamiętajmy o dużych literach), otrzymujemy informację: 70

W taki sposób wczytaliśmy tekst linijkami, dzieląc je w miejscu znaku równości i wczytaliśmy je do dziennika działającego we wszystkich scenach gry. O wiele bardziej przydatna jest kolejna metoda, którą daje nam sam Blender.

Wczytywanie plików ".blend"

LibLoad() - dobre opanowanie tej komendy pozwoli wyeliminować skrypty do minimum, bo pliki te mogą zawierać większość rzeczy potrzebnych do gry, dlatego warto przestudiować ją dokładniej i poszukać o niej więcej informacji w Internecie.

Teraz nasza sytuacja wygląda następująco. Mamy projekt "ekran_wczytywania.blend" i plik gry "drzewo.blend" zamieszczony w folderze "do wczytania" (i na razie nie ma znaczenia co w nim jest, bo chcemy go wczytać w całości). Wczytamy też drugi plik z innego folderu, ale o tym za chwilę. Do wczytania wykorzystamy funkcję LibLoad(), której ważniejszymi parametrami są:

  • pierwszy - gdzie podajemy ścieżkę z nazwą pliku
  • drugi - gdzie podajemy rodzaj wczytywania:
    • "Scene" - wczytuje całą scenę, przy czym:
      • pomijane są animacje, możemy je włączyć przy pomocy parametru "load_actions=True
      • standardowo wczytywane są skrypty sceny, możemy je wyłączyć dając parametr "load_scripts=False" (w poniższym skrypcie jest "True", by upewnić się, że skrypty zostaną wczytane, ale możemy pominąć ten parametr)
      • istnieje także parametr "async=True", standardowo jest wyłączony i powoduje przeniesienie procesu wczytywania do oddzielnego wątku (co powinno usuwać przycinanie się gry podczas wczytywania), ale ten parametr nie działa zbyt dobrze, gdyż po wczytaniu pliku, następuje nagłe renderowanie całości, co skutkuje zacięciem gry (czyli temu, czemu miał zapobiegać) - dlatego zostawiamy ten parametr w spokoju
    • "Mesh" - wczytuje modele siatki mesh do niewidocznej warstwy (wspominałem o nim w poradniku do LoD'ów)

Skrypt metody wczytywania " (cz 2.):

# pobieramy z logic tylko to co potrzebne
from bge.logic import LibLoad, LibList

# wczytujemy scenę z pliku "drzewo.blend" umieszczonego w folderze "do wczytania" znajdującego się obok projektu
LibLoad("do wczytania\\drzewo.blend", "Scene", load_actions=True, load_scripts=True)

# wczytujemy modele siatki z pliku "trawa.blend" umieszczonego w folderze "stałe" znajdującego się obok projektu
LibLoad("stałe\\trawa.blend", "Mesh")

# wypisujemy wczytane pliki .blend
print(LibList())

Linijki:
11. Wypisujemy wczytane pliki, otrzymujemy informację: ['do wczytania\\drzewo.blend', 'stałe\\trawa.blend']

W powyższym przykładzie wczytaliśmy aktywną scenę "drzewo.blend" (wraz z animacjami i skryptami), oraz modele siatki z pliku "trawa.blend".

Inne metody wczytywania

Biblioteka aud jest standardową biblioteką w Blenderze, przydaje się do wczytywania dźwięków z plików (jednak kostki logiki radzą sobie równie dobrze z wczytywaniem dźwięku i nie potrzebują pisania skryptów).

Wczytywanie własnych bibliotek (modułów), czyli plików ze skryptami. Przykładowo, gdy chcemy wczytać plik "skrypt.py" z folderu "folder" zamieszczonego obok projektu - wpisujemy linijkę:

import folder.skrypt

Powielanie i generowanie: addObject()

Szczególną metodą wczytywania jest generowanie obiektów. W poradniku wygenerujemy sobie całą łąkę, przy pomocy jednego obiektu źdźbła trawy (plik "trawa.blend" w folderze "stałe"). Każde źdźbło pojawi się na scenie w tym samym miejscu, dlatego obiekt musi mieć skrypt losujący jego położenie.

Skrypt " losowa pozycja " :

# pobieramy logic z bge
from bge import logic

# pobieramy bibliotekę randomizacji
import random

# ustalamy właściciela - owner
owner = logic.getCurrentController().owner

# losujemy zmienną odległości
rand=random.random() * 33

# ustalamy pozycję obiektu (mapa jest pochyła)
owner.position=[random.random() * 22 - 11, rand - 8, rand / 11]

# ustalamy zmienną skalowania
sc = 1.0 - rand / 33

# skalujemy obiekt
owner.scaling = [sc, sc, sc]

# obracamy losowo obiekt
owner.applyRotation([0, 0, random.random() * 360], False)

Linijki:
11. Otrzymujemy liczbę typu "float" od 0 do 33
14. "position" przyjmuje tablicę "[x, y, z]", gdzie: "x" - to przesunięcie na boki, "y" - odległość od kamery, "z" - przesunięcie góra/dół (mapa jest pochyła, więc im dalej, tym wyżej powinien znajdować się obiekt)
17/20. Dla efektu głębi zmniejszamy obiekty położone dalej

Nie tylko trawa została zaopatrzona w losowanie pozycji, dla naszej wygody jest ona zamieszczona w drzewach, górach i chmurach (każdy obiekt ma troszkę inne parametry - chmury są wyżej, góry są dalej). Skrypt powinien zadziałać raz na grę, dlatego łączymy go na kostkach logiki w sposób pokazany poniżej.

Żeby wczytać wielokrotnie jeden obiekt trawy, musi znajdować się on na niewidocznej scenie, wymaga tego funkcja addObject(). Musimy też podać miejsce w jakim ma pojawić się nasz obiekt (wpisując nazwę jednego z obiektów sceny), użyjemy do tego przycisku "powrót", gdyż znajduje się na wszystkich scenach.

Część skryptu " statyczny " :

# pobieramy logic z bge
from bge import logic
# (...)
# pobieramy obiekt trawy z pliku "trawa.blend" zamieszczonego w folderze "stałe" logic.LibLoad("stałe\\trawa.blend", "Scene") # rozpoczynamy pętlę generującą łąkę for i in range(100): # dodajemy obiekt "trawa" do sceny przyjmując pozycję początkową z obiektu "powrót" (mało ważne jaki obiekt podamy, gdyż trawa po wczytaniu wybierze sobie losową pozycję na mapie) logic.getCurrentScene().addObject("trawa", "powrót")

# (...)

Linijki:
10. Generujemy łąkę o ilości 100 źdźbeł trawy
4/15. Te miejsca kodu tymczasowo pominęliśmy

Warto zauważyć, że zwiększając liczbę stu do tysiąca, łąka będzie gęstsza, ale jej generowanie zajmie 10 razy więcej czasu.

Jeśli na razie się gubicie w całym tym wczytywaniu, to już przechodzimy do przykładu w pełni działającego ekranu wczytywania i na pewno wszystko stanie się bardziej zrozumiałe.

Statyczny ekran wczytywania

Jak to wygląda? Zwykła bitmapa z napisem "wczytywanie" (często z dodanym wielokropkiem). To najprostszy z ekranów wczytywania, który daje nam dość dużą swobodę (gdyż możemy wczytać cały poziom gry z jednego pliku - jednak my wczytamy ich kilka, gdyż później bardzo nam się to przyda).

Scena ekranu znajduje się oczywiście w pliku "ekran_wczytywania.blend" i nazywa się "statyczny", posiada (od góry): kamerę ekranu wczytywania, płaszczyznę z bitmapą (z napisem "wczytywanie..."), światło (sun), przycisk "powrót" oraz kamerę gry. Oczywiście ekran wczytywania z kamerą umieszczono gdzieś, gdzie nie przeszkadza reszcie (gdzieś wysoko). Najpierw widzimy scenę z perspektywy kamery ekranu wczytywania i patrzymy na ładną bitmapę. W tle będzie działał skrypt wczytujący, gdy skończy wczytywać obiekty gry, zmieni aktywną kamerę na kamerę gry, po czym usunie już niepotrzebną kamerę i bitmapę wczytywania. Przycisk "powrót" zaopatrzony jest w specjalny skrypt, którym zajmiemy się w kolejnym podpunkcie. Ale jak będzie wyglądał skrypt wczytywania? Jak wyglądać powinien? Musimy "pokazać palcem" Blenderowi, które pliki ma wczytać i możemy to zrobić w pętli, lub manualnie wypisywać kolejno komendy LibLoad() - my wypróbujemy jedną i drugą możliwość. Z folderu "stałe" wczytamy manualnie a z folderu "do wczytania" przy pomocy pętli. Skrypt powinien wykonać się jednokrotnie, więc w kostkach logiki nie zaznaczamy "True".

Skrypt " statyczny " :

# pobieramy logic z bge
from bge import logic

# ustalamy właściciela - owner
owner=logic.getCurrentController().owner

# pobieramy mapę poziomu
logic.LibLoad("stałe\\land.blend", "Scene")

# pobieramy bibliotekę obsługi systemów operacyjnych
import os

# ustalamy folder, w którym znajdują się pliki do wczytania
sciezka = "do wczytania\\"

# pobieramy zawartość folderu "do wczytania"
nazwy_plikow = os.listdir(sciezka)

# tworzymy dziennik dla plików tekstowych
logic.dziennik = {}

# rozpoczynamy pętlę pobierającą wszystkie pliki z folderu "do wczytania"
for nazwa in nazwy_plikow:
    
    # jeśli plik ma rozszerzenie ".txt"
    if ".txt" in nazwa:
        
        # otwieramy plik
        plik = open(sciezka+nazwa, 'r')
        
        # wczytujemy cały plik do dziennika (ucinając rozszerzenie ".txt" z nazwy - cztery ostatnie znaki)
        logic.dziennik[nazwa[:-4]] = plik.read()
        
        # zamykamy plik
        plik.close()
        
    # jeśli plik ma rozszerzenie ".blend"
    elif ".blend" in nazwa:
        
        # wczytujemy plik jako scenę (razem z animacjami i skryptami)
        logic.LibLoad(sciezka+nazwa, "Scene", load_actions=True, load_scripts=True)

# pobieramy obiekt trawy z pliku "trawa.blend" zamieszczonego w folderze "stałe"
logic.LibLoad("stałe\\trawa.blend", "Scene")

# rozpoczynamy pętlę generującą łąkę
for i in range(100):
    
    # dodajemy obiekt "trawa" do sceny przyjmując pozycję początkową z obiektu "powrót" (mało ważne jaki obiekt podamy, gdyż trawa po wczytaniu wybierze sobie losową pozycję na mapie)
    logic.getCurrentScene().addObject("trawa", "powrót")
    
# pobieramy listę obiektów sceny
object=logic.getCurrentScene().objects

# usuwamy ekran wczytywania
logic.getCurrentScene().objects["ekran statycznego wczytywania"].endObject()

# zmieniamy kamerę wczytywania na kamerę gry
logic.getCurrentScene().active_camera=object["kamera gry (statyczny)"]

# usuwamy kamerę wczytywania
object["kamera wczytywania"].endObject()

Linijki:
8/44. Tutaj podajemy manualnie całą ścieżkę pliku do wczytania
11-17. Dzięki tej metodzie pobieramy nazwy plików, które chcemy pobrać (dzięki temu mogliśmy wcześniej skopiować pliki ".blend" w folderze "do wczytania" i nie bawić się skryptami, by je wczytać)
23. Pętla wczyta po kolei wszystkie pliki z folderu "do wczytania"
26-35. Pobieramy cały plik tekstowy nie dzieląc go na zmienne, żeby nie wydłużać poradnika
44-50. To nasz wcześniejszy generator łąki
53-62. Usuwamy niepotrzebne obiekty

Po wykonaniu tego skryptu, na scenie pojawia się całkiem ciekawy pejzaż. Jednak teraz ważniejszą rzeczą jest usuwanie z pamięci tego co wczytaliśmy, żeby nie zaśmiecać RAMu i by wszystko było stabilne - oczywiście jak dostatecznie napatrzymy się na las, łączkę i chmurki.

Zwalnianie zawartości

Przechodząc z poziomu na poziom w grze, najpierw warto zwolnić pamięć, by kolejne wczytywanie trwało krócej, a gra działała płynnie.

Skrypt " usuwanie " :

# pobieramy logic z bge
from bge import logic

# pobieramy sensory przycisku powrotu
sensors = logic.getCurrentController().owner.sensors

# jeśli klikniemy "lewy" przycisk myszy i myszka znajduje się "nad" przyciskiem powrotu
if sensors["lewy"].status == 1 and sensors["nad"].status == 2:
    
    # usuwamy dziennik z pamięci
    del logic.dziennik
    
    # rozpoczynamy pętlę dla każdej wczytanej sceny
    for lib in logic.LibList():
        
        # usuwamy wczytane sceny z pamięci
        logic.LibFree(lib)

Linijki:
11. Oczywiście kiedy chcemy zostawić dane z dziennika dla innej sceny, musimy usunąć tę linijkę
14. LibLis() powinniśmy znać z podpunktu Wczytywanie plików ".blend"

Jeśli przy pomocy innych metod wczytaliśmy jakieś dane, które nie są potrzebne, powinniśmy o nich pamiętać (i je usunąć). Musimy szczególnie uważać na funkcję LibFree(). Dlaczego? Ta komenda usuwa wczytany kontent w tle i nie daje znać kiedy kończy. Jeśli wczytamy poziom, wyjdziemy z niego i spróbujemy wczytać go ponownie (robiąc to na prawdę szybko), to gra może wczytywać i usuwać jednocześnie ten sam obiekt - wynika z tego cała masa błędów i opiszę jak sobie z nimi radzić w podpunkcie Pułapki.

Zasada wymiany

By przejść do bardziej zaawansowanej metody z animacją, musimy zrozumieć działanie Blendera. BGE czeka na zakończenie skryptu i dopiero wtedy zajmuje się całą resztą (animacją, fizyką oraz innymi skryptami). Żeby animacja zadziałała musimy uwolnić Blendera od czekania na zakończenie skryptu, dlatego stworzymy "prawie asynchroniczny skrypt". To nie jest takie trudne jak się wydaje, zasada jest prosta. Skrypt i BGE nie mogą działać jednocześnie, tak jak dzieci chcące grać na jednym komputerze, ale mogą się nim podzielić. Blender potrzebuje określonej liczby ruchów, by przejść przez całą animację, skrypt wczytujący także ładuje określoną ilość obiektów - mogą to robić wymieniając się co krok.

Jak to będzie wyglądać? Kolejno:

  • Skrypt sprawdzi zawartość folderu "do wczytania", wczyta źdźbło trawy, ustali gęstość łączki, wczyta mapę, ominie resztę linijek i odda władzę BGE
  • Skrypt wczyta pierwszy plik z folderu "do wczytania", ominie resztę linijek i odda władzę BGE
  • Skrypt wczyta drugi plik z folderu "do wczytania", ominie resztę linijek i odda władzę BGE
  • Skrypt wczyta kolejny plik i odda władzę BGE
  • ...
  • Gdy pliki się skończą, wygeneruje jedno źdźbło trawy, ominie resztę linijek i odda władzę BGE
  • Skrypt wygeneruje kolejne źdźbło trawy, ominie resztę linijek i odda władzę BGE
  • ...
  • Gdy skrypt wygeneruje odpowiednią ilość trawy, będzie czekał aż animacja się skończy, po czym usunie wszystkie obiekty ekranu wczytywania

W każdym momencie, gdy skrypt oddaje władzę BGE, animacja będzie odświeżana - Blender ma wtedy pełną władzę, właśnie to pozwoli na odświeżania paska progresu i działanie minigry w kolejnych podpunktach.

Animowany ekran wczytywania

Scena z animowanym ekranem wczytywania niewiele różni się od sceny ekranu statycznego. Znajduje się w niej kula z animacją obrotu i nie ma napisu "wczytywanie...".

Jednak by stworzyć animację działającą podczas wczytywania nie wystarczy dodać animowany obiekt przed kamerą ekranu. Musimy wykorzystać wcześniej wytłumaczoną zasadę.

Skrypt " animacja " :

# pobieramy logic z bge
from bge import logic

# ustalamy właściciela - owner
owner = logic.getCurrentController().owner

# ustalamy folder, w którym znajdują się pliki do wczytania
sciezka = "do wczytania\\"

# jeśli właściciel nie ma zmiennej "wykonaj tylko raz"
if not "wykonaj tylko raz" in owner.getPropertyNames():
    
    # tworzymy zmienną "wykonaj tylko raz"
    owner["wykonaj tylko raz"]="wykonano"
    
    # pobieramy mapę poziomu
    logic.LibLoad("stałe\\land.blend","Scene")
    
    # pobieramy obiekt trawy z pliku "trawa.blend" zamieszczonego w folderze "stałe"
    logic.LibLoad("stałe\\trawa.blend", "Scene")
    
    # pobieramy bibliotekę obsługi systemow operacyjnych
    import os
    
    # pobieramy zawartość folderu "do wczytania"
    owner["nazwy_plikow"] = os.listdir(sciezka)
    
    # ustalamy ile trawy się wygeneruje
    owner["wygeneruj"] = 100
    
    # tworzymy dziennik wpisany w logic (dzięki temu będzie działać we wszystkich scenach gry)
    logic.dziennik={}
    
# jeśli istnieją jakieś nazwy plików do wczytania
elif owner["nazwy_plikow"] != []:
    
    # odcinamy ostatnią nazwę pliku i przyrównujemy ją do zmiennej "pop"
    pop = owner["nazwy_plikow"].pop()
    
    # jeśli nazwa pliku ma rozszerzenie ".txt"
    if ".txt" in pop:
        
        # otwieramy plik
        plik = open(sciezka+pop, 'r')
        
        # wczytujemy cały plik do dziennika (ucinając rozszerzenie ".txt" z nazwy - cztery ostatnie znaki)
        logic.dziennik[pop[:-4]] = plik.read()
        
        # zamykamy plik
        plik.close()
    
    # jeśli nazwa pliku ma rozszerzenie ".blend"
    elif ".blend" in pop:
        
        # wczytujemy plik jako scenę (razem z animacjami i skryptami)
        logic.LibLoad(sciezka+pop, "Scene", load_actions=True, load_scripts=True)

# jeśli mamy coś wygenerować
elif owner["wygeneruj"] > 0:
    
    # dodajemy obiekt "trawa" do sceny przyjmując pozycję początkową z obiektu "powrót" (mało ważne jaki obiekt podamy, gdyż trawa po wczytaniu wybierze sobie losową pozycję na mapie)
    logic.getCurrentScene().addObject("trawa", "powrót")
    
    # odejmujemy 1 od liczby wygenerowanych obiektów
    owner["wygeneruj"] -= 1

# jeśli nie ma nic do wczytania lub wygenerowania
else:
    
    # pobieramy listę obiektów sceny
    object = logic.getCurrentScene().objects
    
    # jeśli animacja jest na końcu (gdy jest zapętlona, musi też dojść do ostatniej klatki)
    if object["animowany obiekt"].getActionFrame() == 60:
        
        # usuwamy ekran wczytywania
        object["ekran animowanego wczytywania"].endObject()
        
        # usuwamy animowany obiekt
        object["animowany obiekt"].endObject()
        
        # zmieniamy kamerę wczytywania na kamerę gry
        logic.getCurrentScene().active_camera = object["kamera gry (animacja)"]
        
        # usuwamy kamerę wczytywania
        object["kamera wczytywania"].endObject()

Linijki:
10-32. Te linijki wykonają się tylko raz na scenę
29. Tutaj możemy zwiększyć gęstość łąki
35-56. To inaczej napisana pętla wczytująca obiekty z folderu "do wczytania", wykonuje tylko jeden krok (na klatkę logiki)
59-65. A to inaczej napisana pętla generowania łąki - zadziała, gdy wcześniejsza pętla wczyta wszystkie pliki

74. Ta linijka powoduje, że ekran wczytywania znika tylko, gdy animacja jest na końcu, więc jeśli zapętliliśmy animacje, to musi ona dojść do ostatniej klatki - możemy usunąć tego "ifa", wtedy ekran zniknie nie czekając na koniec animacji

Skrypt powinien działać cały czas (i znajdować się w obiekcie, który zostanie usunięty po wczytaniu wszystkich elementów gry), tak jak na obrazku poniżej. Animacje możemy mieć dowolną, złożoną z wielu klatek i obiektów (jeśli dodamy linijki usuwające je, na końcu skryptu " animacja ").

Zauważmy, że gdybyśmy wczytali do gry przykładowo mapę, gracza i jego przeciwnika, to podczas wczytywania BGE działa, więc skrypty gracza i przeciwnika działają. Co oznacza, że przeciwnik może zabić gracza podczas wczytywania. To niekoniecznie minus, właśnie przez to, że skrypty obiektów działają, nasz pejzaż jest gotowy od razu po usunięciu ekranu wczytywania. Opiszę to dokładniej w podpunkcie Pułapki, tak jak problem z renderowaniem animacji na silniku gry, podczas wczytywania.

Procentowy ekran wczytywania ( z tekstem i paskiem progresu )

Ten ekran wczytywania podlega dokładnie tej samej zasadzie wymiany, dlatego skrypty ekranu animacji i ekranu procentowego są prawie identyczne. Nie ma tu animowanego obiektu, jest za to pasek progresu i obiekt tekstu (na którym będziemy wypisywać wczytany procent). Jeśli przykładowo będziemy chcieli wczytywać 100 obiektów, to jeden procent będzie odpowiadał jednemu wczytanemu obiektowi, więc procent nie pokazuje wczytanych megabajtów, tylko ilość obiektów do wczytania (i wygenerowania). Dlatego powinniśmy dzielić gry na jak najwięcej części, w przeciwnym wypadku, gdybyśmy mięli do wczytania tylko mapę i gracza, progres będziecie trzy krokowy (0%, 50%, 100%) a pierwszy krok będzie znacznie dłuższy, bo mapa prawdopodobnie zajmować będzie kilkanaście razy więcej miejsca niż obiekt gracza. Nie tylko my korzystamy z takiego uproszczenia, gdyby inne gry wyświetlały ilość wczytanych danych w megabajtach, to pasek progresu zapełniałby się jednostajnie, a wcale tak nie jest.

Jak już poruszyliśmy temat paska, warto pokazać jak prosto stworzyć pasek progresu. Jeśli stworzymy prostokąt, możemy otworzyć Edit Mode i przesunąć go w prawo, by punkt środka znajdował się na lewej krawędzi prostokąta. Poniższy obrazek pokazuje to. Ale co nam to daje? Zmieniając skalę "x" (zaznaczoną czerwonym prostokątem) od 0.0 do 1.0 przedstawiamy progres wczytywania, gdzie: 0.2 oznacza 20%, 0.5 oznacza 50% a 1.0 oznacza 100%. Wykorzystamy to w skrypcie.

Skrypt " procentowy " :

# pobieramy logic z bge
from bge import logic

# ustalamy właściciela - owner
owner=logic.getCurrentController().owner

# ustalamy folder, w którym znajdują się pliki do wczytania
sciezka = "do wczytania\\"

# jeśli właściciel nie ma zmiennej "wykonaj tylko raz"
if not "wykonaj tylko raz" in owner.getPropertyNames():
    
    # tworzymy zmienną "wykonaj tylko raz"
    owner["wykonaj tylko raz"]="wykonano"
    
    # pobieramy mapę poziomu
    logic.LibLoad("stałe\\land.blend", "Scene")
    
    # pobieramy obiekt trawy z pliku "trawa.blend" zamieszczonego w folderze "stałe"
    logic.LibLoad("stałe\\trawa.blend", "Scene")
    
    # pobieramy bibliotekę obsługi systemów operacyjnych
    import os
    
    # pobieramy zawartość folderu "do wczytania"
    owner["nazwy_plikow"] = os.listdir(sciezka)
    
    # ustalamy ile trawy się wygeneruje
    owner["wygeneruj"] = 100
    
    # sumujemy ilość elementów (czyli naszą wartość 100%)
    owner["elementow"] = len(owner["nazwy_plikow"]) + owner["wygeneruj"]
    
    # tworzymy zmienną, która mówi nam ile plików wczytaliśmy (i ile trawy wygenerowaliśmy)
    owner["wczytane"] = 0
    
    # tworzymy dziennik wpisany w logic (dzięki temu będzie działać we wszystkich scenach gry)
    logic.dziennik = {}

# jeśli istnieją jakieś nazwy plików do wczytania
elif owner["nazwy_plikow"] != []:
    
    # odcinamy ostatnią nazwę pliku i przyrównujemy ją do zmiennej "pop"
    pop = owner["nazwy_plikow"].pop()
    
    # jeśli nazwa pliku ma rozszerzenie ".txt"
    if ".txt" in pop:
        
        # otwieramy plik
        plik = open(sciezka+pop, 'r')
        
        # wczytujemy cały plik do dziennika (ucinając rozszerzenie ".txt" z nazwy - cztery ostatnie znaki)
        logic.dziennik[pop[:-4]] = plik.read()
        
        # zamykamy plik
        plik.close()
        
    # jeśli nazwa pliku ma rozszerzenie ".blend"
    elif ".blend" in pop:
        
        # wczytujemy plik jako scenę (razem z animacjami i skryptami)
        logic.LibLoad(sciezka+pop, "Scene", load_actions=True, load_scripts=True)
    
    # dodajemy 1 od liczby wczytanych (i wygenerowanych) obiektów
    owner["wczytane"] += 1
    
# jeśli mamy coś wygenerować
elif owner["wygeneruj"] > 0:
    
    # dodajemy obiekt "trawa" do sceny przyjmując pozycję początkową z obiektu "powrót" (mało ważne jaki obiekt podamy, gdyż trawa po wczytaniu wybierze sobie losową pozycję na mapie)
    logic.getCurrentScene().addObject("trawa","powrót")
    
    # dodajemy 1 od liczby wczytanych (i wygenerowanych) obiektów
    owner["wczytane"] += 1
    
    # odejmujemy 1 od liczby wygenerowanych obiektów
    owner["wygeneruj"] -= 1


# jeśli nie ma nic do wczytania lub wygenerowania
else:
    
    # pobieramy listę obiektów sceny
    object=logic.getCurrentScene().objects
    
    # usuwamy ekran wczytywania
    object["ekran procentowego wczytywania"].endObject()
    
    # usuwamy tekstowy obiekt progresu
    object["procentowe"].endObject()
    
    # usuwamy obiekt paska progresu
    object["pasek"].endObject()
    
    # zmieniamy kamerę wczytywania na kamerę gry
    logic.getCurrentScene().active_camera = object["kamera gry (procentowy)"]
    
    # usuwamy kamerę wczytywania
    object["kamera wczytywania"].endObject()
    
# wyliczamy procent wczytanych elementów
procent = owner["wczytane"] / owner["elementow"]

# uaktualniamy tekst na ekranie
owner.text = "WCZYTANO: " + str(int(procent * 100)) + " %"

# rozszerzamy pasek progresu
logic.getCurrentScene().objects["pasek"].scaling = [procent, 1, 1]

Linijki:
32. Dodajemy do siebie ilość plików w folderze i ilość trawy do wygenerowania, to właśnie nasze 100%
65/74. Dodawanie 1 do licznika, co jest potrzebne do wyliczenia progresu
102-108. Po każdym przejściu skryptu aktualizujemy tekst i pasek progresu

Choć tutaj zaznaczałem, że najlepiej podzielić grę na jak najwięcej plików, to dopiero w kolejnym podpunkcie staje się to szczególnie ważne.

Interaktywny ( minigra )

Doszliśmy do najciekawszego momentu poradnika - minigra! Zasada wymiany ciągle obowiązuje a skrypt jest prawie identyczny ze skryptem " animacja ". Najpierw jednak zastanówmy się jak minigra powinna wyglądać, bo to jest najtrudniejsze. Wczytywanie gry zazwyczaj trwa tak krótko, że nie da się przedstawić graczowi ani zasad minigry, ani ustawień sterowania. Minigra powinna być intuicyjna i mało skomplikowana, ale nie musi dążyć do czegoś konkretnego - zwykły żółw podążający za myszką też skutecznie odwróci uwagę gracza. W naszym projekcie mamy zwykle zbieranie pieniążków do koszyka.

Na scenie "minigra" znajdują się trzy pieniążki, koszyk, licznik punktów i przyciemnione tło ekranu wczytywania (oczywiście są tam też dwie kamery, przycisk powrotu i światło).

Zaczniemy od wyniku, w Properties logiki typ tekstu ustawiliśmy na Integer (zaznaczone czerwonym prostokątem), dzięki czemu możemy operować na obiekcie jak na zmiennej liczbowej (dodawać, odejmować..). Za każdym razem, gdy pieniążek dotknie koszyka, dodamy punkt.

Pieniążki i koszyk mają własne skrypty, dzięki którym gra działa (pieniążki opadają a koszyk podąża za myszką). Te skrypty to " pieniążek " oraz " koszyk ".

Skrypt " pieniążek " :

# pobieramy logic z bge
from bge import logic

# ustalamy właściciela - owner
owner = logic.getCurrentController().owner

# pobieramy sensor "Collision"
collision = owner.sensors["Collision"]

# przesuwamy pieniążek (symulujemy spadanie)
owner.applyMovement([0, -0.3, 0], False)

# jeśli pieniążek znajduje się dostatecznie nisko (poza kamerą)
if owner.position[1] < -4 :
    
    # pobieramy bibliotekę randomizacji
    import random
    
    # losujemy pozycję pojawienia się pieniążka
    owner.position = [random.random() * 13 - 6.5, 4, 16.0]
    
# jeśli pieniążek dotknie koszyka
if collision.status == 1:
    
    # dodajemy 1 punkt do wyniku
    logic.getCurrentScene().objects["wynik"]["Text"] += 1
    
    # pobieramy bibliotekę randomizacji
    import random
    
    # losujemy pozycję pojawienia się pieniążka
    owner.position = [random.random() * 13 - 6.5, 4, 16.0]

 Skrypt " koszyk" :

# pobieramy z bge tylko to co potrzebne
from bge import logic,render

# ustalamy właściciela - owner
owner = logic.getCurrentController().owner

# pobieramy sensor "Movement"
mouse = owner.sensors["Movement"]

# pobieramy pozycję myszy z sensora
w=render.getWindowWidth()

# jeśli mysz poruszyła się
if mouse.status != 0:
    
    # przesuwamy koszyk, by podążał za myszką
    owner.position = [mouse.position[0] / w * 16 - 8, -3.24176, 16.0]

 Oba skrypty mamy ustawione w logice obiektów.

Żeby wykrywanie materiału koszyka działało poprawnie, trzeba ustawić odpowiednio fizykę (oraz włączyć fizykę na materiale).

Skoro skończyliśmy już oglądać obrazki, możemy przejść do skryptu wczytywania.

Skrypt " minigra" :

# pobieramy logic z bge
from bge import logic

# ustalamy właściciela - owner
owner = logic.getCurrentController().owner


# ustalamy folder, w którym znajdują się pliki do wczytania
sciezka = "do wczytania\\"

# jeśli właściciel nie ma zmiennej "wykonaj tylko raz"
if not "wykonaj tylko raz" in owner.getPropertyNames():
    
    # tworzymy zmienną "wykonaj tylko raz"
    owner["wykonaj tylko raz"] = "wykonano"
    
    # pobieramy mapę poziomu
    logic.LibLoad("stałe\\land.blend", "Scene")
    
    # pobieramy obiekt trawy z pliku "trawa.blend" zamieszczonego w folderze "stałe"
    logic.LibLoad("stałe\\trawa.blend", "Scene")
    
    # pobieramy bibliotekę obsługi systemów operacyjnych
    import os
    
    # pobieramy zawartość folderu "do wczytania"
    owner["nazwy_plikow"] = os.listdir(sciezka)
    
    # ustalamy ile trawy się wygeneruje
    owner["wygeneruj"] = 100
    
    # tworzymy dziennik wpisany w logic (dzięki temu będzie działać we wszystkich scenach gry)
    logic.dziennik = {}

# jeśli istnieją jakieś nazwy plików do wczytania
elif owner["nazwy_plikow"] != []:
    
    # odcinamy ostatnią nazwę pliku i przyrownojemy ją do zmiennej "pop"
    pop = owner["nazwy_plikow"].pop()
    
    # jeśli nazwa pliku ma rozszerzenie ".txt"
    if ".txt" in pop:
        
        # otwieramy plik
        plik = open(sciezka+pop, 'r')
        
        # wczytujemy cały plik do dziennika (ucinając rozszerzenie ".txt" z nazwy - cztery ostatnie znaki)
        logic.dziennik[pop[:-4]] = plik.read()
        
        # zamykamy plik
        plik.close()
    
    
    # jeśli nazwa pliku ma rozszerzenie ".blend"
    elif ".blend" in pop:
        
        # wczytujemy plik jako scenę (razem z animacjami i skryptami)
        logic.LibLoad(sciezka + pop, "Scene", load_actions=True, load_scripts=True)

# jeśli mamy coś wygenerować
elif owner["wygeneruj"] > 0:
    
    # dodajemy obiekt "trawa" do sceny przyjmując pozycję początkową z obiektu "powrót" (mało ważne jaki obiekt podamy, gdyż trawa po wczytaniu wybierze sobie losową pozycję na mapie)
    logic.getCurrentScene().addObject("trawa", "powrót")
    
    # odejmujemy 1 od liczby wygenerowanych obiektów
    owner["wygeneruj"] -= 1

# jeśli nie ma nic do wczytania lub wygenerowania
else:
    
    # pobieramy listę obiektów sceny
    object = logic.getCurrentScene().objects
    
    # rozpoczynamy pętlę szukającą pieniędzy (pieniążków)
    for ob in object:
        
        # jeśli w nazwie obiektu znajduje się słowo "pieniążek"
        if "pieniążek" in ob.name:
            
            # usuwamy obiekt
            ob.endObject()
        
    # usuwamy koszyk
    object["koszyk"].endObject()
    
    #usuwamy wynik
    object["wynik"].endObject()
    
    # usuwamy ekran wczytywania
    object["ekran minigry"].endObject()
    
    # zmieniamy kamerę wczytywania na kamerę gry
    logic.getCurrentScene().active_camera = object["kamera gry (minigra)"]
    
    # usuwamy kamerę wczytywania
    object["kamera wczytywania"].endObject()

Linijki:
76-82. Oto największa nowość skryptu - usuwanie pieniążków w pętli

Poruszymy teraz szczególnie istotną kwestię - rozmiar plików. Minigra jest płynna tylko wtedy, gdy nie wczytujemy dużych plików (dlatego szalenie ważne jest dzielenie gry na mniejsze części). Za każdym przejściem kodu, BGE czeka na wczytanie elementu i w tym momencie nie zajmuje się minigrą, więc jeśli mamy pełno małych plików i jeden wielki plik mapy, to spodziewajmy się jednego dużego zacięcia w minigrze. Jedynym sposobem pozbycia się go, jest podział tego dużego pliku na klika mniejszych.

Ponadto nasz wynik znika razem z ekranem wczytywania, gracz powinien wiedzieć ile pieniążków udało mu się zebrać.

Pamiętajmy, że to nie koniec, przed nami wiele problemów, które skutecznie będą próbowały zniechęcić nas do pracy, dlatego warto poznać wroga zanim zaczniemy z nim walkę.

Pułapki

Po chwili zabawy z projektem możemy zauważyć kilka błędów (nawet błędów krytycznych). LibLoad() oraz LibFree() są tego powodem. Funkcja LibLoad() potrafi wczytać kilka identycznych rzeczy w różnych miejscach pamięci - przez co BGE może się pogubić. Przykładowo, jeśli mamy dwa pliki - jeden z drzewem, a drugi z ławką - i wykorzystamy w nich materiał o tej samej nazwie (niekoniecznie z tymi samymi ustawieniami), to BGE z trudem je rozróżni. Możemy próbować nadać unikalną nazwą każdemu elementowi w grze, albo czekać na poprawienie tej funkcji w kolejnych wersjach Blendera.

LibFree() sam w sobie nie jest groźny (jest genialny), potrafi usuwać bardzo szybko wskazane pliki ".blend" i robić to bez zacięć i strat wydajności gry. Jednak gdy przypomnimy sobie poprzedni akapit, to możemy się domyślić, że czasem LibFree() usunie nam nie ten materiał, lub obiekt co trzeba, zdarza się to rzadko, ale skutkuje nieprzewidywalnymi błędami, więc kolejny raz musimy się zastanowić, czy nie nadawać obiektom unikalnych nazw.

Wspomniałem, że przy bardzo szybkim przełączaniu się między tym samym poziomem, może dojść do wczytywania i usuwania tego samego obiektu w jednym czasie, to najtrudniejszy błąd pary LibLoad() - LibFree(). Możemy sobie z nim poradzić tylko utrudniając życie graczowi - jeśli dodamy zwykła animację wejścia przycisków menu, to prawdopodobnie LibFree() poradzi sobie z usunięciem wszystkiego zanim gracz będzie mógł kliknąć na poziom ponownie.

Istnieje też trudność w komunikacji między obiektami podczas gry. Oczywiście obiekty wczytane z różnych plików oddziaływają na siebie, ale w edytorze kostek logiki nie możemy wybrać elementów z innego pliku. Jednym ze sposobów na poradzenie sobie z tym problemem jest wykorzystywanie sensorów i aktywatorów Message w kostkach logiki.

Kolejnym problemem są działające skrypty (i logika) podczas wczytywania. W tym okresie przeciwnicy gracza mogą go zabić, gracz może spaść pod mapę (dlatego w skryptach wczytywaliśmy najpierw mapę a potem resztę, żeby uniknąć tego błędu), postacie niezależne mogą nawet przejść przez ściany. Widzieliśmy to w innych grach. Te problemy nie istnieją przy statycznym ekranie wczytywania, dlatego do tej pory się z niego korzysta. Jeśli jednak chcemy sobie poradzić z problemami, to musimy po prostu przewidzieć co się może dziać i przy pomocy logiki lub skryptów, opanować pierwsze chwile istnienia obiektu (spowalniając, wyłączając, przenosząc go..).

Ostatnim problemem jaki powinniśmy poruszyć są cutscenki renderowane na silniku gry podczas wczytywania. Powinny działać poprawnie, ale jeśli przykładowo chcemy pokazać bitwę pod Grunwaldem, to minie sporo czasu zanim wczyta się sama scenka. Możemy stworzyć ekran wczytywania do sceny bitwy a podczas bitwy wczytywać grę. Podobnie jest z minigrami, łatwo możemy przesadzić z gabarytami i będzie trzeba nieźle kombinować.

Podsumowanie

Poznaliśmy kilka metod wczytywania kontentu gry, prześledziliśmy mechanikę czterech typów ekranu wczytywania, przeszliśmy przez setki linijek kodu - brawo!

Jeśli uważacie, że poradnik jest za długi - macie rację
z poważaniem Łukasz Domski.

3 komentarze
krasnoludek napisał :
godz. 19:04, 18 kwietnia 2014
Świetny poradnik Łukasz, zresztą jak zawsze, w razie czego można zadawać pytania? Może będę pisał, bo nie znam za dobrze pythona. Większość na szczęście jasna.
lukaszfito napisał :
godz. 22:05, 19 kwietnia 2014
Oczywiście, po to jest forum - żeby pytać, odpowiadać i rozmawiać :)
lukaszfito napisał :
godz. 09:01, 19 lipca 2014
Żeby plik "ekran_wczytywania.blend" działał pod Blenderem 2.71, wystarczy do każdej ścieżki w funkcji LibLoad() dodać "//" na początku ścieżki - przykłady: LibLoad( "//"+"stałe\\trawa.blend", "Mesh";) lub LibLoad( "//stałe\\trawa.blend", "Mesh";)

Tak na prawdę problem jest z nazwą, jaka zostaje nadana plikowi .blend przez Blendera, jest to po prostu jego ścieżka (ale nazwa musi zaczynać się od "//", by wczytana "biblioteka" działała poprawnie).
Dodaj komentarz
Aby dodać komentarz do newsa, musisz być zalogowany w Serwisie.. Zaloguj