結果は次のとおりです(マップは5分で作成されました+撮影中にゲームの速度が低下しました+画面があまり伸びません-バンディカムの欠陥):
ソースとexe-記事の下部。
何、どこ、いつ?
Box2d
このライブラリを使用して、プラットフォーマーで物理をシミュレートします(ブロックとの衝突、重力)。 このライブラリをブロックのみに使用する価値はなかったかもしれませんが、美しい生活を禁じることはできません;)
なぜBox2Dなのか? 最も一般的で無料の物理ライブラリであるため
SFML
SFMLを選ぶ理由 最初はSDLライブラリを使用したかったのですが、SFMLと比較すると機能が非常に制限されているため、自分で追加する必要があります。 時間を節約してくれたSFMLの作者に感謝します!
グラフィックを描くために使用します
タイルマップエディター
Tiled Map Editorはここで何をしますか?
ゲーム用のカードを作成したことがありますか? 最初のカードはそういうものだったに違いない。
非表示のテキスト
1111111 1000001 1001001 1000011 1000111 1111111
これはかなり非効率的なソリューションです! マップエディターのようなものを作成する方がはるかに優れていますが、後から考えると、これは5分で完了せず、上記の「マップ」は非常に優れていることがわかります。
タイルマップエディターはそのようなマップエディターの1つです。 このエディターで作成されたマップ(オブジェクト、タイル、レイヤーで構成されている)をXMLのような.tmxファイルに保存し、特別なC ++ライブラリを使用して読み取ることができるため便利です。 しかし、まず最初に。
地図作成
公式サイトからTMEをダウンロードしてください
新しいファイル「ファイル->作成...」を作成します
方向は直交する必要があり(アイソメトリックプラットフォーマーを実行していない場合)、XMLレイヤーの形式は、この特定の形式を読み取ります。
ところで、作成されたマップでは、レイヤーの形式もタイルのサイズも変更できません。
タイル
次に、「マップ->新しいタイルセット...」に進み、タイルセットをロードします。
次のような結果になります。
タイルレイヤーの意味は何ですか?
ほとんどすべてのゲームには多層カードがあります。 最初のレイヤーは地球(氷、チェルノゼムなど)、2番目のレイヤーは建物(兵舎、砦など、背景は透明)、3番目は木(トウヒ、モミなど、背景も透明)です。 つまり、最初に最初のレイヤーが描画され、その上に2番目のレイヤーが配置され、次に3番目のレイヤーが配置されます。
レイヤーを作成するプロセスは、次の4つのスクリーンショットに示されています。
レイヤーのリスト:
オブジェクト
TMEのオブジェクトとは何ですか?
オブジェクトには、独自の名前、タイプ、および値を持つパラメーターがあります。
このパネルはオブジェクトを担当します。
それぞれのボタンが何をするのか、自分で確認することができます。
それでは、オブジェクトを作成してみましょう。
「Kolobosha」レイヤーを削除し、代わりに、例えば「Kolobosha」という同じ名前のオブジェクトのレイヤーを作成します。 オブジェクトのパネルから「タイルオブジェクトの挿入」を選択し(または任意のシェイプ-シェイプを選択できます)、Kolobosiタイルをクリックして、オブジェクトをある場所に配置します。
次に、オブジェクトを右クリックし、「オブジェクトのプロパティ...」をクリックします。 オブジェクトの名前をKoloboshaに変更します。
次に、マップを保存します。
一般に、マップエディターには古風なものはありません。 カードの読み取りに移りましょう。
カードリーダー
XMLファイルを読み込むための優れたTinyXMLライブラリが作成されました。
Visual Studioプロジェクトを作成します。 TinyXMLファイルを含めます(または、xmltest.cppを除き、これらのファイルをすべてプロジェクトにプッシュします:))
「プロジェクト->プロパティ」でinclud'yとlib'y SFMLを接続します。 方法がわからない場合は、Googleへようこそ。
MapsのLevel.hを作成する
#ifndef LEVEL_H #define LEVEL_H #pragma comment(lib,"Box2D.lib") #pragma comment(lib,"sfml-graphics.lib") #pragma comment(lib,"sfml-window.lib") #pragma comment(lib,"sfml-system.lib") #include <string> #include <vector> #include <map> #include <SFML/Graphics.hpp>
これがファイルの始まりです。
次はオブジェクトの構造です
struct Object { int GetPropertyInt(std::string name); float GetPropertyFloat(std::string name); std::string GetPropertyString(std::string name); std::string name; std::string type; sf::Rect<int> rect; std::map<std::string, std::string> properties; sf::Sprite sprite; };
分析しましょう。
既に述べたように、TMEでは各オブジェクトにパラメーターを設定できます。 パラメータはXMLファイルから取得され、プロパティに書き込まれます。その後、最初の3つの関数のいずれかによって取得できます。 name-オブジェクトの名前、type-そのタイプ、rect-オブジェクトを記述する長方形 最後に、スプライト-スプライト(イメージ)-オブジェクト用に取得されたタイルセットの一部。 スプライトがない場合があります。
レイヤー構造が登場しました-非常にシンプルです
struct Layer { int opacity; std::vector<sf::Sprite> tiles; };
レイヤーには透明度(はい、はい、半透明のレイヤーを作成できます!)とタイルのリストがあります。
次はLevelクラスです。
class Level { public: bool LoadFromFile(std::string filename); Object GetObject(std::string name); std::vector<Object> GetObjects(std::string name); void Draw(sf::RenderWindow &window); sf::Vector2i GetTileSize(); private: int width, height, tileWidth, tileHeight; int firstTileID; sf::Rect<float> drawingBounds; sf::Texture tilesetImage; std::vector<Object> objects; std::vector<Layer> layers; }; #endif
LoadFromFileは、指定されたファイルからマップをロードします。 これがLevelクラスの中心です。
GetObjectは指定された名前を持つ最初のオブジェクトを返し、 GetObjectsは指定された名前を持つオブジェクトのリストを返します。 実際、良い方法では、オブジェクトのタイプを使用する必要がありますが、エディターでは名前がオブジェクトの上に表示され、タイプは表示されないため、名前を通してブロックとプレーヤーをキャッチする方が便利でした。
Drawは、RenderWindowのインスタンスを取得して、すべてのタイル(オブジェクトではありません!)
次にLevel.cppを作成します。
#include "level.h" #include <iostream> #include "tinyxml.h"
まず、オブジェクトの構造を処理します
int Object::GetPropertyInt(std::string name) { return atoi(properties[name].c_str()); } float Object::GetPropertyFloat(std::string name) { return strtod(properties[name].c_str(), NULL); } std::string Object::GetPropertyString(std::string name) { return properties[name]; }
レイヤーの実装が不要な場合は、レベルに移動します。
boolレベル:: LoadFromFile(std :: string filename)
bool Level::LoadFromFile(std::string filename) { TiXmlDocument levelFile(filename.c_str()); // XML- if(!levelFile.LoadFile()) { std::cout << "Loading level \"" << filename << "\" failed." << std::endl; return false; } // map TiXmlElement *map; map = levelFile.FirstChildElement("map"); // : <map version="1.0" orientation="orthogonal" // width="10" height="10" tilewidth="34" tileheight="34"> width = atoi(map->Attribute("width")); height = atoi(map->Attribute("height")); tileWidth = atoi(map->Attribute("tilewidth")); tileHeight = atoi(map->Attribute("tileheight")); // TiXmlElement *tilesetElement; tilesetElement = map->FirstChildElement("tileset"); firstTileID = atoi(tilesetElement->Attribute("firstgid")); // source - image TiXmlElement *image; image = tilesetElement->FirstChildElement("image"); std::string imagepath = image->Attribute("source"); // sf::Image img; if(!img.loadFromFile(imagepath)) { std::cout << "Failed to load tile sheet." << std::endl; return false; } // (109, 159, 185) // - , , 16- // "6d9fb9" img.createMaskFromColor(sf::Color(109, 159, 185)); // tilesetImage.loadFromImage(img); // tilesetImage.setSmooth(false); // int columns = tilesetImage.getSize().x / tileWidth; int rows = tilesetImage.getSize().y / tileHeight; // (TextureRect) std::vector<sf::Rect<int>> subRects; for(int y = 0; y < rows; y++) for(int x = 0; x < columns; x++) { sf::Rect<int> rect; rect.top = y * tileHeight; rect.height = tileHeight; rect.left = x * tileWidth; rect.width = tileWidth; subRects.push_back(rect); } // TiXmlElement *layerElement; layerElement = map->FirstChildElement("layer"); while(layerElement) { Layer layer; // opacity, , if (layerElement->Attribute("opacity") != NULL) { float opacity = strtod(layerElement->Attribute("opacity"), NULL); layer.opacity = 255 * opacity; } else { layer.opacity = 255; } // <data> TiXmlElement *layerDataElement; layerDataElement = layerElement->FirstChildElement("data"); if(layerDataElement == NULL) { std::cout << "Bad map. No layer information found." << std::endl; } // <tile> - TiXmlElement *tileElement; tileElement = layerDataElement->FirstChildElement("tile"); if(tileElement == NULL) { std::cout << "Bad map. No tile information found." << std::endl; return false; } int x = 0; int y = 0; while(tileElement) { int tileGID = atoi(tileElement->Attribute("gid")); int subRectToUse = tileGID - firstTileID; // TextureRect if (subRectToUse >= 0) { sf::Sprite sprite; sprite.setTexture(tilesetImage); sprite.setTextureRect(subRects[subRectToUse]); sprite.setPosition(x * tileWidth, y * tileHeight); sprite.setColor(sf::Color(255, 255, 255, layer.opacity)); layer.tiles.push_back(sprite); } tileElement = tileElement->NextSiblingElement("tile"); x++; if (x >= width) { x = 0; y++; if(y >= height) y = 0; } } layers.push_back(layer); layerElement = layerElement->NextSiblingElement("layer"); } // TiXmlElement *objectGroupElement; // if (map->FirstChildElement("objectgroup") != NULL) { objectGroupElement = map->FirstChildElement("objectgroup"); while (objectGroupElement) { // <object> TiXmlElement *objectElement; objectElement = objectGroupElement->FirstChildElement("object"); while(objectElement) { // - , , , etc std::string objectType; if (objectElement->Attribute("type") != NULL) { objectType = objectElement->Attribute("type"); } std::string objectName; if (objectElement->Attribute("name") != NULL) { objectName = objectElement->Attribute("name"); } int x = atoi(objectElement->Attribute("x")); int y = atoi(objectElement->Attribute("y")); int width, height; sf::Sprite sprite; sprite.setTexture(tilesetImage); sprite.setTextureRect(sf::Rect<int>(0,0,0,0)); sprite.setPosition(x, y); if (objectElement->Attribute("width") != NULL) { width = atoi(objectElement->Attribute("width")); height = atoi(objectElement->Attribute("height")); } else { width = subRects[atoi(objectElement->Attribute("gid")) - firstTileID].width; height = subRects[atoi(objectElement->Attribute("gid")) - firstTileID].height; sprite.setTextureRect(subRects[atoi(objectElement->Attribute("gid")) - firstTileID]); } // Object object; object.name = objectName; object.type = objectType; object.sprite = sprite; sf::Rect <int> objectRect; objectRect.top = y; objectRect.left = x; objectRect.height = height; objectRect.width = width; object.rect = objectRect; // "" TiXmlElement *properties; properties = objectElement->FirstChildElement("properties"); if (properties != NULL) { TiXmlElement *prop; prop = properties->FirstChildElement("property"); if (prop != NULL) { while(prop) { std::string propertyName = prop->Attribute("name"); std::string propertyValue = prop->Attribute("value"); object.properties[propertyName] = propertyValue; prop = prop->NextSiblingElement("property"); } } } // objects.push_back(object); objectElement = objectElement->NextSiblingElement("object"); } objectGroupElement = objectGroupElement->NextSiblingElement("objectgroup"); } } else { std::cout << "No object layers found..." << std::endl; } return true; }
他のレベル関数:
Object Level::GetObject(std::string name) { // for (int i = 0; i < objects.size(); i++) if (objects[i].name == name) return objects[i]; } std::vector<Object> Level::GetObjects(std::string name) { // std::vector<Object> vec; for(int i = 0; i < objects.size(); i++) if(objects[i].name == name) vec.push_back(objects[i]); return vec; } sf::Vector2i Level::GetTileSize() { return sf::Vector2i(tileWidth, tileHeight); } void Level::Draw(sf::RenderWindow &window) { // ( !) for(int layer = 0; layer < layers.size(); layer++) for(int tile = 0; tile < layers[layer].tiles.size(); tile++) window.draw(layers[layer].tiles[tile]); }
Level.hで終わりました!
テストします。
main.cppを作成して記述します。
#include "level.h" int main() { Level level; level.LoadFromFile("test.tmx"); sf::RenderWindow window; window.create(sf::VideoMode(800, 600), "Level.h test"); while(window.isOpen()) { sf::Event event; while(window.pollEvent(event)) { if(event.type == sf::Event::Closed) window.close(); } window.clear(); level.Draw(window); window.display(); } return 0; }
マップは何でも似ています!
オブジェクトをいじることができます:
main.cpp
#include "level.h" #include <iostream> int main() { Level level; level.LoadFromFile("test.tmx"); Object kolobosha = level.GetObject("Kolobosha"); std::cout << kolobosha.name << std::endl; std::cout << kolobosha.type << std::endl; std::cout << kolobosha.GetPropertyInt("health") << std::endl; std::cout << kolobosha.GetPropertyString("mood") << std::endl; sf::RenderWindow window; window.create(sf::VideoMode(800, 600), "Kolobosha adventures"); while(window.isOpen()) { sf::Event event; while(window.pollEvent(event)) { if(event.type == sf::Event::Closed) window.close(); } window.clear(); level.Draw(window); window.display(); } return 0; }
結果:
オブジェクトをいじるときは、Box2Dの時間です。
箱
マップ上にオブジェクトがあります-名前のプレイヤー-プレイヤー、敵-敵、ブロック-ブロック、お金-コイン。
プレーヤーをロードし、キーストロークとニュートンの強さに従うようにします。
敵が前後に移動し、プレイヤーをあまりにも近くに押し込み、プレイヤーがジャンプした場合に死にます
ブロックは静的オブジェクトとして「空中」に固定され、プレイヤーはそれらにジャンプできます
コインは何も与えず、プレイヤーと衝突すると消えます
main.hを開き、そこに書き込まれた内容を消去して、書き込みます。
#include "level.h" #include <Box2D\Box2D.h> #include <iostream> #include <random> Object player; b2Body* playerBody; std::vector<Object> coin; std::vector<b2Body*> coinBody; std::vector<Object> enemy; std::vector<b2Body*> enemyBody;
ここで、level.hとBox2D.hを接続しました。 iostreamは、コンソールへの出力、ランダム-敵の移動方向の生成に必要です。
次に、プレイヤーとベクター、各敵、コイン、プレイヤーは自分のオブジェクトとb2Body(Box2Dのボディ)に依存しています。
注意-ブロックは、ゲームロジックではなくBox2D物理レベルでのみプレーヤーと対話するため、これを行うことは想定されていません。
次:
int main() { srand(time(NULL)); Level lvl; lvl.LoadFromFile("platformer.tmx"); b2Vec2 gravity(0.0f, 1.0f); b2World world(gravity); sf::Vector2i tileSize = lvl.GetTileSize();
ランダム化にはsrand(時間(NULL))が必要です。
マップをロードし、b2Worldを作成し、重力をマップに転送します。 ところで、重力は好きな方向から来ることができ、(0.10)からの重力は(0.1)よりも強く作用します。 次に、必要なタイルのサイズを取得します
次に、ブロック本体を作成します。
std::vector<Object> block = lvl.GetObjects("block"); for(int i = 0; i < block.size(); i++) { b2BodyDef bodyDef; bodyDef.type = b2_staticBody; bodyDef.position.Set(block[i].rect.left + tileSize.x / 2 * (block[i].rect.width / tileSize.x - 1), block[i].rect.top + tileSize.y / 2 * (block[i].rect.height / tileSize.y - 1)); b2Body* body = world.CreateBody(&bodyDef); b2PolygonShape shape; shape.SetAsBox(block[i].rect.width / 2, block[i].rect.height / 2); body->CreateFixture(&shape,1.0f); }
bodyDef.type = b2_staticBody;
ブロックは静的な物体であり、質量がなく空中に垂れ下がっています。
bodyDef.position.Set(block[i].rect.left + tileSize.x / 2 * (block[i].rect.width / tileSize.x - 1), block[i].rect.top + tileSize.y / 2 * (block[i].rect.height / tileSize.y - 1));
ここで、ブロックの位置を設定します。 事実は、位置がオブジェクトの位置と同じであることを単純に示すと、陰湿な間違いに直面することです。
b2Body* body = world.CreateBody(&bodyDef);
ワールドでブロックボディを作成します。 さらに、私たちは身体を操作しません(ある意味では、どこにも保存しません):
b2PolygonShape shape; shape.SetAsBox(block[i].rect.width / 2, block[i].rect.height / 2);
それぞれの体にはいくつかの形があります。 このトピックを詳細に分析することはしません。なぜなら、ブロック(および残りのボディ)には1つの長方形で十分だからです。
body->CreateFixture(&shape,1.0f);
体と体をつなぎます。
次に、敵、コイン、およびプレイヤーについて同じことを行いますが、いくつかの違いがあります。
coin = lvl.GetObjects("coin"); for(int i = 0; i < coin.size(); i++) { b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position.Set(coin[i].rect.left + tileSize.x / 2 * (coin[i].rect.width / tileSize.x - 1), coin[i].rect.top + tileSize.y / 2 * (coin[i].rect.height / tileSize.y - 1)); bodyDef.fixedRotation = true; b2Body* body = world.CreateBody(&bodyDef); b2PolygonShape shape; shape.SetAsBox(coin[i].rect.width / 2, coin[i].rect.height / 2); body->CreateFixture(&shape,1.0f); coinBody.push_back(body); } enemy = lvl.GetObjects("enemy"); for(int i = 0; i < enemy.size(); i++) { b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position.Set(enemy[i].rect.left + tileSize.x / 2 * (enemy[i].rect.width / tileSize.x - 1), enemy[i].rect.top + tileSize.y / 2 * (enemy[i].rect.height / tileSize.y - 1)); bodyDef.fixedRotation = true; b2Body* body = world.CreateBody(&bodyDef); b2PolygonShape shape; shape.SetAsBox(enemy[i].rect.width / 2, enemy[i].rect.height / 2); body->CreateFixture(&shape,1.0f); enemyBody.push_back(body); } player = lvl.GetObject("player"); b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position.Set(player.rect.left, player.rect.top); bodyDef.fixedRotation = true; playerBody = world.CreateBody(&bodyDef); b2PolygonShape shape; shape.SetAsBox(player.rect.width / 2, player.rect.height / 2); b2FixtureDef fixtureDef; fixtureDef.shape = &shape; fixtureDef.density = 1.0f; fixtureDef.friction = 0.3f; playerBody->CreateFixture(&fixtureDef);
bodyDef.fixedRotation = true;
体が回転できないことを意味します。
すべてのボディが作成され、スケジュールを初期化するために残ります!
sf::Vector2i screenSize(800, 600); sf::RenderWindow window; window.create(sf::VideoMode(screenSize.x, screenSize.y), "Game");
よく理解されているコードは、指定されたサイズとタイトルでウィンドウを作成します。
sf::View view; view.reset(sf::FloatRect(0.0f, 0.0f, screenSize.x, screenSize.y)); view.setViewport(sf::FloatRect(0.0f, 0.0f, 2.0f, 2.0f));
ここで、ウィンドウのビューを作成します。
なぜこれが必要なのですか? ゲームにピクセルスタイルを与えるために、sf :: Viewを使用して画面サイズを2倍します。すべての画像は2倍の高さと幅で描画されます。
while(window.isOpen()) { sf::Event evt; while(window.pollEvent(evt)) { switch(evt.type) { case sf::Event::Closed: window.close(); break;
赤いXをクリックすると、ウィンドウが閉じます。 そのようなコードは以前のものでした:
case sf::Event::KeyPressed: if(evt.key.code == sf::Keyboard::W) playerBody->SetLinearVelocity(b2Vec2(0.0f, -15.0f)); if(evt.key.code == sf::Keyboard::D) playerBody->SetLinearVelocity(b2Vec2(5.0f, 0.0f)); if(evt.key.code == sf::Keyboard::A) playerBody->SetLinearVelocity(b2Vec2(-5.0f, 0.0f)); break;
すでにもっと面白いものがあります! WADキーを押すことにより、プレーヤーに速度を追加します。
world.Step(1.0f / 60.0f, 1, 1);
ここでは、Box2Dの物理世界を更新します。 最初の引数は、velocityIterationsおよびpositionIterationsの数と同様に、ワールドを更新する頻度(1/60秒ごと)を取ります。 最後の2つの引数の値が高いほど、ゲームの物理はより現実的です。 AngryBirdsのように複雑な形状はなく、長方形だけなので、必要なのは1回だけです。
for(b2ContactEdge* ce = playerBody->GetContactList(); ce; ce = ce->next) { b2Contact* c = ce->contact;
ここでは、プレイヤーと他のボディとの衝突を処理します。
for(int i = 0; i < coinBody.size(); i++) if(c->GetFixtureA() == coinBody[i]->GetFixtureList()) { coinBody[i]->DestroyFixture(coinBody[i]->GetFixtureList()); coin.erase(coin.begin() + i); coinBody.erase(coinBody.begin() + i); }
コイン衝突処理。
コインがプレイヤーと衝突した場合、単に破壊され、ベクターから消去されます。
for(int i = 0; i < enemyBody.size(); i++) if(c->GetFixtureA() == enemyBody[i]->GetFixtureList()) { if(playerBody->GetPosition().y < enemyBody[i]->GetPosition().y) { playerBody->SetLinearVelocity(b2Vec2(0.0f, -10.0f)); enemyBody[i]->DestroyFixture(enemyBody[i]->GetFixtureList()); enemy.erase(enemy.begin() + i); enemyBody.erase(enemyBody.begin() + i); }
敵がプレイヤーと衝突した場合、敵プレイヤーがより高いかどうかがチェックされます。 プレイヤーが敵より背が高い場合、彼は消去され、プレイヤーはジャンプアップします。
それ以外の場合、プレイヤーは敵に跳ね返ります:
else { int tmp = (playerBody->GetPosition().x < enemyBody[i]->GetPosition().x) ? -1 : 1; playerBody->SetLinearVelocity(b2Vec2(10.0f * tmp, 0.0f)); } } }
プレイヤーは、敵に対する現在の位置に応じて右または左に移動します。
for(int i = 0; i < enemyBody.size(); i++) { if(enemyBody[i]->GetLinearVelocity() == b2Vec2_zero) { int tmp = (rand() % 2 == 1) ? 1 : -1; enemyBody[i]->SetLinearVelocity(b2Vec2(5.0f * tmp, 0.0f)); } }
敵の速度が0の場合、再び速度が与えられます-敵は右または左に動きます。 視覚的には、ぎくしゃくした動きのように見えます。
b2Vec2 pos = playerBody->GetPosition(); view.setCenter(pos.x + screenSize.x / 4, pos.y + screenSize.y / 4); window.setView(view);
グラフィックを操作します。 プレイヤーの位置を取得し、ビューの中心を変更して、ビューを使用します
player.sprite.setPosition(pos.x, pos.y); for(int i = 0; i < coin.size(); i++) coin[i].sprite.setPosition(coinBody[i]->GetPosition().x, coinBody[i]->GetPosition().y); for(int i = 0; i < enemy.size(); i++) enemy[i].sprite.setPosition(enemyBody[i]->GetPosition().x, enemyBody[i]->GetPosition().y);
プレーヤーのスプライト、コイン、および敵をb2Bodyから取得した位置に設定します。
window.clear(); lvl.Draw(window); window.draw(player.sprite); for(int i = 0; i < coin.size(); i++) window.draw(coin[i].sprite); for(int i = 0; i < enemy.size(); i++) window.draw(enemy[i].sprite); window.display();
ウィンドウをクリアし、マップタイルを描画してから、プレイヤー、コイン、敵を描画し、その後ウィンドウを表示します。
} return 0; }
できた!
サンプルマップ:
ソースコード
https://github.com/Izaron/Platformer
謝辞
このトピックの著者へ
SFMLの著者へ
Box2D著者
著者TinyXml