Crash, Replace, Repeat — Porting Resolutiion to Godot 3

in #games6 years ago (edited)

Resoluttion with the Godot logo and the number 3

<p dir="auto"><a href="https://resolutiion.monolithofminds.com/" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Resolutiion is an indie action-adventure game in the making, with a modern pixel style and plenty of fat, yellow worms. My brother and I have been working on this passion project for the better part over the last three years. <p dir="auto">Initially “The Game” needed an engine for the heavy lifting. We are both big open source enthusiasts and I really wanted to do all the development on my Linux machine. Unity and Unreal both support development and exporting projects to Linux platforms and allow access to parts of their source code. Still, they don’t feel truly open-source in the sense of community engagement and direction. <p dir="auto">Searching for alternatives I stumbled upon Godot. The young game engine looked very promising, following a holistic open source approach and all the features I needed for our little 2D game. It felt similar to Blender and I didn’t expect to outgrow its capabilities in the future, being my first game development project — ever. <h1>Port Royal <p dir="auto">July 9th, 2015.<br /> I committed the first lines of code to our repository, based on Godot 1.0. <p dir="auto">Since the API never changed dramatically during the following months, I jumped from version to version like nothing could stop me, taking “The Game” along. <p dir="auto">When Godot 3.0 was announced in 2017, mayor API breaks appeared on the horizon. By then we had called the project “Resolutiion” and it had grown to a beast, collecting over 1,700 files and resources. Porting all our scripts and scenes to a new system would require tons of work. But the new features like audio effects, sprite tessellation and plenty of editor improvements were just too tempting to pass up. <p dir="auto">I waited until early August when Godot 2.1.5 had reached the “stable” mark. It included an exporter to help transfer any project from the 2.x to the 3.x version. The rest of the team was on vacation, so no one could distract me. <p dir="auto">This is how porting our game to Godot 3.1 went down. <h1>Step 1 — The Magic Button <p dir="auto">I created a new Git-branch, called it “PortTo3.0”, and fired up Godot 2.1.5. This version included a promising button, labeled “Export to Godot 3.0 (WIP)”. <p dir="auto">Click! <p dir="auto"><img src="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmUEqnSPUF6QafVRxaRfDCtLch7wSbvr1osJiWiCNkMHdU/resolutiion-export.png" alt="Export-progress at 6 percent" srcset="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmUEqnSPUF6QafVRxaRfDCtLch7wSbvr1osJiWiCNkMHdU/resolutiion-export.png 1x, https://images.hive.blog/1536x0/https://cdn.steemitimages.com/DQmUEqnSPUF6QafVRxaRfDCtLch7wSbvr1osJiWiCNkMHdU/resolutiion-export.png 2x" /> <p dir="auto">A nice loading screen appeared and started to count percentages up. Be patient I told myself, this was going to take a while... Eventually the counter hit 99%, and then disaster: <pre><code>Couldn't load resource: res://scenes/tileset.xscn::40 Method/Function failed, returning: ERR_BUG <p dir="auto">Ok, that’s easy. Godot used to support three internal file-types: <em>scn (binary), <em>xscn (xml) and <em>tscn (readable text). The <em>xscn-format is now deprecated, in favor of <em>scn and <em>tscn. No big deal. I changed the corresponding files and restarted the routine. This time no errors occurred and the 100% export was completed safe and sound. <p dir="auto">Importing the converted project into Godot 3.0.6 resulted in another progress bar. Plenty of warnings were thrown on the console, but I ignored them for the time being. Since the whole game is made up of individual scenes and sub-scenes, I had to take a look at the important ones anyway. Let’s see what works and what breaks. <p dir="auto"><strong>Lessons learned <ul> <li>Don’t forget to branch (Git) <li>Convert <em>xscn files to <em>tscn format <h1>Step 2 — Global Scene <p dir="auto">Like most games, we run a global scene. Ours is called <em>global.scn. It acts as a singleton and handles all the boring stuff like saving and loading, switching levels, input controls, the audio system and a couple of globally used shaders. It's the foundation for every other scene that runs on top of it. <p dir="auto">Opening <em>global.scn, I noticed that all our shaders were missing. The shaders had to be rewritten.<br /> Here’s a simple example: <p dir="auto"><strong>Old <pre><code>uniform float size_pixel=0.008; uv-=mod(uv,vec2(size_pixel,size_pixel)); COLOR.rgb = texscreen(uv); <p dir="auto"><strong>New <pre><code>shader_type canvas_item; uniform float size_pixel=0.008; void fragment() { vec2 uv = SCREEN_UV; uv-=mod(uv,vec2(size_pixel,size_pixel)); COLOR.rgb= textureLoad(SCREEN_TEXTURE,uv,0.0).rgb; } <p dir="auto">Resolutiion’s pixel aesthetic does not require many shaders, so the workload was manageable, but this might have been a different story for any other game. <p dir="auto"><img src="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmeM2PBzxEEmZioUxaNb5LFD5GiAhKMZFCvmT3g9MDgcZ5/resolutiion-screenshots.png" alt="Resolutiion screenshots" srcset="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmeM2PBzxEEmZioUxaNb5LFD5GiAhKMZFCvmT3g9MDgcZ5/resolutiion-screenshots.png 1x, https://images.hive.blog/1536x0/https://cdn.steemitimages.com/DQmeM2PBzxEEmZioUxaNb5LFD5GiAhKMZFCvmT3g9MDgcZ5/resolutiion-screenshots.png 2x" /> <p dir="auto">Let’s start the scene.<br /> Crash! <p dir="auto">The debugger tells me <code>Identifier not found: Globals.<br /> That right. The global variables <code>Globals are now called <code>ProjectSettings. <p dir="auto">We use the Globals system everywhere in the game. It would be a pain to change all respective scripts by hand, but there are great tools available to help with bulk processing. I use <a href="https://atom.io/" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Atom due to its powerful and extensively used “find-all-occurrences-of-a-string-in-the-whole-project-folder-and-rename-them-accordingly” feature. Nothing to do with Atom being open source software ;-) <p dir="auto">Let’s start the scene again.<br /> Crash!<br /> <code>get_data_dir for loading save-files is now called <code>get_user_data_dir.<br /> A quick fix. <p dir="auto">Restart.<br /> Crash!<br /> Something was wrong with the audio system.<br /> Finally, we’re onto the fun stuff. <p dir="auto">Since Godot’s audio system had been completely reworked, we needed to do the same to our internal pipeline. I set up some busses for music and effects streams, and adapted the configuration of those according to the new bus API. The volume scale in Godot 2.x was defined from 0 to 1. Godot 3’s new bus system makes use of the decibel scale instead, ranging from -80 to 0. <p dir="auto"><img src="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmeCHD9JBfUCwrbkEdP228q75wLZvUEW57pN3M76sW53mN/resolutiion-audio.png" alt="Audio busses" srcset="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmeCHD9JBfUCwrbkEdP228q75wLZvUEW57pN3M76sW53mN/resolutiion-audio.png 1x, https://images.hive.blog/1536x0/https://cdn.steemitimages.com/DQmeCHD9JBfUCwrbkEdP228q75wLZvUEW57pN3M76sW53mN/resolutiion-audio.png 2x" /> <p dir="auto">Ok, restart scene.<br /> Crash!<br /> Dammit, it’s the input-event-system-types.<br /> All right, that one was also redesigned and event-types don’t exist anymore. Instead, input-events are now resources and checking them looks something like: <pre><code>func _input(ev): if ev is InputEventMouseButton or ev is InputEventKey: #mouse/keyboard cur_input_type = 0 elif ev is InputEventJoypadButton or ev is InputEventJoypadMotion: #joypad cur_input_type = 1 <p dir="auto">Restart scene.<br /> Wait …<br /> By Marty’s Boot: it’s working! <p dir="auto"><img src="https://images.hive.blog/0x0/https://cdn.steemitimages.com/DQmafRe2VtRSHoYKnyZj6hSZ9QzpQcThYZS7FB6T3azB1cS/resolutiion-martys_boot.gif" alt="Marty’s Boot" /> <p dir="auto">Committing all those audio related changes to the global scene, I realized that my trusty, sound-handling buddy <em>SamplePlayer2D was no more. Instead there was a new <em>AudioStream2D node, connecting to the audio busses. This fella can hold no more than a single sample, instead of a variable array, like my old friend.<br /> What a wuss. <p dir="auto">Also, looping and loop times are now decoupled from the audio player and set directly on the audio resource (wav or ogg). So we have to do some more coding. <p dir="auto">Play music in a loop: <pre><code>func music_play(newsong, offset): #load audio players, set loop & offset #newsong is an audio file like ogg or wav if newsong != null: newsong.loop = true newsong.loop_offset = offset get_node("MusicPlayer").stream = newsong <p dir="auto">At this point it became obvious to me that Godot 3’s audio system was not mature enough for our project. Most of the player and NPC characters trigger their many audio effects by performing their respective animations. Due to the new AudioStreamPlayer’s limitations, juggling the streams at the time as the animations would be very cumbersome. <p dir="auto">All was not lost. After some frantic Googling, I discovered that Godot 3.1 will have an improved audio workflow. At that point, I realised I had to leave safe developer space and dive into the unstable and deep waters of Godot 3.1-some-weird-development-build-from-a-shady-website. I downloaded the latest <a href="https://hugo.pro/projects/godot-builds/" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">nightly build and went on with my business, slightly more anxious. <p dir="auto"><strong>Lessons learned <ul> <li><a href="http://docs.godotengine.org/en/stable/tutorials/audio/index.html" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Audio system has to be set up from scratch <li><code>AudioServer.get_stream_global_volume_scale() becomes AudioServer.get_bus_volume_db(0) <li><a href="http://docs.godotengine.org/en/stable/tutorials/shading/index.html" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Shaders need to be rewritten <li><code>Global becomes <code>ProjectSettings <li><code>OS.get_data_dir() becomes <code>OS.get_user_data_dir() <li>Input Event types become Input Event resources <h1>Step 3 — Start Menu <p dir="auto"><img src="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmNqpi7TizRyyBocHZwC9Q94Q3CnVZyD5ToCXNmKDHVq5g/resolutiion-menu.png" alt="The menu with floating particles" srcset="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmNqpi7TizRyyBocHZwC9Q94Q3CnVZyD5ToCXNmKDHVq5g/resolutiion-menu.png 1x, https://images.hive.blog/1536x0/https://cdn.steemitimages.com/DQmNqpi7TizRyyBocHZwC9Q94Q3CnVZyD5ToCXNmKDHVq5g/resolutiion-menu.png 2x" /> <p dir="auto">Enter Resolutiion. An atmospheric tune fades in. The logo appears. Buttons emerge and the background graphics slide up — parallaxing (naturally). Some particles float...<br /> Wait, there are no particles!<br /> Probably because the particle system was also completely refactored in Godot 3.x.<br /> Does that mean I have to dive into all scenes where particles are used, and set them up properly again? <p dir="auto">Yes. Yes it does.<br /> And there are plenty. <p dir="auto"><img src="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmSGDbDBUKfMPW7MizTMTxiYzD96QwcBg3EHa8ZFtVVmaC/resolutiion-particles.png" alt="Particle effects" srcset="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmSGDbDBUKfMPW7MizTMTxiYzD96QwcBg3EHa8ZFtVVmaC/resolutiion-particles.png 1x, https://images.hive.blog/1536x0/https://cdn.steemitimages.com/DQmSGDbDBUKfMPW7MizTMTxiYzD96QwcBg3EHa8ZFtVVmaC/resolutiion-particles.png 2x" /> <p dir="auto">Another problem: buttons and other UI elements no longer reacted to my clicks. This was down to some control nodes in the top layer that stopped the mouse event. The solution was to set all control nodes that could intercept the mouse click events to <em>mouse → ignore. <p dir="auto">Some asset positions in the timelines are messed up, too. So is the UI theme in the settings menu.<br /> More renaming work.<br /> Fixing asset positions.<br /> Starting the scene.<br /> Success! <p dir="auto"><strong>Lessons learned <ul> <li>All particle systems have to be refactored <li>Set control nodes to <em>mouse → ignore <li>Animation player signal <code>finished becomes <code>animation_finished <li>Tween signal <code>tween_complete becomes <code>tween_completed <h1>Step 4 — Characters <p dir="auto">With all the main control scenes running again, continuing the port got more colorful: levels, our player character Valor, NPCs and enemies. The latter didn’t get very far: trying to start the first level caused the NPC class to crash. <p dir="auto">Let’s fix those buggers first, shall we?<br /> <code>collision_layer and <code>collision_mask are key words now. Variables with the same name have to be renamed.<br /> Also <code>get_layer_mask becomes <code>get_collision_layer, which makes it more consistent with the old and new <code>get_collision_mask.<br /> And for helper systems within the editor, we have to change <code>get_tree().is_editor_hint() to <code>Engine.editor_hint == true.<br /> Again, Atom did that work for me in bulk. <p dir="auto">Another major change for Godot 3.x is movement and collision handling of kinematic characters. There are two new methods for moving characters: <code>move_and_slide or <code>move_and_collide, depending on what you want to accomplish. The first will do for most applications and clearly reduces the amount of code needed: <p dir="auto"><strong>Old <pre><code>func _fixed_process(delta): if state in move_states: move( vec.normalized() * speed * delta ) if (is_colliding()): var n = get_collision_normal() move( n.slide(vec).normalized() * speed * delta ) <p dir="auto"><strong>New <pre><code>move_and_slide( vec.normalized() * speed ) <p dir="auto">Don’t forget to remove the <code>delta when calculating the motion, or your character will just crawl very slowly across the screen. <p dir="auto">Developer-humor:<br /> In Godot 2.1 line 19 said <code>trailpath.remove(0), where <code>trailpath is an array. Somehow the exporter thought re-<code>move should do some collisions and renamed that line to <code>trailpath.remove_and_collide().<br /> The auto-exporter clearly showed some character here. <p dir="auto">With the enemies fixed, I moved along to our big, bad, black and deadly player character. <p dir="auto"><img src="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmaQZEnTm9aLafEqZk7gKHsezeoAXPPwPLzNe7pe9tGYCF/resolutiion-spawns.png" alt="Various spawn-items" srcset="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmaQZEnTm9aLafEqZk7gKHsezeoAXPPwPLzNe7pe9tGYCF/resolutiion-spawns.png 1x, https://images.hive.blog/1536x0/https://cdn.steemitimages.com/DQmaQZEnTm9aLafEqZk7gKHsezeoAXPPwPLzNe7pe9tGYCF/resolutiion-spawns.png 2x" /> <p dir="auto">Valor is a toolbox of his own, spawning guns, bombs, and various animals to wreak ruin to the world of Resolutiion. When these spawned objects “die”, I usually removed their collision with the <code>clear_shapes()-function. <p dir="auto">Well, this function doesn’t exist anymore in Godot 3.x, instead we have the very cool <em>disable-variable for all shapes. It lets us toggle the collision of a single shape at runtime. <pre><code>$CollisionShape2D.disabled = true <p dir="auto">With enemies and our player character back in place, they may resume beating the crap out of each other. <p dir="auto"><strong>Lessons learned <ul> <li><a href="http://docs.godotengine.org/en/stable/tutorials/physics/using_kinematic_body_2d.html" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Character movement needs to be refactored to use the new <code>move_and_slide and <code>move_and_collide functions <li>Variable names that are keywords now need to be changed, for example <code>collision_layer <li><code>get_layer_mask() becomes <code>get_collision_layer() <li><code>get_tree().is_editor_hint() becomes <code>Engine.editor_hint == true <li><code>clear_shapes() becomes <code>$CollisionShape2D.disabled = true <li>Rotations are now calculated clockwise instead of counterclockwise <h1>Step 5 — Levels <p dir="auto">In Resolutiion every level is its own scene and embeds some logic for the save and audio systems, as well as the scripts for all cutscenes. There’s a new helper to yield a timer timeout, reducing six lines of code to one: <p dir="auto"><strong>Old <pre><code>var t = Timer.new() t.set_wait_time(3) t.set_one_shot(true) add_child(t) t.start() yield(t, "timeout") <p dir="auto"><strong>New <pre><code>yield(get_tree().create_timer(3), "timeout") <p dir="auto">After hitting Atom’s “Replace all” button for what felt like days, I got a very unsettling message when opening one of the levels: <pre><code>Alert! Error while parsing 'Stadium02.tscn'. <p dir="auto">Another one gave me: <pre><code>Alert! Unexpected end of file 'Grass.scn'. <p dir="auto">Probably a bug in the exporter?<br /> Usually the terminal gives a hint on what went wrong and in which line the problem occurred. In this case it was mostly audio related stuff: <em>SampleLibrary’s sub-resources were used, which Godot 3.x has forgotten all about. <pre><code>Parse Error: Can't create sub-resource of type: SampleLibrary. <p dir="auto">Here, the problem with easily editable scene files became obvious. Occasionally I had to remove a whole sub-resource block and its corresponding links — the sub-resource’s ID comes in handy. Saving the <em>tscn-file solves the error. <p dir="auto">Then, files that were saved as <em>scn can not be opened with a text editor. So I had to fire up Godot 2.1, open the particular scene, save it as a <em>tscn file, move it to the new project folder, and reopen it in Godot 3.1.<br /> Tedious. <p dir="auto"><strong>Lessons learned <ul> <li>Open all scenes and check if they were converted properly. If not, save them as <em>tscn files in Godot 2.x and fix them manually. <h1>Final thoughts <p dir="auto">As I write these lines, Resolutiion might easily be twelve months away from being released. But over the last few weeks, we got much, much closer. Taking our not-so-little project from Godot 2.1.5 to 3.1 gave us plenty of exciting new options and set us up nicely for more to come. <p dir="auto">It took me about twenty hours to refactor all the changes in logic and scripts, and twenty more to grind through the boring tasks. The former was fine, because I used the opportunity to fix some bugs that had been around for a while. The latter one, though, was just repetitive and annoying: opening every single scene and object, updating each animation in respect to the new audio system and then rebuilding all particle systems we had distributed in our heavily connected world. <p dir="auto">An agony.<br /> But it was worth it. <p dir="auto">Investing a week of pain to gain access to amazing features like the new audio pipeline and others can easily be justified. Being able to quickly toggle collision shapes, yielding a Timer, and the improved helper systems <code>$ and <code>get_node will be used extensively in my future development. <p dir="auto">Godot 3 is a wonderful game engine. Using and seeing it mature month by month is certainly a great inspiration. It helps us to stay motivated as we progress with Resolutiion. <p dir="auto"><img src="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmac9ojgskj7y52nPH2x8nQiikDGJRRi3zYAE3trR9sPgh/resolutiion-godot_ui.png" alt="Creating Resolutiion with Godot 3" srcset="https://images.hive.blog/768x0/https://cdn.steemitimages.com/DQmac9ojgskj7y52nPH2x8nQiikDGJRRi3zYAE3trR9sPgh/resolutiion-godot_ui.png 1x, https://images.hive.blog/1536x0/https://cdn.steemitimages.com/DQmac9ojgskj7y52nPH2x8nQiikDGJRRi3zYAE3trR9sPgh/resolutiion-godot_ui.png 2x" /> <hr /> <p dir="auto">Richi and Günther Beyer from Germany are developing their first video game. Inspired by early Zelda and fueled by the do-it-yourself spirit of the open-source movement, <a href="http://resolutiion.monolithofminds.com/" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Resolutiion will ship in 2019, with plenty of pixels, cats and the mystery of the Cradles. <a href="http://resolutiion.monolithofminds.com/#newsletter" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Subscribe to our newsletter or tag along at <a href="https://twitter.com/monolithofminds" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Twitter.
Sort:  

Make real ether while destroying your opponents.
Chibi Fighters is a brutally fun and addicting game.

Come check out Chibi Fighters
https://chibifighters.io/

That is something that will be developing quickly creating of programs for computer games. That is a great market and have more and more users, especially considering that young generation know computer games already from their almost first steps. Everyone who play games have his own thought what is good and what needs to be better and that is the best stimulus to create it better and having such big market it brings a competition and again that is engine to improve yourselves.

It is nice to see how much time and efforts you put in your work and I like the way you mentioned what you have learnt from it. This is sometimes we learn on our mistakes and improve ourselves. I wish you a lot of success!

Hey thanks so much for that, I'll make a port for my game, Moon Cheeser as well since 3.1 supports GLES2 again. This will for sure help a lot

Congratulations, this post was rewarded with a SteemGC Upvote!

Want to meet fellow Steemit gamers and earn upvotes yourself? Join the SteemGC Discord channel!

Hi cloudif,

This post has been upvoted by the Curie community curation project and associated vote trail as exceptional content (human curated and reviewed). Have a great day :)

Visit curiesteem.com or join the Curie Discord community to learn more.


Your post was mentioned in the Steemit Hit Parade for newcomers in the following categories:Congratulations @cloudif!

  • Upvotes - Ranked 8 with 530 upvotes
  • Pending payout - Ranked 1 with $ 36

I also upvoted your post to increase its reward
If you like my work to promote newcomers and give them more visibility on Steemit, consider to vote for my witness!

Thank you for sharing you game development post with us! You have received an upvote! Please visit our page @steemgg to learn more about how you can use our platform Steemgg, the first html5 gaming platform built on the Steem Blockchain to get more out of game development.

Congratulations @cloudif! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

<p dir="auto"><a href="http://steemitboard.com/@cloudif" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link"><img src="https://images.hive.blog/768x0/https://steemitimages.com/70x80/http://steemitboard.com/notifications/payout.png" srcset="https://images.hive.blog/768x0/https://steemitimages.com/70x80/http://steemitboard.com/notifications/payout.png 1x, https://images.hive.blog/1536x0/https://steemitimages.com/70x80/http://steemitboard.com/notifications/payout.png 2x" /> Award for the total payout received <p dir="auto"><sub><em>Click on the badge to view your Board of Honor.<br /> <sub><em>If you no longer want to receive notifications, reply to this comment with the word <code>STOP <p dir="auto"><strong><span>Do not miss the last post from <a href="/@steemitboard">@steemitboard: <table><tr><td><a href="https://steemit.com/spanish/@steemitboard/presentamos-el-ranking-de-steemitboard" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link"><img src="https://images.hive.blog/768x0/https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmfRVpHQhLDhnjDtqck8GPv9NPvNKPfMsDaAFDE1D9Er2Z/header_ranking.png" srcset="https://images.hive.blog/768x0/https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmfRVpHQhLDhnjDtqck8GPv9NPvNKPfMsDaAFDE1D9Er2Z/header_ranking.png 1x, https://images.hive.blog/1536x0/https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmfRVpHQhLDhnjDtqck8GPv9NPvNKPfMsDaAFDE1D9Er2Z/header_ranking.png 2x" /><td><a href="https://steemit.com/spanish/@steemitboard/presentamos-el-ranking-de-steemitboard" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Presentamos el Ranking de SteemitBoard <blockquote> <p dir="auto">Support <a href="https://steemit.com/@steemitboard" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">SteemitBoard's project! <strong><a href="https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1" target="_blank" rel="noreferrer noopener" title="This link will take you away from hive.blog" class="external_link">Vote for its witness and <strong>get one more award!

Congratulations @cloudif! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

SteemitBoard supports the SteemFest⁴ Travel Reimbursement Fund.
Vote for @Steemitboard as a witness to get one more award and increased upvotes!