<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Klaudia Romek</title>
    <description>The latest articles on Forem by Klaudia Romek (@k_romek).</description>
    <link>https://forem.com/k_romek</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F399828%2Fe6f333d8-cfab-4a9f-8087-8acc6a168d08.jpg</url>
      <title>Forem: Klaudia Romek</title>
      <link>https://forem.com/k_romek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/k_romek"/>
    <language>en</language>
    <item>
      <title>OCR + SwiftUI + Japanese. Quite a training project! 😅</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Tue, 19 Mar 2024 19:49:22 +0000</pubDate>
      <link>https://forem.com/k_romek/ocr-swiftui-japanese-quite-a-training-project-3feg</link>
      <guid>https://forem.com/k_romek/ocr-swiftui-japanese-quite-a-training-project-3feg</guid>
      <description>&lt;p&gt;Hi! It’s been a while! I’ve been learning a lot of new things lately. I came up with some OCR and AI related projects at work, and have been exploring SwiftUI and Flutter in more depth in my spare time. As I’m reaching a big milestone in development of One a Day (my gratitude and positivity platform), I decided it’s time for a mini-project-sized break! :D&lt;/p&gt;

&lt;p&gt;I like to code useful things, so rather than create a yet another to-do-list app, I wondered what I could make in a short time that would provide value. I’ve had fun with AI and OCR at work, and wanted to dive deeper into it, whilst also sticking to mobile apps. I remembered I have a very real pain point I haven’t been able to resolve with any existing solutions. &lt;/p&gt;

&lt;h2&gt;
  
  
  I﻿t's a struggle
&lt;/h2&gt;

&lt;p&gt;When I (try to) read Japanese books or manga, I often have a problem of not being able to fully understand certain kanji, words, or sentences. I can easily translate them using Google Translate, but that doesn’t give me a kanji-by-kanji breakdown needed to fully grasp sentence structure. I want to learn, not just translate. I have access to great dictionaries and grammar resources, but they’re not very useful when I don’t even know how to find the problematic kanji, because I don’t know its reading. What I often end up doing is to translate with the Google app, switch to original text, copy kanji, paste into dictionary, get all the necessary information, add it to flash cards (if I get that far). Quite a lot of steps that quickly kill any bit of motivation I have!&lt;/p&gt;

&lt;h2&gt;
  
  
  A﻿ shotgun to kill a fly
&lt;/h2&gt;

&lt;p&gt;Here comes my project idea then! Create an app that will help me with reading native Japanese texts without the need for all those different tools. &lt;/p&gt;

&lt;p&gt;Feature requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Take a picture / upload a picture of Japanese writing&lt;/li&gt;
&lt;li&gt;Implement OCR technology to read text from the images&lt;/li&gt;
&lt;li&gt;Display this generated text with furigana to support me with reading&lt;/li&gt;
&lt;li&gt;Implement a translation feature for selected words&lt;/li&gt;
&lt;li&gt;Add option to save vocabulary for future study&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some quick designs from my other half to solidify the concept and kick off the project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flw47bzvjlp6e72gogm90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flw47bzvjlp6e72gogm90.png" alt="Image description" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4b4x8b5y09vgg2tcor4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo4b4x8b5y09vgg2tcor4.png" alt="Image description" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfazc5e132dqpbzk667l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfazc5e132dqpbzk667l.png" alt="Image description" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial investigation / Expected problems
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Even though the OCR APIs I’m familiar with (such as Pen to Print) were more than capable to read single words, they were not able to read vertical, right-to-left writing. This is going to be trickier than I initially assumed, and I will have to test more solutions. Shortlisted Azure AI Vision, and Google Vision, though the setup and pricing might be a bit of an overkill for a small side project. OCR.Space seems promising and the first few requests returned good results, though I did notice some small mistakes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There seem to be plenty of Japanese text analysers, dictionary packages, and learning software so I naively assumed I would have no problems finding a furigana generator API. Wrong! Initial search left me with a few excellent web tools, which I could scrape, some python based open source API projects which I could translate and host myself, and a very capable, albeit slow ChatGPT solution I quickly put together. I’ll need to research this some more, although I’m leaning heavily towards AI at the moment, as it would allow me to achieve the POC very quickly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Furigana requires using ruby characters, which complicates the matter of displaying it in a mobile app. From what I’ve seen this can be achieved using attributed strings with ruby annotations, though not without issues. Moreover, this is not supported by SwiftUI by default and I’ll have to look into some custom code magic to bring this to life.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Translation feature should be achievable with the use of openly available XML dictionaries (JMdict for word meaning and KanjiDic for more in depth information on kanji). This is honestly such a relief to be able to access dictionary data so easily, especially after the struggles of having to implement Easy Polish News (my other language learning app) through web scraping!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Not giving up
&lt;/h2&gt;

&lt;p&gt;That’s pretty much it for now! I should have everything I need to get this working, although it will be a bit more difficult than I initially assumed. Still, it’s been fun researching all these concepts and thinking about a language from a technical point of view again. I’m looking forward to working on this. Hopefully I’ll be able to make something that will finally help me tackle those books!&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>learning</category>
      <category>japanese</category>
      <category>mobile</category>
    </item>
    <item>
      <title>I’ve had enough. Bye, Xamarin!</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Mon, 21 Aug 2023 17:31:46 +0000</pubDate>
      <link>https://forem.com/k_romek/ive-had-enough-bye-xamarin-368n</link>
      <guid>https://forem.com/k_romek/ive-had-enough-bye-xamarin-368n</guid>
      <description>&lt;p&gt;&lt;strong&gt;My first job revolved around building a Xamarin Native app. Every full-time role I had afterwards focused mainly on Xamarin Forms. I’ve worked with other technologies, sure, but I always introduced myself as a Full Stack Developer specialising in Xamarin. I know it well and can make things quickly. Or at least I thought so…&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Writing code in Xamarin.Forms is quite pleasant. I love C#, and I find XAML very easy to work with. Throughout the years I’ve come to know all the useful packages and tricks to bypass some of the framework quirks. It is a smooth and enjoyable process… until you press that Debug button😅.&lt;/p&gt;

&lt;p&gt;I use a Mac, so perhaps this problem isn’t something Windows users experience, but I find Android Emulators horrible to work with. They’re slow and take forever to launch. Using a device was always much easier. The same can’t be said about iOS, where simulators are generally easy to work with, but you can’t use them for everything. Things such as push notifications, or Bluetooth connection require a physical device, which in turn needs the provisioning profiles and certificates set up. It’s completely automatic in XCode, but VS never caught up, making things doable, but a bit of a pain.&lt;/p&gt;

&lt;p&gt;Build and deploy times in XF are generally long, but the worst part is when after an eternity of waiting you see an error that has nothing to do with the code you just changed. Just the environment being hostile 😂. If you didn’t rebuild, restart, or clear the cache/project at least 10 times, did you even work today?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3hrrav8rnzzemlcr0xm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3hrrav8rnzzemlcr0xm.jpg" alt="Frustrated developer with a laptop" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Truth to be told, it wasn’t always that bad, but I found these issues getting worse in the past year or two. On top of that, every VS or Xamarin update brought a feeling of dread, as it was very likely your project would break completely for the next couple of weeks if you only decided to download the new version. It’s become the norm in the community to avoid the updates and stay a couple of versions behind for fear of losing productivity.&lt;/p&gt;

&lt;p&gt;You can’t always avoid the latest versions, however. I bought a new Mac a while ago to, ironically, be more productive on my projects. I installed MacOS Ventura, the latest XCode and Visual Studio, cloned my repos and ran one of my apps on a simulator. All worked fine, and I spent the next two weeks introducing new updates and features to the project. The last of them had to do with push notifications, so I plugged in my iPhone, and… error. I couldn’t debug on the device. Not iOS, not Android. &lt;/p&gt;

&lt;p&gt;I spent the next two weeks researching the errors and trying to make things work. Combing through SO, Github, and all articles I could find. Tinkering with settings, recloning repos, resetting, restarting, and reinstalling. Nothing. I spent one day wiping off my Mac and setting up everything from scratch. Still no luck.&lt;/p&gt;

&lt;p&gt;I won’t be going into the specifics of the errors I was facing, as this is not the focus of this &lt;del&gt;rant&lt;/del&gt; blog. Suffice it to say there is an open issue on &lt;a href="https://github.com/dotnet/maui/issues/15542" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and after over two months it’s still not been resolved. This caused me a lot of headaches and stress on a hobby project. I can’t imagine people experiencing it on commercial apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  E﻿nter Flutter(Flow)
&lt;/h3&gt;

&lt;p&gt;Coincidentally, I had been exploring the world of no-code and low-code builders at work at that time and had just finished a small project in FlutterFlow. I liked this tool a lot as it allows you to download code and carry on with your work in Flutter. I had wanted to get into Flutter for a while now and the app I was trying to update was quite simple. I decided it was a good opportunity to attempt a rewrite.&lt;/p&gt;

&lt;p&gt;It took me about three afternoons to rewrite the app to the state matching the feature set of the live Xamarin version. Another three to add the changes I wanted in the update. Things that took me days to figure out in Xamarin worked out of the box in Flutter. I downloaded the code and carried on with the release stuff and things unavailable in the builder. This was probably the hardest as both frameworks are very different, but it’ll get easier with time. What’s most important is that even with the initial learning curve, the rewrite took me pretty much the same amount of time as trying to make Xamarin work (and failing)!&lt;/p&gt;

&lt;p&gt;With both iOS and Android updates now live in stores, I can’t help but feel silly for “sticking to what I know” for so long. Part of it was the genuine belief that I am much more productive working in tech I am familiar with, which is true enough, but the other part was probably just my brain rejecting change and the discomfort that it brings. This is normal for most of us, but learning to notice it will surely make life easier in the future.&lt;/p&gt;

&lt;p&gt;Lesson learned. New skills unlocked. I fell out of love with mobile development lately, but with all those recent experiences I am now looking at my apps with excitement again. Knowing that a simple update won’t take forever made the previously shelved projects feasible. The spark’s come back and it feels as good as ever🔥&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>xamarinforms</category>
      <category>flutter</category>
      <category>lowcode</category>
    </item>
    <item>
      <title>Just launched my mental health app</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Mon, 06 Feb 2023 12:41:56 +0000</pubDate>
      <link>https://forem.com/k_romek/just-launched-my-mental-health-app-4if4</link>
      <guid>https://forem.com/k_romek/just-launched-my-mental-health-app-4if4</guid>
      <description>&lt;p&gt;Hi everyone, I wanted to share my latest project and talk about the reasons for making it.&lt;/p&gt;

&lt;p&gt;One a Day is a gratitude and positivity platform for people struggling with their mental health. It offers a safe space where we can practice gratitude and celebrate our achievements, no matter how small. &lt;/p&gt;

&lt;p&gt;When dealing with mental health issues, we encounter problems, which we think should be trivial to solve. We get frustrated thinking "Why am I like this!?" or we compare ourselves to others thinking "This should be easy, everyone can do it". Even when we manage to do something hard, we often don't give ourselves credit, but rather shoot ourselves down thinking "It wasn't that hard" or "I should have done this a long time ago".&lt;/p&gt;

&lt;p&gt;These thoughts contributes to further feelings of shame, guilt and isolation. We don't appreciate ourselves for the good things. We feel unworthy of praise. We don't want to open up to people thinking they wouldn't understand, or worse, they'd scoff at us. We keep fuelling our depression, anxiety, eating disorders, you name it. &lt;/p&gt;

&lt;p&gt;I've been there. To be honest, I still am. This is why I decided to build One a Day. To find others like me. To help them and to get help myself. To tackle those issues by building a community where we all support each other on our path to recovery by practicing gratitude and making that One small step a Day. &lt;/p&gt;

&lt;p&gt;If you were so kind as to give my app a go, and let me know your thoughts, whether it’s around the idea, the technical aspects, usability, or anything else, I would be extremely grateful. Or if it's not for you, perhaps you know someone who would benefit from it. In that case, please pass it on.&lt;/p&gt;

&lt;p&gt;I hope it can grow into something meaningful.&lt;/p&gt;

&lt;p&gt;Thank you!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.oad.oneaday" rel="noopener noreferrer"&gt;S﻿ee on Play Store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.apple.com/app/one-a-day-gratitude-platform/id1662212197" rel="noopener noreferrer"&gt;See on App Store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.oneadayapp.com/" rel="noopener noreferrer"&gt;S﻿ee product page&lt;/a&gt;&lt;/p&gt;

</description>
      <category>announcement</category>
      <category>devto</category>
      <category>webmonetization</category>
      <category>web3</category>
    </item>
    <item>
      <title>My new Xamarin app - Easy Polish News</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Thu, 15 Jul 2021 16:01:07 +0000</pubDate>
      <link>https://forem.com/k_romek/my-new-xamarin-app-easy-polish-news-4c4c</link>
      <guid>https://forem.com/k_romek/my-new-xamarin-app-easy-polish-news-4c4c</guid>
      <description>&lt;p&gt;I recently finished working on the MVP version of my latest application - Easy Polish News and published it in stores. It was an interesting training project so I thought it might be good  to share more about the app and how I made it.&lt;/p&gt;

&lt;p&gt;The idea for Easy Polish News came from a similar application I was using to study Japanese, where you could read news in Japanese and get immediate translation for the unknown vocabulary. I thought it would also be a good training project, as it would allow me to try out Azure functions, which is something I had never used before. On that note, I started planning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The back-end
&lt;/h2&gt;

&lt;p&gt;The reason to use Azure functions was that the application was extremely simple and did not need a complicated solution. I would also be able to enhance the featureset easily and add more functions if necessary. Initially however, I only needed two functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timer triggered function to periodically update the news database&lt;/li&gt;
&lt;li&gt;HTTP triggered function used by the app to request news for a specific day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, I wanted to use an existing API to fetch the latest news from Polish websites, however I quickly found that there were very few solutions which would allow me to do that, and most of them were very expensive. In the end, there was only one API I could use, but I quickly realised the quality of the news was questionable. I was receiving a lot of irrelevant articles from silly websites which were in no way useful nor interesting. I realised I needed to create my own solution. &lt;/p&gt;

&lt;p&gt;The way I approached it is by using an RSS feed from one of the most popular news providers in Poland. The feed allowed me to get a list of titles and URLs to the most recent articles. They also have different feeds for specific categories such as sports, politics and so on, which leaves some room for potential new features in the future.&lt;/p&gt;

&lt;p&gt;Using the URLs, I could then scrape the important information using XPATH queries. The HTMLAgilityPack nuget package came in very handy for that purpose. Scraping this information is not really a reliable solution, as my code will stop working the moment the website’s HTML DOM structure changes (OR if they even change elements’ ids), but with no alternatives in sight I decided this was my best option and it was also a good opportunity to learn about XPATH queries (which was surprisingly fun!).&lt;/p&gt;

&lt;p&gt;Having all of the necessary information, I could start building a small news database using Azure Blob Storage. My own news feed would then be retrieved by the app using the HTTP triggered function.&lt;/p&gt;

&lt;h2&gt;
  
  
  The application
&lt;/h2&gt;

&lt;p&gt;The application is written using Xamarin Forms and consists of three screens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Newsfeed page&lt;/li&gt;
&lt;li&gt;Article page&lt;/li&gt;
&lt;li&gt;Notebook page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The newsfeed page is probably the most self explanatory, as it is simply displaying the latest news. I also implemented infinite scrolling using Xamarin Forms’ CollectionView where I simply request news for the previous day when the user reaches the end of their current content. There is also a search bar which allows for simple filtering of the fetched articles. In the future, I want to add the previously mentioned categories, which will allow users to choose the content they’re most interested in.&lt;/p&gt;

&lt;p&gt;The article page is the most complex one as it features a custom selectable label (implementation described in detail &lt;a href="https://dev.to/k_bronowicka/selectable-text-with-no-context-menu-in-xamarin-forms-3bmc"&gt;here&lt;/a&gt;) to allow the users to select words they do not understand. I then display a view where I show the base form of the word (because Polish is fun like that) and its English translation. Both of these are obtained by scraping dictionary sites, as, again, there was no useful API available. In the future, I would like to build my own dictionary database so I don’t need to rely on third parties and fickle solutions. &lt;/p&gt;

&lt;p&gt;The users can choose to save chosen vocabulary which is then displayed on the notebook page. The users can then study and review new words and remove them if needed. It would be a nice addition to have some sort of flashcard or spaced repetition system to help the users test themselves better.&lt;/p&gt;

&lt;p&gt;To top it all off I also added notifications to show users new articles and remind them to study! &lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Easy Polish News was a fun little project to work on and allowed me to learn a lot of new things from Azure through web scraping all the way to custom, OS specific components in Xamarin. As fun as it was, it would be fantastic if it actually benefited someone in their studies so let’s see how that works out :)&lt;/p&gt;

&lt;p&gt;If you want to have a look at the app, you can download it on the &lt;a href="https://apps.apple.com/us/app/easy-polish-news/id1573109935" rel="noopener noreferrer"&gt;App Store&lt;/a&gt; or &lt;a href="https://play.google.com/store/apps/details?id=com.klaudia.easypolishnews" rel="noopener noreferrer"&gt;Play Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5w2dam34mrxb67rqru1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5w2dam34mrxb67rqru1o.png" alt="Alt Text" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>xamarinforms</category>
      <category>mobile</category>
      <category>project</category>
    </item>
    <item>
      <title>Selectable text with no context menu in Xamarin Forms</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Sat, 24 Apr 2021 14:55:11 +0000</pubDate>
      <link>https://forem.com/k_romek/selectable-text-with-no-context-menu-in-xamarin-forms-3bmc</link>
      <guid>https://forem.com/k_romek/selectable-text-with-no-context-menu-in-xamarin-forms-3bmc</guid>
      <description>&lt;p&gt;Recently I've been working on an app where users can read articles in a foreign language and get an immediate translation of the words they select. Unfortunately, it turns out that text selection isn't supported out of the box in Xamarin Forms so I needed to do some digging.&lt;/p&gt;

&lt;p&gt;First of all, I found these two great articles explaining how to create selectable labels:&lt;/p&gt;

&lt;p&gt;- &lt;a href="https://medium.com/@HeikkiDev/selectable-label-on-xamarin-forms-9b050267bf8e" rel="noopener noreferrer"&gt;Article by @HeikkiDev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;- &lt;a href="https://medium.com/@anna.domashych/selectable-read-only-multiline-text-field-in-xamarin-forms-69d09276d580" rel="noopener noreferrer"&gt;Article by @anna.domashych&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's almost what I needed but the default selection functionality shows a context menu allowing the user to copy text etc. In my application, I wanted to handle the selected word on my own, without showing the menu. Here is how to do it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared code
&lt;/h3&gt;

&lt;p&gt;First of all, we need to define a custom control in our shared project. It will support basic text properties and a bindable command to get the currently selected word.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class SelectableLabel : View
    {
        public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(SelectableLabel), default(string));
        public static readonly BindableProperty TextColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(SelectableLabel), Color.Black);
        public static readonly BindableProperty FontAttributesProperty = BindableProperty.Create(nameof(FontAttributes), typeof(FontAttributes), typeof(SelectableLabel), FontAttributes.None);
        public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(nameof(FontSize), typeof(double), typeof(SelectableLabel), -1.0);

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public Color TextColor
        {
            get { return (Color)GetValue(TextColorProperty); }
            set { SetValue(TextColorProperty, value); }
        }

        public FontAttributes FontAttributes
        {
            get { return (FontAttributes)GetValue(FontAttributesProperty); }
            set { SetValue(FontAttributesProperty, value); }
        }

        [TypeConverter(typeof(FontSizeConverter))]
        public double FontSize
        {
            get { return (double)GetValue(FontSizeProperty); }
            set { SetValue(FontSizeProperty, value); }
        }

        public static readonly BindableProperty OnTextSelectedCommandProperty =
            BindableProperty.Create(nameof(OnTextSelectedCommand), typeof(ICommand), typeof(SelectableLabel), null);

        public ICommand OnTextSelectedCommand
        {
            get { return (ICommand)GetValue(OnTextSelectedCommandProperty); }
            set { SetValue(OnTextSelectedCommandProperty, value); }
        }

        public static void Execute(ICommand command, object parameter)
        {
            if (command == null) return;
            if (command.CanExecute(parameter))
            {
                command.Execute(parameter);
            }
        }

        public Command&amp;lt;string&amp;gt; OnTextSelected =&amp;gt; new Command&amp;lt;string&amp;gt;((s) =&amp;gt; Execute(OnTextSelectedCommand, s));
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS
&lt;/h3&gt;

&lt;p&gt;On iOS, we enable selection by setting UITextView's properties &lt;code&gt;Editable = false&lt;/code&gt; and &lt;code&gt;Selectable = true&lt;/code&gt;. In order to remove the context menu, we have to subclass UITextView and override its &lt;code&gt;CanPerform(Selector action, NSObject withSender)&lt;/code&gt; function to return &lt;strong&gt;false&lt;/strong&gt;. I also added a callback to send the selected word back to the shared code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[assembly: ExportRenderer(typeof(SelectableLabel), typeof(XFSelectableLabel.iOS.Renderers.SelectableLabelRenderer))]
namespace XFSelectableLabel.iOS.Renderers
{
    public class SelectableLabelRenderer : ViewRenderer&amp;lt;SelectableLabel, UITextView&amp;gt;
    {
        CustomTextView uiTextView;

        protected override void OnElementChanged(ElementChangedEventArgs&amp;lt;SelectableLabel&amp;gt; e)
        {
            base.OnElementChanged(e);

            var label = (SelectableLabel)Element;
            if (label == null)
                return;

            if (Control == null)
            {
                uiTextView = new CustomTextView(Element.OnTextSelected.Execute);
            }

            uiTextView.Selectable = true;
            uiTextView.Editable = false;
            uiTextView.ScrollEnabled = false;
            uiTextView.TextContainerInset = UIEdgeInsets.Zero;
            uiTextView.TextContainer.LineFragmentPadding = 0;
            uiTextView.BackgroundColor = UIColor.Clear;

            // Initial properties Set
            uiTextView.Text = label.Text;
            uiTextView.TextColor = label.TextColor.ToUIColor();
            switch (label.FontAttributes)
            {
                case FontAttributes.None:
                    uiTextView.Font = UIFont.SystemFontOfSize(new nfloat(label.FontSize));
                    break;
                case FontAttributes.Bold:
                    uiTextView.Font = UIFont.BoldSystemFontOfSize(new nfloat(label.FontSize));
                    break;
                case FontAttributes.Italic:
                    uiTextView.Font = UIFont.ItalicSystemFontOfSize(new nfloat(label.FontSize));
                    break;
                default:
                    uiTextView.Font = UIFont.BoldSystemFontOfSize(new nfloat(label.FontSize));
                    break;
            }

            SetNativeControl(uiTextView);
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == SelectableLabel.TextProperty.PropertyName)
            {
                if (Control != null &amp;amp;&amp;amp; Element != null &amp;amp;&amp;amp; !string.IsNullOrWhiteSpace(Element.Text))
                {
                    uiTextView.Text = Element.Text;
                }
            }
            else if (e.PropertyName == SelectableLabel.TextColorProperty.PropertyName)
            {
                if (Control != null &amp;amp;&amp;amp; Element != null)
                {
                    uiTextView.TextColor = Element.TextColor.ToUIColor();
                }
            }
            else if (e.PropertyName == SelectableLabel.FontAttributesProperty.PropertyName
                        || e.PropertyName == SelectableLabel.FontSizeProperty.PropertyName)
            {
                if (Control != null &amp;amp;&amp;amp; Element != null)
                {
                    switch (Element.FontAttributes)
                    {
                        case FontAttributes.None:
                            uiTextView.Font = UIFont.SystemFontOfSize(new nfloat(Element.FontSize));
                            break;
                        case FontAttributes.Bold:
                            uiTextView.Font = UIFont.BoldSystemFontOfSize(new nfloat(Element.FontSize));
                            break;
                        case FontAttributes.Italic:
                            uiTextView.Font = UIFont.ItalicSystemFontOfSize(new nfloat(Element.FontSize));
                            break;
                        default:
                            uiTextView.Font = UIFont.BoldSystemFontOfSize(new nfloat(Element.FontSize));
                            break;
                    }
                }
            }
        }

        public class CustomTextView : UITextView
        {
            private Action&amp;lt;string&amp;gt; _callback;

            public CustomTextView(Action&amp;lt;string&amp;gt; callback)
            {
                _callback = callback;
            }

            public override bool CanPerform(Selector action, NSObject withSender)
            {
                var word = Text.Substring((int)SelectedRange.Location, (int)SelectedRange.Length);

                _callback(word);

                return false;
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Android
&lt;/h3&gt;

&lt;p&gt;To make Android TextView selectable we need to call &lt;code&gt;textView.SetTextIsSelectable(true)&lt;/code&gt;. This should be enough, however there might be an issue of it not working where you will see an error in the console log saying "&lt;em&gt;TextView does not support text selection. Selection cancelled.&lt;/em&gt;" This seems to be an Android bug, which you can work around by overriding &lt;code&gt;OnAttachedToWindow()&lt;/code&gt; function in your renderer code like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;protected override void OnAttachedToWindow()
{
    base.OnAttachedToWindow();
    textView.Enabled = false;
    textView.Enabled = true;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to remove the context menu, we need to create our own &lt;code&gt;CustomSelectionActionModeCallback&lt;/code&gt; and overwrite its &lt;code&gt;OnActionItemClicked&lt;/code&gt;and &lt;code&gt;OnPrepareActionMode&lt;/code&gt; functions. The whole renderer code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[assembly: ExportRenderer(typeof(SelectableLabel), typeof(XFSelectableLabel.Droid.Renderers.SelectableLabelRenderer))]

namespace XFSelectableLabel.Droid.Renderers
{
    public class SelectableLabelRenderer : ViewRenderer&amp;lt;SelectableLabel, TextView&amp;gt;
    {
        TextView textView;

        public SelectableLabelRenderer(Context context) : base(context)
        {
        }

        protected override void OnElementChanged(ElementChangedEventArgs&amp;lt;SelectableLabel&amp;gt; e)
        {
            base.OnElementChanged(e);

            var label = (SelectableLabel)Element;

            if (label == null) return;

            if (Control == null)
            {
                textView = new TextView(this.Context);
            }

            textView.Enabled = true;
            textView.SetTextIsSelectable(true);
            textView.DefaultFocusHighlightEnabled = true;

            textView.CustomSelectionActionModeCallback = new CustomSelectionActionModeCallback(TextSelected);

            textView.Text = label.Text;

            textView.SetTextColor(label.TextColor.ToAndroid());

            switch (label.FontAttributes)
            {
                case FontAttributes.None:
                    textView.SetTypeface(null, Android.Graphics.TypefaceStyle.Normal);
                    break;
                case FontAttributes.Bold:
                    textView.SetTypeface(null, Android.Graphics.TypefaceStyle.Bold);
                    break;
                case FontAttributes.Italic:
                    textView.SetTypeface(null, Android.Graphics.TypefaceStyle.Italic);
                    break;
                default:
                    textView.SetTypeface(null, Android.Graphics.TypefaceStyle.Normal);
                    break;
            }

            textView.TextSize = (float)label.FontSize;
            SetNativeControl(textView);
        }

        private void TextSelected()
        {
            var word = textView.Text[textView.SelectionStart..textView.SelectionEnd];

            Element.OnTextSelected.Execute(word);
        }

        protected override void OnAttachedToWindow()
        {
            base.OnAttachedToWindow();
            textView.Enabled = false;
            textView.Enabled = true;
        }

        private class CustomSelectionActionModeCallback : Java.Lang.Object, ActionMode.ICallback
        {
            private Action _selectionCallback;

            public CustomSelectionActionModeCallback(Action selectionCallback)
            {
                _selectionCallback = selectionCallback;
            }

            public bool OnActionItemClicked(ActionMode m, IMenuItem i) =&amp;gt; false;

            public bool OnCreateActionMode(ActionMode mode, IMenu menu) =&amp;gt; true;

            public void OnDestroyActionMode(ActionMode mode) { }

            public bool OnPrepareActionMode(ActionMode mode, IMenu menu)
            {
                menu?.Clear();
                _selectionCallback?.Invoke();

                return false;
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! All that's left now is to add the label into your view and bind its &lt;code&gt;OnTextSelectedCommand&lt;/code&gt; to get the selected word. An example of how to do it and the whole code is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/KlaudiaBronowicka/XFSelectableLabel" rel="noopener noreferrer"&gt;https://github.com/KlaudiaBronowicka/XFSelectableLabel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>xamarin</category>
      <category>xamarinforms</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>It doesn't have to be perfect</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Wed, 17 Feb 2021 13:38:20 +0000</pubDate>
      <link>https://forem.com/k_romek/it-doesn-t-have-to-be-perfect-21di</link>
      <guid>https://forem.com/k_romek/it-doesn-t-have-to-be-perfect-21di</guid>
      <description>&lt;h2&gt;
  
  
  “Just ship it!”
&lt;/h2&gt;

&lt;p&gt;I used to hear these words a lot from the business folks trying to convince devs (including me) that the product is good to go. “Crazy! What are they talking about!? Development takes time! It needs testing! It needs to be thorough! It can’t be just shipped unfinished!” I would think.&lt;/p&gt;

&lt;p&gt;I was wrong. The product definitely can be shipped unfinished. That’s because it rarely ever gets finished in the first place. There will always be bugs to fix and more features to add. Even when you tick off every single task from the backlog, your users will always come back with more suggestions. You don’t ever finish, you just decide when to step away.&lt;/p&gt;

&lt;p&gt;Most websites and apps we interact with keep getting updates and new features. It's normal that you release a product when it’s “good enough” and then you just keep working on it. We all know it, and yet when working on your own product, it’s so hard to let go of perfectionism.&lt;/p&gt;

&lt;h2&gt;
  
  
  MVP hype
&lt;/h2&gt;

&lt;p&gt;When developing a new product, MVP (Minimum Viable Product) is something you’ll be thinking about a lot. It is the minimum set of features required for the successful launch of your product. Thinking of Airbnb it would be everything that lets people browse, and book short-term accommodation. Nothing more. Nowadays, they offer much more, including experiences and online classes. But all of that came after they established their product. &lt;/p&gt;

&lt;p&gt;MVP might be one of those overhyped words in the software/business conversations, but there is a reason for it. MVP lets you focus. After the initial planning phase, it should be unchangeable. If you have an idea for a great new feature, you can add it to the backlog. Don’t change the scope or you’re risking postponing the launch. I’ve personally experienced what can happen without a concrete plan and trust me, it is not something I ever want to repeat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1581291518857-4e27b48ff24e%3Fixid%3DMXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%253D%26ixlib%3Drb-1.2.1%26auto%3Dformat%26fit%3Dcrop%26w%3D1350%26q%3D80" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1581291518857-4e27b48ff24e%3Fixid%3DMXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%253D%26ixlib%3Drb-1.2.1%26auto%3Dformat%26fit%3Dcrop%26w%3D1350%26q%3D80" alt="Hand holding a pen and drawing user journey diagrams" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Sinking dreams
&lt;/h2&gt;

&lt;p&gt;I used to work at a startup where we were developing a new, ambitious platform. It was one of those ”solve all your problems” cases, where we could see so much potential, we couldn't stop thinking of new ways to use it and new features to add. There was no MVP. Back then, I didn’t even know that word. We just wanted to build this app and we kept changing the plan as we went on.&lt;/p&gt;

&lt;p&gt;As you can imagine, it didn't go well. We would often run out of money and annoy the investors with more delays. We kept thinking “this one more feature will get things going” or "this functionality will get that company interested" and so we kept adding more to it. Even after the launch, the lack of focus and constant change of direction made it extremely difficult to stay afloat. After all, even having developed an awesome product, we had no money to market it properly and gain the traction we needed to keep going. &lt;/p&gt;

&lt;p&gt;Lesson learned. Don't spend months working on all the fancy features which will be rarely even used. Don't waste your time figuring out creative solutions for content filtering, when the initial amount of content will not even need to be filtered. Focus on what matters. Focus on what your product needs to be successful. After all, nobody will see those beautiful animations if you don’t have any users in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Love the feedback
&lt;/h2&gt;

&lt;p&gt;When working on a personal side project, we don’t need to be all that concerned with how long it’s going to take. We're doing it (most of the time) for fun so obviously, we want it to be exactly how we imagined. However, if you do eventually want to publish it, it’s for the best to avoid the scope creep hellhole. &lt;/p&gt;

&lt;p&gt;Last year I released my first indie game. It wasn’t too big, but it still took me about 2 years from the first concept to the release (and I was not working alone). Looking back, one of the biggest mistakes I made while working on this project was refusing to show it to anyone until I deemed it "good enough". Now, I’m a clinical perfectionist so "good enough" meant the game was pretty much finished when I gathered all my 2 grams of courage and shared the demo with the few people I knew would be kind to me even if the game sucked. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1587614313085-5da51cebd8ac%3Fixid%3DMXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%253D%26ixlib%3Drb-1.2.1%26auto%3Dformat%26fit%3Dcrop%26w%3D1350%26q%3D80" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1587614313085-5da51cebd8ac%3Fixid%3DMXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%253D%26ixlib%3Drb-1.2.1%26auto%3Dformat%26fit%3Dcrop%26w%3D1350%26q%3D80" alt="Three people working on a laptop together" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The feedback was great, so I shared it with more people. Everyone agreed the game was good. It wasn’t perfect, of course, and there was some criticism which I had to address. The problem was that the game’s code was so convoluted at that stage, that making any bigger changes proved to be very difficult. If a small change was going to take a week of work, I had to let it go. Otherwise, I would have never released the game.&lt;/p&gt;

&lt;p&gt;Had I asked for feedback from the very beginning, it could have been so much better! My fear of sharing unfinished work managed to sabotage the project. Also, a lot of time within those 2 years was spent on doubt, hesitation and trying to regain lost motivation. I’m pretty sure that if I had heard that positive feedback earlier on, I would have finished it much sooner.&lt;/p&gt;

&lt;h2&gt;
  
  
  It doesn’t have to be perfect
&lt;/h2&gt;

&lt;p&gt;Development is both a technical and creative process. It's impossible to have all the answers from the start, so it's almost impossible to avoid some scope creep. Of course, everyone wants their product to be as good as possible. However, we all need to ask ourselves how far we can go with it before it starts being harmful. When in doubt, ask your friends, family and community, but don't go overboard with extra features. Remember, it doesn’t have to be perfect.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>startup</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Getting that junior role</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Wed, 20 Jan 2021 13:08:14 +0000</pubDate>
      <link>https://forem.com/k_romek/getting-that-junior-role-24f9</link>
      <guid>https://forem.com/k_romek/getting-that-junior-role-24f9</guid>
      <description>&lt;p&gt;I started coding at the age of 19, right after high school. I was about to start my Computer Science degree and I was convinced that I would be the only person in the year who hadn’t been coding since childhood. Of course, that belief was completely wrong but I needed to wait a few years to realise how many amazing developers only learned how to code after 25, 35, or even 45. That said, my fear of sticking out (along with the newly found passion I found in coding) pushed me to learn a lot in quite a short time. This allowed me to land an intern role at a promising startup for my placement year, which turned into me dropping out of university to take the lead of the mobile team at the same company a year later. In that role, I interviewed many candidates who were in the same position I was not so long ago. Remembering how anxious I was when being evaluated by strangers, I didn’t want to make any mistakes or misjudge people. I spent a lot of time thinking and discussing what to look for in someone applying for that junior role. Now I want to share my thoughts with you.&lt;/p&gt;

&lt;h3&gt;
  
  
  I don’t care about your grades
&lt;/h3&gt;

&lt;p&gt;In the UK, we get to pause our studies for a year to do the industrial placement. This typically happens after the second year of university and allows the student to come back and finish their studies having gained important technical knowledge. All in all, it’s a great initiative that can really boost one’s career. However, being on the other side of the fence allowed me to see the weaknesses of certain universities in terms of preparing their students for this new chapter. Most of the applications I’d received were almost the same, with the same subjects learned, the same projects created and the same problems solved. Nobody had told them the importance of having a portfolio with their own work. As a result, all their work and knowledge were based on the exact same coursework their peers did. Even the top students very often had not much else to show apart from what they learned in class. None of those applications were successful.&lt;/p&gt;

&lt;p&gt;What I said might sound harsh. However, I want you to realise that relying purely on your course or degree to get the job of your dreams is probably not the way to go. You will be just like tens of other students on your course. Even with top grades, if you don’t go beyond your curriculum, your application will be just like many others. Without initiative, passion, and willingness to put some hard work in, it will be difficult to convince anyone that programming is truly what you want to do. Think of your course material as the foundation. It’s up to you what and how to build on it.&lt;/p&gt;

&lt;p&gt;One person I had the pleasure of working with did not even come from a computer science degree. He was doing a music course and learning programming in his own time. His portfolio and knowledge were much more impressive than many of the applications from students of one of the top universities in the country.&lt;/p&gt;

&lt;p&gt;Forget your grades. Make some awesome stuff.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show me your passion
&lt;/h3&gt;

&lt;p&gt;When I was hired, my boss told me the reason he offered me the job was not because I was the best candidate. He said my knowledge and experience were actually far from it. However, he decided to give me a chance because he saw how fast I’d learned what I knew and how excited I was about it all. During the interview, even when I didn’t know something, I would ask him what the correct answer was and why. We took much longer than expected because we ended up discussing many programming concepts and I was soaking in every piece of information he shared.&lt;/p&gt;

&lt;p&gt;He knew I loved what I was doing and this is why he took a chance on me. He bet that I would catch up to everyone else in no time and that I would keep growing within the team. He wasn’t wrong.&lt;/p&gt;

&lt;p&gt;This is the same kind of attitude I would look for when building my team. Extra projects, game jams, blogs, personal websites, active git profile, open-source work, YouTube channel, pretty much anything that shows that you love what you’re doing. Of course, you don’t need to do all of those things. Find what works for you, what brings you the most joy. Make sure those things stand out in your application. Show them your passion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fklaudiabronowicka.com%2Fimg%2Fphoto-1504384764586-bb4cdc1707b0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fklaudiabronowicka.com%2Fimg%2Fphoto-1504384764586-bb4cdc1707b0.jpeg" title="Hackathon, by Alex Kotliarskyi" alt="Man with a laptop, coding at a hackathon surrounded by other programmers" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Show me your portfolio
&lt;/h3&gt;

&lt;p&gt;I strongly believe that the single best thing you can do to boost your career is to have a solid portfolio. That said, you need to be smart about how to structure it, what to put on it, and how to present your work.&lt;/p&gt;

&lt;h4&gt;
  
  
  You’re only as good as your worst project
&lt;/h4&gt;

&lt;p&gt;I’m not sure who said it first as I see this rule pop up in various places very often and for a good reason. When it comes to your portfolio, always aim for quality over quantity. Three highly polished projects are much more impressive than ten unfinished prototypes. Only showcase the work you’re really proud of and skip the rest. You can always mention it during the interview if they ask for more examples of your work.&lt;/p&gt;

&lt;h4&gt;
  
  
  Be creative
&lt;/h4&gt;

&lt;p&gt;If you’ve been coding for a while you have most likely built a to-do list application. If not, you surely must have at least heard of it as a beginner’s project idea. I can certainly understand why it’s a good one to make, as it offers a way to practice a few very important programming concepts without being overly complicated. However, I would advise against putting it on your portfolio or at least against highlighting it as one of your most important projects. Why? Because you want to stand out. Showcasing a simple project most of the other programmers have done as well will not ‘wow’ anyone. Think about how to make this project unique by adding some unusual features or visuals. Or work on something completely different, something that only you would have thought of. You want to show that you’re creative and that you can think outside the box. Both of these are very important aspects of being a developer and can give you the edge many people tend to forget about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fklaudiabronowicka.com%2Fimg%2Fxiaole-tao-fo-hqulrgku-unsplash.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fklaudiabronowicka.com%2Fimg%2Fxiaole-tao-fo-hqulrgku-unsplash.jpg" title="Photo by Xiaole Tao" alt="Laptop, 3d printer and a number of 3d printed colourful models on a desk" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Be focused
&lt;/h4&gt;

&lt;p&gt;Following your passion and creating what you love is most often a great piece of advice to anyone creating a portfolio. After all, you want it to reflect your character and interests. Also, nobody likes to work on things they’re not interested in. However, you should make sure the projects are varied and clearly show your learning progression. For example, if you love memes, creating a meme generator app can be a fun project, but creating 14 more with just different themes doesn’t necessarily show your creativity and a focused approach to learning.&lt;/p&gt;

&lt;p&gt;I think it’s a very good idea to work on small projects which let you practice a specific tool, a programming concept, or simply prototype a feature of a much bigger project. Approaching it in this way will let you keep things interesting with a variety of projects to work on. It will also show the company you‘re applying to that you are independent and can successfully navigate your learning path.&lt;/p&gt;

&lt;p&gt;A great portfolio project I once saw was a small website where you could specify a challenge you wanted to take, such as exercising every day for a month. You would then pay a small fee to show your seriousness. You could only get your money back if you completed the challenge (a person of your choice vouched for you). The project was very small in terms of the number of screens and interaction, so it was a very good way to practice the basics of React. The payments were handled with Stripe, so it showed that the person was not only familiar with that service but also that they can integrate a 3rd party product into their project. Moreover, it showed a bit of humour and was a fun thing to discuss during the interview, which allowed us to see if we would enjoy working together or not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Be flexible and willing to learn
&lt;/h3&gt;

&lt;p&gt;From my experience, working as a software developer doesn’t mean you will be coding 8h a day. Your job will involve many different things, and as you progress in your career, programming itself will most likely become a smaller and smaller aspect of your work. First of all, you will have to work with a team of different people, front-end and back-end developers, designers, managers, QA testers etc. To work well within such a team it’s important to be able to communicate well and what certainly helps with that is having at least a bit of understanding of their jobs. That said, I would encourage you to explore different areas around your speciality. Show your potential employer that you’re flexible, willing to learn and that you will be a great team player. If your focus is on front-end development, create at least one full-stack project with a simple API. As you will be working very closely with designers, familiarize yourself with some more popular design systems and design tools (Adobe XD, Figma, etc.). Having a small project based on designs you created would be something very few people have on their portfolio and shows that you understand the importance of other roles within the business (and the importance of good design).&lt;/p&gt;

&lt;p&gt;To be clear, you certainly don’t need to become a full-stack developer before applying for your first job, but simply having an understanding and appreciation of other fields will be very helpful and shows your flexibility and willingness to learn. These characteristics are especially critical when applying to small companies and/or start-ups, where you will often have to go beyond your comfort zone and ‘get your hands dirty’ doing various new things. Don’t be afraid of this. This is how we grow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fklaudiabronowicka.com%2Fimg%2Fclark-tibbs-oqstl2l5oxi-unsplash.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fklaudiabronowicka.com%2Fimg%2Fclark-tibbs-oqstl2l5oxi-unsplash.jpg" title="Do something great, by Clark Tibbs" alt="Neon banner saying 'Do something great'" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Final thoughts
&lt;/h3&gt;

&lt;p&gt;Getting a software development job can be stressful, even after a few years in the industry. I certainly wouldn’t want to go back to that time where I was sending out dozens of applications only to be rejected before even getting a chance to talk to anyone. It can be depressing and demotivating and that’s normal. You need to remember that there are hundreds of other candidates and that it simply just is very difficult. That said, it’s not impossible so if you ever feel discouraged, just remember that it sometimes takes dozens of applications to get that dream job, so just keep going. Show them your passion, show them your work, be creative, focused, flexible and willing to learn. Keep growing as a developer and keep making awesome things. I hope 2021 will bring you that dream job!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by Tim Gouw&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>startup</category>
      <category>portfolio</category>
    </item>
    <item>
      <title>5 lessons learned from working with startups</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Sun, 03 Jan 2021 13:43:23 +0000</pubDate>
      <link>https://forem.com/k_romek/5-lessons-learned-from-working-with-startups-l5m</link>
      <guid>https://forem.com/k_romek/5-lessons-learned-from-working-with-startups-l5m</guid>
      <description>&lt;p&gt;A few years ago, I joined a startup where we were developing a new, very ambitious mobile app. Having spent there over two years, I left. It’s fair to say that things weren’t going great. Nowadays I’m working at a digital agency where we help companies of all sizes develop their apps and websites. I work specifically with clients who want to kick off their startup ideas and build an app from scratch. I help them with planning, design, and development. As I am now starting to build my own app, I wanted to recall the most important lessons learned from the mistakes I saw made in the past. &lt;/p&gt;

&lt;h2&gt;
  
  
  Focus on one user group
&lt;/h2&gt;

&lt;p&gt;Before jumping into developing your application, you’ll have to do some preparatory work like market research, competitor analysis, etc. You should have a good understanding of what you’re trying to achieve with your product, what problems you’re trying to solve, what other apps are already in this space, and who your client base is. Each one of these items deserves a separate article, but I want to focus on the last one - your target market. &lt;/p&gt;

&lt;p&gt;When planning your app it’s easy to say you want it to be used by people of all ages, genders, ethnicities, etc. After all, you want as many users as possible, right? You’d be shooting yourself in the foot with that approach. Think about marketing, how are you going to target every person on this planet? Unless you have a giant pile of money to throw into marketing, it’s going to be nearly impossible. Think about design and your feature set. How are you going to please people from very different groups with all their distinct needs? &lt;/p&gt;

&lt;p&gt;You need to have a consistent mission statement and aim to solve one problem for one group of people. Think about the best way to reach those people. Ideally, you want them to be so excited about your product, that they’re going to spread the word for you. After the release, you can target more groups and expand further. But in the beginning, just keep things simple. After all, you have limited time and resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stick to the plan
&lt;/h2&gt;

&lt;p&gt;As I said before, you should know exactly what your goal and the purpose of the product are. Before you start working, define what features are absolutely necessary to fulfill that goal - this is your Minimum Viable Product (MVP). Once you have that list of features, stick to it and don’t add anything unless it’s essential for the successful launch. It’s very tempting to keep adding functionality to the scope of the project, but don’t do it, for when you start, it’s very easy to fall down the rabbit hole of constant improvements and additions. &lt;/p&gt;

&lt;p&gt;For example, imagine you’re trying to create a movie rental app, where you can order a DVD/Blu-Ray straight to your house. It might be tempting to add some extra features. Maybe you could add comments/reviews so that people can share their thoughts about the movies they watched? That would be awesome! You could also add the ability to like those comments and maybe even reply to have proper conversations. That said, you could also have interest groups where people can discuss all the things movie-related. And also… Yeah, I think I’ll stop here. &lt;/p&gt;

&lt;p&gt;You can see how easy it is to steer away from your original idea, especially when you’re really excited about the product. However, each one of those features will take time and money to develop. If you don’t think that’s a problem, just think about it. People won’t wait forever for your product. Eventually, they’re going to get frustrated or simply forget about it. Or a competitor might release their own app and steal your prospective audience. Don’t get me started on investors and shareholders if you have them. Also, the longer you’re working, the more likely it is that you will burn out or simply lose interest. You should really aim to get that MVP to a beautiful stage and release it as soon as you can. It’s not going to be perfect, but you can keep building on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn from others
&lt;/h2&gt;

&lt;p&gt;When doing the initial competitor analysis, you might get discouraged to find that there are already very similar products out there. If they’re very popular, you might think there’s no more room in the market for your solution. On the other hand, if they’re not doing great, you might think there’s no demand for a product like this. Wrong. In both of these cases, you still have a chance for success, as long as you don’t just blindly copy what others have already done. Study their strengths and weaknesses. See what they did well, and what went wrong. Analyze, where you could improve their products or how to add some of your own, unique, approach. &lt;/p&gt;

&lt;p&gt;Whatever you find during the competitor analysis, don’t get discouraged. This is the time to redefine your solution and make sure you’re creating a solid product. Also, make sure you stay up to date with what your competitors are doing. Don’t get too hung up on it and turn into a crazy stalker, but make sure that every now and then you check how they’re doing and see if there is anyone new you should know about. Readjust your strategy if needed.&lt;/p&gt;

&lt;p&gt;Also, don’t be cocky. If you think your solution is the most innovative product that has never been done before, that’s great, but still, try to find what others have done in this space before you. Learn from their mistakes. Never dismiss others, no matter how small they are or how irrelevant you deem their experience/knowledge to be. Arrogance is your big enemy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don’t solve problems you don’t have
&lt;/h2&gt;

&lt;p&gt;When planning for the future, it’s easy to get ahead of yourself and worry about things that might never happen. Whilst it’s good to be prepared for any possibility, it’s just as important to know when to stop. You can’t solve all your future problems, so might as well limit the things you’re spending your brain energy on. Focus on the nearest future, like 6 months ahead (this may be shorter or longer, depending on the size of your project, of course). If something is not bound to happen before that time, just leave it be for now. Don’t worry about it and focus on the more pressing matters. Remember that stress makes you less productive, and more likely to burn out. It will also affect your team/family, whether you want it or not. &lt;/p&gt;

&lt;p&gt;Similar when it comes to development, don’t overcomplicate things. I personally love beautifully designed architecture with cleverly structured components and a lot of room for customization. However, you need to keep in mind the needs of your product. Whilst it’s definitely beneficial to design your application to be easily scalable in the future, it might not be useful to spend the time making it ready to handle millions of users simultaneously when you’re only bound to have tens or hundreds at first. Long story short, make sure you don’t spend too much time and energy overengineering things, as your biggest priority is to release your product. You can improve it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make a product, not a framework
&lt;/h2&gt;

&lt;p&gt;Staying with my point to keep things simple, make sure to check if the feature you’re working on has not been developed already. There are so many open-source libraries available on all platforms, you’re bound to find something for yourself. If you need something that’s not supported, make a pull request! It’s going to take less time than building the whole thing and you’ll add something from yourself. This is what open-source is for, after all. Only make things from scratch when you really need them (i.e. they’ve not been done before or you need a very custom solution). There’s no shame in using a ready-made component if it helps you build your product much faster.&lt;/p&gt;

&lt;p&gt;I know the case where someone spent months developing a custom, lightweight serialization tool instead of using one of the commonly available ones. It was great at first, but as the project grew, more features were needed and the maintenance became too costly. In the end, the team had to refactor the whole product to use a third-party tool. Imagine how much time they could have saved by doing that in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus lesson: Don’t burn out
&lt;/h2&gt;

&lt;p&gt;Writing software is hard. Of course, it can be extremely fun and exciting, but creating a fully-fledged application from scratch is not an easy task, especially when you’re all alone. That said, make sure to take care of yourself and your health. Don’t pull 14h workdays, because you WILL burn out at some point. Your body and your mind will start sabotaging you sooner than you think. Running a startup is a marathon, not a sprint so make sure you’re ready for it. Take breaks and rest days when you need them, as they will keep you motivated and efficient. Also, remember that whilst you can always delay the launch or find a job to support development, you can’t replace or buy health. So whatever you do, don’t burn out.&lt;/p&gt;

</description>
      <category>startup</category>
      <category>mobile</category>
      <category>career</category>
    </item>
    <item>
      <title>Making a Visual Novel with Unity (5/5) - Localization</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Tue, 22 Dec 2020 11:50:22 +0000</pubDate>
      <link>https://forem.com/k_romek/making-a-visual-novel-with-unity-5-5-localization-12b3</link>
      <guid>https://forem.com/k_romek/making-a-visual-novel-with-unity-5-5-localization-12b3</guid>
      <description>&lt;p&gt;When making your game you might think you only need it in one language. However, it’s always better to be able to reach a wider audience. You might decide you want to support multiple languages at some point, and it’s good to be prepared for that moment. You don’t want to be frantically looking for answers just before the release, after all (definitely not speaking from experience). That’s why we’ll look at how to implement a localization system into our game and how to make it work with ink. Let’s get to it!&lt;/p&gt;

&lt;h1&gt;
  
  
  I2Loc setup
&lt;/h1&gt;

&lt;p&gt;In order to save time (and keep your sanity) I very much recommend buying plugins and assets to speed up your development process. The one I used is &lt;a href="https://assetstore.unity.com/packages/tools/localization/i2-localization-14884" rel="noopener noreferrer"&gt;I2 Localisation&lt;/a&gt;, which seems to be one of the most popular and highly recommended solutions. A big reason why I chose it is that it offers synchronization with Google Sheets, which made things very easy for our team during development. Everyone could work on the same online document and all I had to do to update the game content was just to press one button (you can also automate and make it update the game with new document content AFTER it’s released which is also pretty cool).&lt;/p&gt;

&lt;p&gt;As I mentioned, you can link your game to an external Google spreadsheet. The I2Loc team has created an easy to follow tutorial on how to install the plugin and connect it to Google, which you can check out &lt;a href="http://inter-illusion.com/tools/i2-localization/how-to-link-with-google" rel="noopener noreferrer"&gt;here&lt;/a&gt;. There is also &lt;a href="http://inter-illusion.com/assets/I2LocalizationManual/I2LocalizationManual.html" rel="noopener noreferrer"&gt;full documentation&lt;/a&gt; if you need more information. What I want to focus on is how to actually use it within the game and, most importantly, how to localize our ink file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spreadsheet setup
&lt;/h2&gt;

&lt;p&gt;I2Loc (and pretty much any other localization system) works on a key-value basis. Each piece of text has its key, which we can use to find the text within the spreadsheet (or CSV file, if you’re using a different format). That’s why we’ll need a &lt;em&gt;Keys&lt;/em&gt; column along with one column for each language we want to support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqwu5o8hcle5pjtmz7gg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foqwu5o8hcle5pjtmz7gg.png" width="437" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An important thing to remember is that we can generate this spreadsheet content within the Unity editor, using the i2Loc asset, and then merge it with or overwrite the content in the sheet. As you can see below, there are various ways to handle the import/export of text. I preferred to do my edits in the spreadsheet, so I only used &lt;em&gt;Import Merge&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb7cp0xep1bxejer4gcqk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb7cp0xep1bxejer4gcqk.png" width="606" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you have a lot of text in your game, it’s a good idea to separate it into different sheets. We will use two for our project, one for UI elements and one for the story.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m93s8v89ukhl6yvy5i0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m93s8v89ukhl6yvy5i0.png" width="278" height="49"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  UI localization
&lt;/h1&gt;

&lt;p&gt;After you’ve set everything up, you should be able to see all your text within the i2Loc asset’s &lt;em&gt;Terms&lt;/em&gt; tab. On the right, you can see the name of the sheet the term is defined in. In this case, it’s &lt;em&gt;UI&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9d0l81qdlql220ux1jr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9d0l81qdlql220ux1jr.png" width="605" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click on a term, it will show its translations for each language. As I mentioned, if you notice a mistake, you can edit it right here and then export your changes to the spreadsheet. It also supports automated translation, which can be a good idea for simple things, like button text, or to do a quick, “dirty” translation before hiring someone for the job. I would definitely NOT recommend to use it in the actual release version.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidh3mmjwcib0c61zdv43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidh3mmjwcib0c61zdv43.png" width="605" height="633"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s localize our main menu. Select one of the button’s text elements and add the I2 Localize component to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq3m2pt000isol9yhmt1o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq3m2pt000isol9yhmt1o.png" width="605" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Terms/Term, select the key you want to use for this button. In my case, it was UI/NewGame. That’s all you need to do, the button will now be translated into the currently selected language when we press play. You’ll also see a bunch of options you can modify, such as uppercase/lowercase formatting under &lt;em&gt;Modifier&lt;/em&gt;. You can also choose to use a different font for each language, which can be defined under the &lt;em&gt;Secondary&lt;/em&gt; tab.&lt;/p&gt;

&lt;p&gt;Go ahead and set up the remaining buttons in the same way.&lt;/p&gt;

&lt;h1&gt;
  
  
  Language Switching
&lt;/h1&gt;

&lt;p&gt;Let’s see if the localization actually worked. On the i2 Languages asset, select the ‘Languages’ tab, and switch &lt;em&gt;Default language&lt;/em&gt; to &lt;em&gt;First in the List&lt;/em&gt;. Then, change the order of languages in your list and put the one you want to test on top.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq42rtkc94ot129ap3e14.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq42rtkc94ot129ap3e14.png" width="605" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go ahead and press play. Your scene should be now translated!&lt;/p&gt;

&lt;p&gt;If you want to change the language from the code, you can do it like so:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;I2.Loc.LocalizationManager.CurrentLanguage = "English";&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;I2.Loc.LocalizationManager.CurrentLanguage = "English (Canada)"; // Language and Variant&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can also set it using language codes, more information about it can be found &lt;a href="http://inter-illusion.com/assets/I2LocalizationManual/HowtochangecurrentLanguage.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszzn7gm8folr6zavrldx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszzn7gm8folr6zavrldx.png" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Ink localization
&lt;/h1&gt;

&lt;p&gt;Now onto the fun stuff, translating the story written in our ink file. There are many ways to approach this, we could, for example, have a different ink file per language. This could quickly become a headache though, as we would need to maintain several different files, which are, most likely, all edited by different people. What if you decide to change one line in the middle of the story after everything has been already translated? Or if you realize there was a mistake? You’ll need to update each one of those files separately. Can it be done? Sure it can, but in the case of a narrative game with thousands of lines of text, it’s a dangerous bet to make.&lt;/p&gt;

&lt;p&gt;I decided to follow a different strategy and make the google spreadsheet a master file. All changes to the original text would have to be done there and everyone would be working on the same copy. That way, everyone had access to the most up to date version and I could always import it into Unity with one click. Moreover, Google supports version control, so it is very easy to see any changes that were made and rollback to previous versions.&lt;/p&gt;

&lt;p&gt;Let’s talk about the actual implementation now. As you know, we need a key for each piece of text. A good way to do it would be to use hashtags and assign a unique key to each line within our story. If your game is very small or if you’re only starting it, this might be a good idea. However, if you (like me), want to add localization to an existing game, you might already have a lot of text in there. You probably don’t want to be adding tens of thousands of hashtags manually line by line.&lt;/p&gt;

&lt;p&gt;That said, we’re going to use knots and simple indexing for the keys. For each line of text we request from ink, it’s key is going to be a combination of its knot name and index, which will make every key unique and easy to create in code. First, we need to prepare the spreadsheet by copying all the text from the ink file. The end result is going to look like &lt;a href="https://docs.google.com/spreadsheets/d/1C4AoUPJV--EsRmtf8z5Shtp70ufHRKu1FKQOtSV1KtM/edit?usp=sharing" rel="noopener noreferrer"&gt;this&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmid130l6zepqk7qaumco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmid130l6zepqk7qaumco.png" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice how choice texts are also included in their respective knot section. Also, we don’t need the external function calls and hashtags in here, just the text. As for the indexing, you don’t need to type it all manually. Simply create the first key, press Shift and drag down from the bottom right corner to add the remaining keys like so:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiyshjhfhonkdswwyl9i6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiyshjhfhonkdswwyl9i6.gif" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that the spreadsheet is ready, let’s look at the code next. First, we’re going to add two new class-level variables to InkManager script:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;private int _currentLocIndex;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;private string _currentKnot;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;These variables will be updated with each line we request from ink and used to create its localization key. Given that our knot indexes start from 1, let’s make sure we initialize the value of &lt;code&gt;_currentKnot&lt;/code&gt; to be 1 as well, so add the following line in the &lt;code&gt;Start()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_currentLocIndex = 1;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s move onto the &lt;code&gt;DisplayNextLine()&lt;/code&gt; function. At the moment, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void DisplayNextLine()
{
  if (_story.canContinue)
  {
    string text = _story.Continue(); // gets next line

    text = text?.Trim(); // removes white space from text

    ApplyStyling();

    _textField.text = text; // displays new text
  }
  else if (_story.currentChoices.Count &amp;gt; 0)
  {
    DisplayChoices();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to create the localization key, we first need to find out the name of the current knot. We’re going to use this line of code:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var knot = _story.state.currentPathString?.Split('.')[0] ?? _currentKnot;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_story.state.currentPathString&lt;/code&gt; returns a string that’s formatted like so: &lt;code&gt;{knot_name}.{index}&lt;/code&gt;. The index is different for each line, and it seems to be a random number each time. That’s why we’re only using the first part of the string. If the path is not found for whatever reason, we’re using the &lt;code&gt;_currentKnot&lt;/code&gt; as a fallback.&lt;/p&gt;

&lt;p&gt;Next, we need to check if the knot for the current line is the same as the one we got for the previous line. If it’s not, it means we’ve moved into a new knot, so we need to reset &lt;code&gt;_currentKnot&lt;/code&gt; and &lt;code&gt;_currentLocIndex&lt;/code&gt; values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (_currentKnot != knot)
{

  // new knot, reset values
  _currentKnot = knot;

  _currentLocIndex = 1;

  Debug.Log($"NEW KNOT: {_currentKnot}");

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s create the key:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var key = $"{_currentKnot }_{_currentLocIndex}";&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And use it to request the localized version of the text:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var locText = LocalizationManager.GetTranslation($"Story/{key}");&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the path to get text uses the name of the sheet the key can be found in. In this case, it’s the &lt;code&gt;Story&lt;/code&gt; one.&lt;/p&gt;

&lt;p&gt;We now have our localized text, so we just need to update the rest of the function to use it. Also, we’ll need to increment the value of _currentLocIndex. The whole code should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void DisplayNextLine()
{
  if (_story.canContinue)
  {
    string text = _story.Continue(); // gets next line

    var knot = _story.state.currentPathString?.Split('.')[0] ?? _currentKnot;

    if (_currentKnot != knot)
    {
      // new knot, reset values
      _currentKnot = knot;

      _currentLocIndex = 1;

      Debug.Log($"NEW KNOT: {_currentKnot}");
    }

    var key = $"{_currentKnot }_{_currentLocIndex}";

    var locText = LocalizationManager.GetTranslation($"Story{key}");

    locText = locText?.Trim(); // removes white space from text

    ApplyStyling();

    _textField.text = locText; // displays new text

    _currentLocIndex++;

  }
  else if (_story.currentChoices.Count &amp;gt; 0)
  {
    DisplayChoices();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now get through the story in a different language!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjl0gbvdawvcb3fe7nr12.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjl0gbvdawvcb3fe7nr12.gif" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You might notice that the choice buttons are still in English though. We need to update our &lt;code&gt;DisplayChoices()&lt;/code&gt; function. This time, we don’t have to find the knot name, as we already know it. We simply request each choice text as with the rest of the story.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private void DisplayChoices()
{
  // check if choices are already being displayed
  if (_choiceButtonContainer.GetComponentsInChildren&amp;lt;Button&amp;gt;().Length &amp;gt; 0) return;

  for (int i = 0; i &amp;lt; _story.currentChoices.Count; i++)
  {
    var choice = _story.currentChoices[i];

    var key = $"{_currentKnot }_{_currentLocIndex}";

    var locText = LocalizationManager.GetTranslation($"Story/{key}");

    _currentLocIndex++;

    var button = CreateChoiceButton(locText);

    button.onClick.AddListener(() =&amp;gt; OnClickChoiceButton(choice));
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All done! When you hit play, the whole game should be now fully translated!&lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;Please be aware that the system introduced here works only with simple knots, where each choice immediately leads to a different knot. Because of the way indexing is done, having alternative, choice-based flows within one knot is not going to work. It might be counter-intuitive to separate everything like this in some cases, but doing it from the start makes it easy to handle localization with just a few lines of code.&lt;/p&gt;

&lt;p&gt;Also, please bear in mind that at this stage, the translation won’t work when you load the game. You’ll need to include &lt;code&gt;_currentLocIndex&lt;/code&gt; and &lt;code&gt;_currentKnot&lt;/code&gt; when saving/loading the game in GameStateManager. If you haven’t followed this series from the beginning, state management was covered in part 4.&lt;/p&gt;

&lt;p&gt;That’s it! We’ve covered how to use i2Loc to localize your game from within the Unity Editor as well as from the code. We’ve also discussed how to handle ink localization in a simple way. If you have any issues or questions, don’t hesitate to get in touch. And if you’ve been following this series from the start, I would love to see what you have created!&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All visual assets and sprites used in this tutorial are drawn by &lt;a href="https://twitter.com/squarecr0w" rel="noopener noreferrer"&gt;Erica Koplitz&lt;/a&gt; for the game &lt;a href="https://store.steampowered.com/app/1385950/Guilt_Free" rel="noopener noreferrer"&gt;Guilt Free&lt;/a&gt;, created together by me and Erica.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>beginners</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>Making a Visual Novel with Unity (4/5) - Variables and state management</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Tue, 15 Dec 2020 17:33:28 +0000</pubDate>
      <link>https://forem.com/k_romek/making-a-visual-novel-with-unity-4-5-variables-and-state-management-4b0d</link>
      <guid>https://forem.com/k_romek/making-a-visual-novel-with-unity-4-5-variables-and-state-management-4b0d</guid>
      <description>&lt;p&gt;We have learned the &lt;a href="https://dev.to/k_bronowicka/making-a-visual-novel-with-unity-1-5-introduction-to-ink-2i5b"&gt;basics of Ink&lt;/a&gt;, how to &lt;a href="https://dev.to/k_bronowicka/making-a-visual-novel-with-unity-2-5-integration-with-ink-256g"&gt;integrate your ink story with Unity&lt;/a&gt;, and how to &lt;a href="https://dev.to/k_bronowicka/making-a-visual-novel-with-unity3d-characters-and-emotions-47ag"&gt;use external functions&lt;/a&gt; to show and hide characters. This time we’re going to look at state handling in regards to variables and the game itself. As always, feel free to use the example files available &lt;a href="https://github.com/KlaudiaBronowicka/VisualNovelTutorial" rel="noopener noreferrer"&gt;here&lt;/a&gt;, or to work on your own game. Let’s get started!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The example project uses materials from the game &lt;a href="https://store.steampowered.com/app/1385950/Guilt_Free/" rel="noopener noreferrer"&gt;Guilt Free&lt;/a&gt;, which depicts someone struggling with an eating disorder. If you think this type of story may not be appropriate for you, please use your own files.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Variable state
&lt;/h2&gt;

&lt;p&gt;You might remember, that ink allows you to introduce variables and to change their value as the story progresses. This can be then used to display some specific, otherwise blocked parts of the story, such as various dialog options based on your charisma level, etc. In some cases, it might be useful to be able to access those values within our C# code, for example, to update the health bar when the player receives damage. The way to achieve this is by using the &lt;code&gt;variable_state&lt;/code&gt; property on the Story object, like so:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_story.variablesState["variable_name"]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You might have noticed that my ink file already has some variables which change as the story progresses. Let’s add some code to display their values when the game launches. We’ll need to update InkManager’s &lt;code&gt;Start()&lt;/code&gt; function to do that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;void Start()
{
  _characterManager = FindObjectOfType&amp;lt;CharacterManager&amp;gt;();

  StartStory();

  var relationshipStrength = (int)_story.variablesState["relationship_strength"];

  var mentalHealth = (int)_story.variablesState["mental_health"];

  Debug.Log($"Logging ink variables. Relationship strength: {relationshipStrength}, mental health: {mentalHealth}");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we can access all variables through &lt;code&gt;_story.variablesState&lt;/code&gt;. Make sure the variable names are exactly the same as the ones in ink. When you hit run, your console should be showing the values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22a6xrdgsdwvdj9vquuy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22a6xrdgsdwvdj9vquuy.png" width="563" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, we could put this code in a separate function which we could call at any point to check the current values, but how do we know when to check for it? Would we do it every 5 minutes? Every time we display a new line? Or maybe… No, that’s not going to work. It would be just great if we could have some kind of a listener, which would update the values whenever they were changed in ink, right?. Luckily for us, we can do exactly that!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_story.ObserveVariable("relationship_strength", (arg, value) =&amp;gt;
{
  Debug.Log($"Value updated. Relationship strength: {value}");
});

_story.ObserveVariable("mental_health", (arg, value) =&amp;gt;
{
  Debug.Log($"Value updated. Mental health: {value}");
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;arg&lt;/code&gt; parameter, in this case, will be the name of the variable which was updated. As you can see, we can set up variable observers and specify what exactly we want to do every time the value changes. We could use this to update the UI or maybe to start playing different, mood-specific music. Whatever you want to do with it, it can be very useful. Bear in mind, however, that the observer won’t be called in the beginning, so we need to use the variable state property if we want to know the variable’s initial value. Let’s update InkManager to handle that. First, let’s add some properties with private setters, which will log every update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public int RelationshipStrength
{
  get =&amp;gt; _relationshipStrength;
  private set
  { 
    Debug.Log($"Updating RelationshipStrength value. Old value: {_relationshipStrength}, new value: {value}");
    _relationshipStrength = value;
  }
}

private int _mentalHealth;
public int MentalHealth
{
  get =&amp;gt; _mentalHealth;
  private set
  {
    Debug.Log($"Updating MentalHealth value. Old value: {_mentalHealth}, new value: {value}");
    _mentalHealth = value;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s add a new function, which will handle variable updates. It’ll be called right after &lt;code&gt;StartStory()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private void InitializeVariables()
{
  RelationshipStrength = (int)_story.variablesState\["relationship_strength"];
  MentalHealth = (int)_story.variablesState\["mental_health"];

  _story.ObserveVariable("relationship_strength", (arg, value) =&amp;gt; 
  {
    RelationshipStrength = (int)value;
  });

  _story.ObserveVariable("mental_health", (arg, value) =&amp;gt;
  {
    MentalHealth = (int)value;
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we should always have up-to-date values available within our C# code!&lt;/p&gt;

&lt;h2&gt;
  
  
  Game State
&lt;/h2&gt;

&lt;p&gt;We’ve covered most of the basic visual novel features, so the next thing we’re going to look at is how to save and load our game.&lt;/p&gt;

&lt;p&gt;To do that, we’re going to introduce a new manager script, which will be responsible for all the game state related logic. Let’s create its skeleton to know what functionality we’ll need to cover next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class GameStateManager : MonoBehaviour
{
  private InkManager _inkManager;
  private CharacterManager _characterManager;

  private void Start()
  {
    _inkManager = FindObjectOfType&amp;lt;InkManager&amp;gt;();
    _characterManager = FindObjectOfType&amp;lt;CharacterManager&amp;gt;();
  }

  public void StartGame()
  {
  }

  public void SaveGame()
  {
    // Here we will collect all the data from other managers and save it to a file
  }

  public void LoadGame()
  {
    // Here we will load data from a file and make it available to other managers
  }

  public void ExitGame()
  {
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will also need a &lt;code&gt;SaveData&lt;/code&gt; class which will hold all the information we want to save about the game. Make sure to mark it with a &lt;code&gt;[Serializable]&lt;/code&gt; attribute, as we will need to serialize the information it contains. For now, we’re only focusing on the ink story state, but we’ll add more data to it later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Serializable]
public class SaveData
{
  public string InkStoryState;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Saving game
&lt;/h3&gt;

&lt;p&gt;Let’s focus on saving the game first. Ink provides a very straightforward way to save and load its state:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;var storyState = _story.state.ToJson(); // export game state for saving&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_story.state.LoadJson(storyState); // load state&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We’re going to use this functionality to pass the current story state into the GameStateManager. Add the following method to your InkManager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public string GetStoryState()
{
  return _story.state.ToJson();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s move back to GameStateManager and start implementing the logic to save the game. We will need these two functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void SaveGame()
{
  SaveData save = CreateSaveGameObject();
  var bf = new BinaryFormatter();

  var savePath = Application.persistentDataPath + "/savedata.save";

  FileStream file = File.Create(savePath); // creates a file at the specified location

  bf.Serialize(file, save); // writes the content of SaveData object into the file

  file.Close();

  Debug.Log("Game saved");

}

private SaveData CreateSaveGameObject()
{
  return new SaveData
  {
    InkStoryState = _inkManager.GetStoryState(),
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the &lt;code&gt;SaveGame()&lt;/code&gt; function uses another helper function to create a &lt;code&gt;SaveData&lt;/code&gt; object and then serializes it into a new save file at the location we specified. &lt;code&gt;Application.persistentDataPath&lt;/code&gt; is a path to a folder that can be used to store data between runs. Its exact location differs between platforms and the details are described &lt;a href="https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s test this code. We’ll need to add a new button on the screen and link it to this function. Its script should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class SaveGameButtonScript : MonoBehaviour
{
  GameStateManager _gameStateManager;

  void Start()
  {
    _gameStateManager = FindObjectOfType&amp;lt;GameStateManager&amp;gt;();

    if (_gameStateManager == null)
    {
      Debug.LogError("Game State Manager was not found!");
    }
  }

  public void OnClick()
  {
    _gameStateManager?.SaveGame();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjd3sgiob1j53wa14txv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjd3sgiob1j53wa14txv2.png" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll also need to create a new object to hold the GameStateManager script within the scene. Your hierarchy should be similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe89auyzwpf76a1s8xi8w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe89auyzwpf76a1s8xi8w.png" width="675" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s make sure our code works. Press play and try to save the game. You should see a new line (“Game saved”) within your console. If you want, you can also check if the save file is visible in the persistent data folder (&lt;a href="https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html" rel="noopener noreferrer"&gt;check its path here&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading game
&lt;/h2&gt;

&lt;p&gt;Time to implement the &lt;code&gt;LoadGame()&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void LoadGame()
{
  var savePath = Application.persistentDataPath + "/savedata.save";

  if (File.Exists(savePath))
  {
    BinaryFormatter bf = new BinaryFormatter();

    FileStream file = File.Open(savePath, FileMode.Open);
    file.Position = 0;

    SaveData save = (SaveData)bf.Deserialize(file);

    file.Close();

    InkManager.LoadState(save.InkStoryState);

    StartGame();
  }
  else
  {
    Debug.Log("No game saved!");
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Load()&lt;/code&gt; function uses the same FileStream and BinaryFormatter classes to handle the save file. We first check if the file exists and, if it does, we deserialize its contents into a &lt;code&gt;SaveData&lt;/code&gt;object. Then we pass the ink state into the InkManager and start the game. Notice that we will be using a static function on the InkManager. That’s because loading will happen in a different scene (Main menu) and we want to make sure the state is preserved between scenes. Let’s make some changes to InkManager and implement that function then.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private static string _loadedState;

public static void LoadState(string state)
{
  _loadedState = state;
}

private void StartStory()
{
  _story = new Story(_inkJsonAsset.text);

  if (!string.IsNullOrEmpty(_loadedState))
  {
    _story?.state?.LoadJson(_loadedState);

    _loadedState = null;
  }

  // external function bindings etc.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the &lt;code&gt;LoadState()&lt;/code&gt; function will update the private static variable with the loaded state. We will then check it in the &lt;code&gt;StartStory()&lt;/code&gt; function. If a loaded state exists, we use it to initialize the story and clear the variable to avoid potential confusion in the future. If not, we continue as usual, with a new story.&lt;/p&gt;

&lt;p&gt;Let’s put everything together by adding the main menu. Go ahead and create a new scene with buttons to start a new game, load an existing one, or exit entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5f6bph0szr3jx455g9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5f6bph0szr3jx455g9n.png" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll need a script for each of those buttons. They will all be very similar, as we’re going to call GameStateManager functions from each of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class LoadGameButtonScript : MonoBehaviour
{
  GameStateManager _gameStateManager;

  void Start()
  {
    _gameStateManager = FindObjectOfType&amp;lt;GameStateManager&amp;gt;();

    if (_gameStateManager == null)
    {
      Debug.LogError("Game State Manager was not found!");
    }
  }

  public void OnClick()
  {
    _gameStateManager?.LoadGame();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case of the script for the new game button, we will be calling &lt;code&gt;_gameStateManager?.StartGame()&lt;/code&gt;and for the ‘Exit game’ button, it will be &lt;code&gt;_gameStateManager?.ExitGame()&lt;/code&gt;. Make sure to add these scripts to the buttons within the editor and to add a new object with the manager itself.&lt;/p&gt;

&lt;p&gt;Now, let’s implement those remaining GameStateManager functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void StartGame()
{
  UnityEngine.SceneManagement.SceneManager.LoadScene("MainScene");
}

public void ExitGame()
{
  Application.Quit();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go ahead and enter play mode. You should be able to save and load your game now!&lt;/p&gt;

&lt;p&gt;You might notice a little problem though...&lt;/p&gt;

&lt;h2&gt;
  
  
  Character state
&lt;/h2&gt;

&lt;p&gt;When you load the game, our characters will not be showing anymore! We’re only handling the ink state, but we don’t do anything with the characters. Let’s change that now.&lt;/p&gt;

&lt;p&gt;First, we’re going to create a new class, which will work as a data container for all the information we’ll need when saving/loading the characters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Serializable]
public class CharacterData
{
  public CharacterPosition Position { get; set; }

  public CharacterName Name { get; set; }

  public CharacterMood Mood { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s create a function on the &lt;code&gt;Character&lt;/code&gt; script, which will return a &lt;code&gt;CharacterData&lt;/code&gt; object for each character.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public CharacterData GetCharacterData()
{
  return new CharacterData
  {
    Name = Name,
    Position = Position,
    Mood = Mood
  };
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The place where we’re going to handle character state is CharacterManager. Let’s add the necessary code. It will be similar to how we approached it with InkManager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public List&amp;lt;CharacterData&amp;gt; GetVisibleCharacters()
{
  var visibleCharacters = _characters.Where(x =&amp;gt; x.IsShowing).ToList();

  var characterDataList = new List&amp;lt;CharacterData&amp;gt;();

  foreach (var character in visibleCharacters)
  {
    characterDataList.Add(character.GetCharacterData());
  }

  return characterDataList;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetVisibleCharacters()&lt;/code&gt; will be used when saving the game. First, we select currently visible characters, and then, using the newly added &lt;code&gt;GetCharacterData()&lt;/code&gt; function, we populate a new list which will be returned to GameStateManager.&lt;/p&gt;

&lt;p&gt;Next, we need to add the code for loading state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private static List&amp;lt;CharacterData&amp;gt; _loadedCharacters;

public static void LoadState(List&amp;lt;CharacterData&amp;gt; characters)
{
  _loadedCharacters = characters;
}

private void Start()
{
  _characters = new List&amp;lt;Character&amp;gt;();

  if (_loadedCharacters != null)
  {
    RestoreState();
  }
}

private void RestoreState()
{
  foreach (var character in _loadedCharacters)
  {
    ShowCharacter(character.Name, character.Position, character.Mood);
  }

  _loadedCharacters = null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we’re using the static context again, this time to populate &lt;code&gt;_loadedCharacters&lt;/code&gt;. I’ve also added a &lt;code&gt;RestoreState()&lt;/code&gt; function, which is called from &lt;code&gt;Start()&lt;/code&gt;. If we have any characters to restore, this function will just iterate through all of them and show them on the screen using the &lt;code&gt;ShowCharacter()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;We still need to update GameStateManager and SaveData.&lt;/p&gt;

&lt;p&gt;The latter will now look like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Serializable]
public class SaveData
{
  public string InkStoryState;
  public List&amp;lt;CharacterData&amp;gt; Characters;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There won’t be many changes within GameStateManager. We just need to make sure we added character-specific lines into our &lt;code&gt;CreateSaveGameObject()&lt;/code&gt; and &lt;code&gt;LoadGame()&lt;/code&gt; functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private SaveData CreateSaveGameObject()
{
  return new SaveData
  {
    InkStoryState = _inkManager.GetStoryState(),
    Characters = _characterManager.GetVisibleCharacters()
  };
}

public void LoadGame()
{
  var savePath = Application.persistentDataPath + "/savedata.save";

  if (File.Exists(savePath))
  {
    // file loading code

    InkManager.LoadState(save.InkStoryState);
    CharacterManager.LoadState(save.Characters);

    StartGame();
  }
  else
  {
    Debug.Log("No game saved!");
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go ahead and press play. Your save system should be fully functional now!&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;That’s all for today. I hope that you have a solid foundation for your visual novel now and that things started coming into shape. As always, if you want to check my code for this tutorial, you can access it &lt;a href="https://github.com/KlaudiaBronowicka/VisualNovelTutorial" rel="noopener noreferrer"&gt;here&lt;/a&gt;. And if you need any help, don’t hesitate to get in touch either here or on &lt;a href="https://twitter.com/K_Bronowicka" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All visual assets and sprites used in this tutorial are drawn by &lt;a href="https://twitter.com/squarecr0w" rel="noopener noreferrer"&gt;Erica Koplitz&lt;/a&gt; for the game &lt;a href="https://store.steampowered.com/app/1385950/Guilt_Free" rel="noopener noreferrer"&gt;Guilt Free&lt;/a&gt;, created together by me and Erica.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>gamedev</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>Making a Visual Novel with Unity (3/5) - Characters and emotions</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Tue, 08 Dec 2020 16:54:14 +0000</pubDate>
      <link>https://forem.com/k_romek/making-a-visual-novel-with-unity3d-characters-and-emotions-47ag</link>
      <guid>https://forem.com/k_romek/making-a-visual-novel-with-unity3d-characters-and-emotions-47ag</guid>
      <description>&lt;p&gt;Welcome back to the series on how to make a visual novel with Unity3D and ink. We have already learned the &lt;a href="https://dev.to/k_bronowicka/making-a-visual-novel-with-unity-1-5-introduction-to-ink-2i5b"&gt;basics of ink&lt;/a&gt; and &lt;a href="https://dev.to/k_bronowicka/making-a-visual-novel-with-unity-2-5-integration-with-ink-256g"&gt;how to use ink API&lt;/a&gt; in order to access our story within Unity. We managed to set up a basic structure of our visual novel where we can click through the story and select choices. Today, we’re going to make things more exciting by adding in characters and emotions. If you ever get stuck or just want to see the whole project in action, you can access the files &lt;a href="https://github.com/KlaudiaBronowicka/VisualNovelTutorial" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Let’s get started!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The example project uses materials from the game &lt;a href="https://store.steampowered.com/app/1385950/Guilt_Free/" rel="noopener noreferrer"&gt;Guilt Free&lt;/a&gt;, which depicts someone struggling with an eating disorder. If you think this type of story may not be appropriate for you, please use your own files.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  External functions
&lt;/h1&gt;

&lt;p&gt;Most of what we’ll be doing today will depend on a very useful functionality of ink - external functions. It’s a way of calling C# code from within ink. Given that, in visual novels, the story is at the core of the gameplay, having the ability to directly call a function at a specified point in our ink file can be extremely useful. An example of this could be showing or hiding characters on the screen. That’s what we’ll focus on today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining functions
&lt;/h2&gt;

&lt;p&gt;In order to call external functions, first, we need to define them at the top of our ink file. We do it by using keyword &lt;code&gt;EXTERNAL&lt;/code&gt; like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXTERNAL ShowCharacter(characterName, position, mood)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXTERNAL HideCharacter(characterName)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;As you can see, we can define the function name and any parameters we want to pass into it. For now, we want to define two functions, &lt;code&gt;ShowCharacter&lt;/code&gt; will introduce new characters on the screen. We need the name of the character, the position on the screen where we want to display them, and their mood. When removing a character with the &lt;code&gt;HideCharacter()&lt;/code&gt; function, we’ll only need the name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling functions
&lt;/h2&gt;

&lt;p&gt;After defining a function, we can call it within our ink file by using curly braces &lt;code&gt;{}&lt;/code&gt; like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{ShowCharacter("Alice", "Center", “Fine”)}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Similar to ink variables, string parameters need to be wrapped in &lt;code&gt;“”&lt;/code&gt;. However, if you were to pass a number, you don’t need to do that.&lt;/p&gt;

&lt;p&gt;Go ahead and add the call to show and hide characters anywhere you want in your ink script.&lt;/p&gt;

&lt;p&gt;Now, let’s move onto the C# side of things and connect to those functions. In the InkManager class, add the following lines into the &lt;code&gt;StartStory()&lt;/code&gt; function. You want to do this after creating a story object and before actually starting the story. Otherwise, ink will throw an error, complaining about missing bindings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_story.BindExternalFunction("ShowCharacter", (string name, string position, string mood) 
    =&amp;gt; Debug.Log($"Show character called. {name}, {position}, {mood}"));

_story.BindExternalFunction("HideCharacter", (string name) 
    =&amp;gt; Debug.Log($"Hide character called. {name}"));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we use &lt;code&gt;_story.BindExternalFunction()&lt;/code&gt; to create a hook connecting to our predefined functions. It takes function name and function callback as parameters. Bear in mind to use appropriate types for the callback parameters. In our case, we need three strings for the character name, position, and mood. For now, we just want to make sure everything works fine so let’s simply call &lt;code&gt;Debug.Log&lt;/code&gt;. We’ll replace it with the actual functionality later on.&lt;/p&gt;

&lt;p&gt;Go ahead and press play, your console should be showing some logs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmonjw86r2a5h19u0jwrg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmonjw86r2a5h19u0jwrg.png" width="605" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Character management
&lt;/h1&gt;

&lt;p&gt;Managing characters is actually quite a big piece of functionality, so let’s pause to recap the requirements for it. Our game will have a few characters which we should be able to show and hide from the screen. The characters might need to re-enter the screen, so it would be good to keep references to them instead of destroying objects and recreating them. We should also be able to update their sprites, as they will have a few different emotions which can change as they speak.&lt;/p&gt;

&lt;p&gt;We will create a &lt;code&gt;CharacterManager&lt;/code&gt; class to handle all of that logic from the top level and to be a bridge between InkManager and individual character objects. It will be responsible for keeping references to characters on the screen, moving and updating them, as well as spawning new ones if needed.&lt;/p&gt;

&lt;p&gt;We will also need a Character prefab with an image, and a &lt;code&gt;Character&lt;/code&gt; script on it. The script will hold information about the character such as name, mood, etc. It will also provide functionality for showing, hiding, and updating the character it’s attached to.&lt;/p&gt;

&lt;p&gt;In order to make mood changes easier, we will create a &lt;code&gt;CharacterMoods&lt;/code&gt; script that will be used to store and manage mood sprites for each character within the Unity editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Let’s start by creating a new game object and calling it Characters. Place it at the top of the Canvas hierarchy as we want characters to display behind the textbox and all the other elements on the screen. Next, create a script called CharacterManager and add it to our new object.&lt;/p&gt;

&lt;p&gt;As mentioned before, CharacterManager will be used to spawn new characters using a Character prefab. Let’s go ahead and create it now, a new game object with an image component. Depending on your sprites, the size of your object will most likely be different from mine, but make sure to keep the position, anchors, and pivots the same as in the picture below. It will ensure the animation code works correctly later on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwvqexipkwhwln4zevwr4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwvqexipkwhwln4zevwr4.png" width="412" height="797"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will also want to create a new Character script and add it to the prefab. Let’s have a look at it now.&lt;/p&gt;

&lt;p&gt;We will use the Character class from the CharacterManager when initializing the character with a correct name, position, and mood. For those values, we could simply use strings, but I like to use enums in these situations to, among other reasons, avoid the potential typos. Place their definitions within a separate CharacterEnums.cs file to keep things clean. In my case they look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public enum CharacterName { Alice, Me };

public enum CharacterPosition { Center, Left, Right };

public enum CharacterMood { Fine, Happy, Sad, SadHappy, Upset, Blush, Crying, Serious, Surprised, Uncomfortable };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, your values here could be very different, depending on the characters within your stories and the moods you need. Now, back in the Character class, let’s define an Init function, which will be used after spawning a new character.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class Character : MonoBehaviour
{

  public CharacterPosition Position { get; private set; }

  public CharacterName Name { get; private set; }

  public CharacterMood Mood { get; private set; }

  public bool IsShowing { get; private set; }

  private CharacterMoods _moods;

  private float _offScreenX;

  private float _onScreenX;

  private readonly float _animationSpeed = 0.5f;

  public void Init(CharacterName name, CharacterPosition position, CharacterMoods mood, CharacterMoods moods)
  {
    Name = name;
    Position = position;
    Mood = mood;

    _moods = moods;

    Show();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might notice some variables I didn’t mention before. &lt;code&gt;_moods&lt;/code&gt; is a container holding all the mood sprites for the character. We will look at it in more detail later on. &lt;code&gt;_offScreenX&lt;/code&gt; will be the X value of the position the character will be at when not on the screen. Similarly, &lt;code&gt;_onScreenX&lt;/code&gt; will be used to correctly place the character when the &lt;code&gt;Show()&lt;/code&gt; function is called. &lt;code&gt;_animationSpeed&lt;/code&gt; will define the speed of animation for entering and leaving the screen. Let’s have a look at the &lt;code&gt;Show()&lt;/code&gt; function next, but before we do that, make sure to add the &lt;a href="https://assetstore.unity.com/packages/tools/animation/leantween-3595" rel="noopener noreferrer"&gt;LeanTween&lt;/a&gt; plugin into your project. We’re going to use it to handle animations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void Show()
{

  SetAnimationValues();

  // Position outside of the screen
  transform.position = new Vector3(_offScreenX, transform.position.y, transform.localPosition.z);

  // Set correct mood sprite
  UpdateSprite();

  LeanTween.moveX(gameObject, _onScreenX, _animationSpeed).setEase(LeanTweenType.linear).setOnComplete(() =&amp;gt;
  {
    _data.IsShowing = true;
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, first, we’re calling &lt;code&gt;UpdateAnimationValues()&lt;/code&gt;. It’s a helper function that will determine the correct values for &lt;code&gt;_offScreenX&lt;/code&gt; and &lt;code&gt;_onScreenX&lt;/code&gt; based on the desired position of the character (left, right, center). Next, we’re going to place the character at the starting position of our animation, &lt;code&gt;_offScreenX,&lt;/code&gt; which will be right outside of the screen. After setting the correct mood sprite in &lt;code&gt;UpdateSprite()&lt;/code&gt; function, we’re going to animate the character into the desired position with the use of LeanTween. If you’re not familiar with it, I recommend checking out their &lt;a href="http://dentedpixel.com/LeanTweenDocumentation/classes/LeanTween.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. It’s a comprehensive animation engine which is more than enough for what we need here.&lt;/p&gt;

&lt;p&gt;You might notice that we’re adding an &lt;code&gt;OnComplete&lt;/code&gt;callback to our animation, which will change the value of &lt;code&gt;IsShowing&lt;/code&gt; property within &lt;code&gt;CharacterData&lt;/code&gt;. We’ll implement the &lt;code&gt;Hide()&lt;/code&gt; function in a similar way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void Hide()
{

  LeanTween.moveX(gameObject, _offScreenX, _animationSpeed).setEase(LeanTweenType.linear).setOnComplete(() =&amp;gt;
  {
    _data.IsShowing = false;
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let’s have a look at the &lt;code&gt;SetAnimationValues()&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private void SetAnimationValues()
{
  switch (_data.Position)
  {
    case Position.Left:
      _onScreenX = Screen.width * 0.25f;
      _offScreenX = -Screen.width * 0.25f;
      break;

    case Position.Center:
      _onScreenX = Screen.width * 0.5f;
      _offScreenX = -Screen.width * 0.25f;
      break;

    case Position.Right:
      _onScreenX = Screen.width * 0.75f;
      _offScreenX = Screen.width * 1.25f;
      break;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this function, we’re setting different animation values based on the character position. The way I approached it was to mentally slice the screen into 4 even parts and position characters at the edges of those parts:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtjd4wm6gllg6xe74zjo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgtjd4wm6gllg6xe74zjo.png" width="681" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s have a look at the remaining functions within the Character class next. They’re both used to handle setting the correct character sprite based on the current mood.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void ChangeMood(Mood mood)
{
  _data.Mood = mood;
  UpdateSprite();
}

private void UpdateSprite()
{
  var sprite = _moods.GetMoodSprite(_data.Mood);
  var image = GetComponent&amp;lt;Image&amp;gt;();

  image.sprite = sprite;
  image.preserveAspect = true;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll see that the actual sprite selection happens within the CharacterMoods script, which we haven’t covered yet. Let’s have a look at it now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class CharacterMoods
{
  public CharacterName Name;
  public Sprite Fine;
  public Sprite Sad;
  public Sprite SadHappy;
  public Sprite Upset;
  public Sprite Serious;
  public Sprite Surprised;
  public Sprite Crying;
  public Sprite Uncomfortable;

  public Sprite GetMoodSprite(CharacterMood mood)
  {

    switch (mood)
    {
      case CharacterMood.Fine:
        return Fine;
      case CharacterMood.Sad:
        return Sad ?? Fine;
      case CharacterMood.SadHappy:
        return SadHappy ?? Fine;
      case CharacterMood.Upset:
        return Upset ?? Fine;
      case CharacterMood.Serious:
        return Serious ?? Fine;
      case CharacterMood.Surprised:
        return Surprised ?? Fine;
      case CharacterMood.Crying:
        return Crying ?? Fine;
      case CharacterMood.Uncomfortable:
        return Uncomfortable ?? Fine;
      default:
        Debug.Log($"Didn't find Sprite for character: {Name}, mood: {mood}");
        return Fine;
  }
}

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CharacterMoods is a fairly straightforward script, where we keep all mood sprites for one character and return them based on the enum value passed to the &lt;code&gt;GetMoodSprite()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;The last thing we need to cover is the CharacterManager script. Go ahead and open it up. First, we’ll need to add the following fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private List&amp;lt;Character&amp;gt; _characters;

[SerializeField]
private GameObject _characterPrefab;

[SerializeField]
private CharacterMoods _aliceMoods;

[SerializeField]
private CharacterMoods _playerMoods;

private void Start()
{
  _characters = new List&amp;lt;Character&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_characterPrefab&lt;/code&gt; is the prefab we created earlier which we’re going to use to spawn new characters. _charactes, on the other hand, will be a list of characters we have created and displayed on the screen already. You will also notice &lt;code&gt;AliceMoods&lt;/code&gt;and &lt;code&gt;PlayerMoods&lt;/code&gt;. These might be different in your project, depending on the characters in your story. Make sure to have one field for each character.&lt;/p&gt;

&lt;p&gt;Let’s have a look at the &lt;code&gt;ShowCharacter()&lt;/code&gt; function, which will be called from classes like InkManager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void ShowCharacter(CharacterName name, CharacterPosition position, CharacterMood mood)
{
  var character = _characters.FirstOrDefault(x =&amp;gt; x.Name == name);

  if (character == null)
  {
    var characterObject = Instantiate(_characterPrefab, gameObject.transform, false);

    character = characterObject.GetComponent&amp;lt;Character&amp;gt;();

    _characters.Add(character);
  }
  else if (character.IsShowing)
  {
    Debug.LogWarning($"Failed to show character {name}. Character already showing");
    return;
  }

  character.Init(name, position, mood, GetMoodSetForCharacter(name));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we’re checking if we’ve not already created this character and if it’s not currently showing. If needed, we’re instantiating a new character using the character prefab and adding its Character script to our &lt;code&gt;_characters&lt;/code&gt; list. Then, we’re initializing the character with the received information. You might remember that in ink, we’re using strings to pass character data. That’s why we need to create an overload for the &lt;code&gt;ShowCharacter()&lt;/code&gt; function, which will accept strings and convert them to appropriate enums like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void ShowCharacter(string name, string position, string mood)
{
  if (!Enum.TryParse(name, out CharacterName nameEnum))
  {
    Debug.LogWarning($"Failed to parse character name to enum: {name}");
    return;
  }

  if (!Enum.TryParse(position, out CharacterPosition positionEnum))
  {
    Debug.LogWarning($"Failed to parse character position to enum: {position}");
    return;
  }

  if (!Enum.TryParse(mood, out CharacterMood moodEnum))
  {
    Debug.LogWarning($"Failed to parse character mood to enum: {mood}");
    return;
  }

  ShowCharacter(nameEnum, positionEnum, moodEnum);

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll see that if a conversion didn’t succeed, we’re logging an error and returning. Doing it this way should allow us to detect and fix any potential typos and errors early on. We’ll implement the &lt;code&gt;Hide()&lt;/code&gt; function in a similar way, by providing overloads for both an enum and a string argument&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void HideCharacter(string name)
{
  if (!Enum.TryParse(name, out CharacterName nameEnum))
  {
    Debug.LogWarning($"Failed to parse character name to character enum: {name}");
    return;
  }

  HideCharacter(nameEnum);
}

public void HideCharacter(CharacterName name)
{
  var character = _characters.FirstOrDefault(x =&amp;gt; x.Name == name);
  if (character?.IsShowing != true)
  {
    Debug.LogWarning($"Character {name} is not currently shown. Can't hide it.");
    return;
  }
  else
  {
    character.Hide();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We first check if a character exists in our list and if it is showing. If not, we display a warning and exit the function.&lt;/p&gt;

&lt;p&gt;We will need to also be able to handle the change of mood, which will be very similar to how we’ve implemented the &lt;code&gt;Hide()&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void ChangeMood(string name, string mood)
{

  if (!Enum.TryParse(name, out CharacterName nameEnum))
  {
    Debug.LogWarning($"Failed to parse character name to character enum: {name}");
    return;
  }

  if (!Enum.TryParse(mood, out CharacterMood moodEnum))
  {
    Debug.LogWarning($"Failed to parse character mood to enum: {mood}");
    return;
  }

  ChangeMood(nameEnum, moodEnum);
}

public void ChangeMood(CharacterName name, CharacterMood mood)
{
  var character = _characters.FirstOrDefault(x =&amp;gt; x.Name == name);

  if (character?.IsShowing != true)
  {
    Debug.LogWarning($"Character {name} is not currently shown. Can't change the mood.");
    return;
  }
  else
  {
    character.ChangeMood(mood);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We still need to cover one more function in the CharacterManager and that is &lt;code&gt;GetMoodSetForCharacter()&lt;/code&gt; which we use when calling &lt;code&gt;Character.Init()&lt;/code&gt;. This function consists of a switch statement that returns an appropriate moodset for the requested character.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private CharacterMoods GetMoodSetForCharacter(CharacterName name)
{
  switch (name)
  {
    case CharacterName.Alice:
      return _aliceMoods;
    case CharacterName.Me:
      return _playerMoods;
    default:
      Debug.LogError($"Could not find moodset for {name}");
      return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have all the character handling code in place, let’s go back to the Unity Editor and make sure everything is connected. Your Characters object should look similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffhuikaeolwq13iha9a2c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffhuikaeolwq13iha9a2c.png" width="800" height="892"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s update our InkManager code now. First of all, we’ll need to create a reference to CharacterManager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private CharacterManager _characterManager;

void Start()
{
  _characterManager = FindObjectOfType&amp;lt;CharacterManager&amp;gt;();
  StartStory();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, let's update the external function bindings to use the appropriate methods on the CharacterManager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_story.BindExternalFunction("ShowCharacter", (string name, string position, string mood) 
  =&amp;gt; _characterManager.ShowCharacter(name, position, mood));

_story.BindExternalFunction("HideCharacter", (string name) 
  =&amp;gt; _characterManager.HideCharacter(name));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the project at this stage should let you see your characters sliding in and out of the screen. However, we still need to make them change moods. There won’t be anything new to this, just more of what we’ve covered already.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu68rlsbrcxgxal0wj0pq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu68rlsbrcxgxal0wj0pq.gif" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, define an external function in ink.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXTERNAL ChangeMood(characterName, mood)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then, place the ChangeMood calls in appropriate places within your story.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{ChangeMood("Alice", "Sad")}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next, create a binding in InkManager and have it call the CharacterManager to handle the update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_story.BindExternalFunction("ChangeMood", (string name, string mood) 
  =&amp;gt; _characterManager.ChangeMood(name, mood));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you press play, your characters should be now able to change moods!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yw449zaki20ceyh5tlo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yw449zaki20ceyh5tlo.gif" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;That’s all for today. I hope you enjoyed learning about external functions and that you can see their potential outside of character management. As always, if you want to check my code for this tutorial, you can access it &lt;a href="https://github.com/KlaudiaBronowicka/VisualNovelTutorial" rel="noopener noreferrer"&gt;here&lt;/a&gt;. And if you need any help, don’t hesitate to get in touch on &lt;a href="https://twitter.com/K_Bronowicka" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All visual assets and sprites used in this tutorial are drawn by &lt;a href="https://twitter.com/squarecr0w" rel="noopener noreferrer"&gt;Erica Koplitz&lt;/a&gt;  for the game &lt;a href="https://store.steampowered.com/app/1385950/Guilt_Free" rel="noopener noreferrer"&gt;Guilt Free&lt;/a&gt;, created together by me and Erica.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>beginners</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>Making a Visual Novel with Unity (2/5) - Integration with Ink</title>
      <dc:creator>Klaudia Romek</dc:creator>
      <pubDate>Tue, 01 Dec 2020 16:42:04 +0000</pubDate>
      <link>https://forem.com/k_romek/making-a-visual-novel-with-unity-2-5-integration-with-ink-256g</link>
      <guid>https://forem.com/k_romek/making-a-visual-novel-with-unity-2-5-integration-with-ink-256g</guid>
      <description>&lt;p&gt;Now that we know &lt;a href="https://dev.to/k_bronowicka/making-a-visual-novel-with-unity-1-5-introduction-to-ink-2i5b"&gt;the basics of ink&lt;/a&gt;, let’s start to actually work on our game. In this article, I will explain how to integrate ink with our Unity project and how to use the ink API in order to interact with our story. &lt;/p&gt;

&lt;p&gt;Before we start, make sure you have an ink file with some dialogues and choices ready. You might want to download and use an &lt;a href="https://github.com/KlaudiaBronowicka/VisualNovelTutorial/blob/master/Visual%20Novel%20Tutorial%20Part%202/Assets/story.ink" rel="noopener noreferrer"&gt;example story&lt;/a&gt; I prepared. It’s actually a simplified scene from &lt;a href="https://store.steampowered.com/app/1385950/Guilt_Free" rel="noopener noreferrer"&gt;Guilt Free&lt;/a&gt; - a game I released earlier this year. By sharing this, I want to show you a real life example of a visual novel and techniques that actually went into production code. I want to note, however, that the game talks about eating disorders, so if you're sensitive to this type of content, please use your own script.&lt;/p&gt;

&lt;p&gt;Let’s get started!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: Although this tutorial is aimed at beginners, it does require some basic knowledge of programming and Unity3D. If you’re struggling with something, leave a comment or &lt;a href="https://twitter.com/K_Bronowicka" rel="noopener noreferrer"&gt;message me on Twitter&lt;/a&gt;, I’ll be happy to help!&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Project setup
&lt;/h1&gt;

&lt;p&gt;Go ahead and create a new 2D project in Unity. Then add the ink plugin which you can find &lt;a href="https://assetstore.unity.com/packages/tools/integration/ink-unity-integration-60055" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Whilst you’re downloading the package, feel free to check out the official &lt;a href="https://github.com/inkle/ink/blob/master/Documentation/RunningYourInk.md#getting-started-with-the-runtime-api" rel="noopener noreferrer"&gt;ink API guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the ink plugin folder (Plugins/Ink) there will be an Example scene, where you can play a very simple novel. The logic for this scene will be written in BasicInkExample script. It handles starting the story, clicking through the dialogues, and selecting choices. Have a look at it, if you like, because we’re going to closely mimic what it does in our own game. We’re not going to work with it though because it's best to learn by doing!&lt;/p&gt;

&lt;h1&gt;
  
  
  UI Setup
&lt;/h1&gt;

&lt;p&gt;Before we do any UI work, let's just make sure the Canvas is set up correctly. On the Canvas, you should find Canvas Scaler component. Make sure that the UI Scale Mode is set to Scale With Screen Size. This will ensure the elements on the screen look the same on any screen size. I also like to set the Reference Resolution to 1920 x 1080, as this is the resolution I work most often with, but I'll leave that one up to you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hyc555adsittwmqwo9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hyc555adsittwmqwo9e.png" width="495" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re familiar with visual novels, you’ll know that most of the time they have a big text box at the bottom of the screen. It displays lines of the story which we can click through by pressing a button. That’s pretty much all we need for now. Go ahead, and add those components into your scene. It might look similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdppwuaruj396muzvlg7s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdppwuaruj396muzvlg7s.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Connecting Ink
&lt;/h1&gt;

&lt;p&gt;Now let’s create an empty object within the hierarchy and call it InkManager. Then add to it a new script with the same name. This will be the core of our game, working as the bridge between the story and the unity project. Write these lines at the top of your InkManager class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;TextAsset&lt;/span&gt; &lt;span class="n"&gt;_inkJsonAsset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Story&lt;/span&gt; &lt;span class="n"&gt;_story&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SerializeField&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="n"&gt;_textField&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_inkJsonAsset&lt;/code&gt; is a reference to the story file which will be used to create an instance of the Story class. This json file is compiled automatically by the ink plugin from the .ink file we wrote in Inky. Let’s go ahead and drop our story file into the Assets folder. You should immediately see two files, like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft22mf3cb3tfqfwijf09l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft22mf3cb3tfqfwijf09l.png" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Assign the .json file to the appropriate field on your InkManager script. Don’t forget to assign the text field as well!&lt;/p&gt;

&lt;p&gt;At this stage your scene hierarchy and InkManager should look similar to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rluvd56bxv2s9j327dk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rluvd56bxv2s9j327dk.png" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s do some coding now!&lt;/p&gt;

&lt;p&gt;We need a way to start our story. First, we have to create an instance of the Story class:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_story = new Story(_inkJsonAsset.text);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This class contains everything you need to interact with ink. We will be using it a lot.&lt;/p&gt;

&lt;p&gt;In your InkManager, create a &lt;code&gt;void StartStory()&lt;/code&gt; function and add the above line to it. This should be called from your &lt;code&gt;Start()&lt;/code&gt; function, as we want to start the story the moment we run the game. We will also need a way to display the lines of our story. Let’s create another function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void DisplayNextLine()
{
  if (!_story.canContinue) return;

  string text = _story.Continue(); // gets next line
  text = text?.Trim(); // removes white space from text
  _textField.text = text; // displays new text
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line you see will be very useful in many places. You will often want to check if the story can continue before actually using &lt;code&gt;_story.Continue()&lt;/code&gt;to get the next line. Moments, where the story can’t continue are, for example, when we’re awaiting input from the user (choices), when the story has finished, or when something went wrong. We will handle that better later on, but for now, it’s good enough to just exit the function.&lt;/p&gt;

&lt;p&gt;Now that we have a way to display our story, let’s call &lt;code&gt;DisplayNextLine()&lt;/code&gt; from the &lt;code&gt;StartStory()&lt;/code&gt; function. Your script should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Ink.Runtime;
using UnityEngine;
using UnityEngine.UI;

public class InkManager : MonoBehaviour
{
  [SerializeField]
  private TextAsset _inkJsonAsset;
  private Story _story;

  [SerializeField]
  private Text _textField;

  void Start()
  {
    StartStory();
  }

  private void StartStory()
  {
    _story = new Story(_inkJsonAsset.text);
    DisplayNextLine();
  }

  public void DisplayNextLine()
  {
    if (!_story.canContinue) return;

    string text = _story.Continue(); // gets next line
    text = text?.Trim(); // removes white space from text
    _textField.text = text; // displays new text
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go ahead and hit play. You should see the first line of your story show up on the screen!&lt;/p&gt;

&lt;p&gt;Now let’s add some interaction and make sure we can click through the story with the button we added earlier. We'll need to create a script which will call &lt;code&gt;InkManager.DisplayNextLine()&lt;/code&gt; function on each click.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using UnityEngine;

public class NextButtonScript : MonoBehaviour
{
  private InkManager _inkManager;

  void Start()
  {
    _inkManager = FindObjectOfType&amp;lt;InkManager&amp;gt;();

    if (_inkManager == null)
    {
      Debug.LogError("Ink Manager was not found!");
    }
  }

  public void OnClick()
  {
    _inkManager?.DisplayNextLine();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also ensures that we’ll get an error whenever InkManager is not in the scene, which can come in handy.&lt;/p&gt;

&lt;p&gt;In the editor, create an OnClick event handler and use it to call our new &lt;code&gt;OnClick&lt;/code&gt; function. You should now be able to click through the story when you run the game!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9ahc2nwteh0r8unag5m.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg9ahc2nwteh0r8unag5m.gif" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Choices
&lt;/h1&gt;

&lt;p&gt;There would be no visual novel without choices and that’s what we’re going to do next. Let’s start by creating an empty game object and calling it ‘Choice Buttons’. This will be a container for our dynamically created choices and we will need to add a Vertical Layout Group to display them correctly. Create a few buttons, add them to your container, and adjust the settings to make sure everything looks good. You’ll want your buttons to be fairly big to make sure they can fit the text. You can also select ‘Best Fit’ property on the Button Text to be on the safe side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gxrfuti0j6f1kozdvhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gxrfuti0j6f1kozdvhy.png" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now save one of the buttons as a ChoiceButton prefab and remove all of them from the button container. We don’t need them now.&lt;/p&gt;

&lt;p&gt;Let’s add the following lines into the InkManager script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SerializeField]
private VerticalLayoutGroup _choiceButtonContainer;

[SerializeField]
private Button _choiceButtonPrefab;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now assign the Choice Buttons object we just created and the ChoiceButton prefab to the appropriate fields in the editor.&lt;/p&gt;

&lt;p&gt;We need to write some code to create and display the choices now. You will see that we can access the current choices by using &lt;code&gt;_story.currentChoices&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private void DisplayChoices()
{
  // checks if choices are already being displaye
  if (_choiceButtonContainer.GetComponentsInChildren&amp;lt;Button&amp;gt;().Length &amp;gt; 0) return;

  for (int i = 0; i &amp;lt; _story.currentChoices.Count; i++) // iterates through all choices
  {

    var choice = _story.currentChoices[i];
    var button = CreateChoiceButton(choice.text); // creates a choice button

    button.onClick.AddListener(() =&amp;gt; OnClickChoiceButton(choice));
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our main choice handling function, we first check if we’re not already displaying some choices. Then we iterate through all of the available choices and create a button for each of them. Lastly, we add an OnClick event listener to run when the player presses a choice button. Let’s implement those missing functions now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Button CreateChoiceButton(string text)
{
  // creates the button from a prefab
  var choiceButton = Instantiate(_choiceButtonPrefab);
  choiceButton.transform.SetParent(_choiceButtonContainer.transform, false);

  // sets text on the button
  var buttonText = choiceButton.GetComponentInChildren&amp;lt;Text&amp;gt;();
  buttonText.text = text;

  return choiceButton;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this function we create a new button from the prefab, insert it into our container, and set the text we passed as a parameter. Then we return the button back to our &lt;code&gt;DisplayChoices()&lt;/code&gt; function, where we assign the &lt;code&gt;OnClick&lt;/code&gt; event handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;void OnClickChoiceButton(Choice choice)
{
  _story.ChooseChoiceIndex(choice.index); // tells ink which choice was selected
  RefreshChoiceView(); // removes choices from the screen
  DisplayNextLine();

}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we need to tell Ink which choice was clicked by using &lt;code&gt;ChooseChoiceIndex()&lt;/code&gt;. Then you’ll see a new function here. After clicking one of the choices, we need to remove all of them from the screen, and that’s what &lt;code&gt;RefreshChoiceView()&lt;/code&gt; will do. Afterward, we’ll continue the story with our old &lt;code&gt;DisplayNextLine()&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;void RefreshChoiceView()
{
  if (_choiceButtonContainer != null)
  {
    foreach (var button in _choiceButtonContainer.GetComponentsInChildren&amp;lt;Button&amp;gt;())
    {
      Destroy(button.gameObject);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refreshing choices is pretty straightforward, we just destroy all the objects inside the button container.&lt;/p&gt;

&lt;p&gt;Now that we can create and display choices, we need to know when to actually do it. This brings me back to the &lt;code&gt;_story.canContinue&lt;/code&gt; property which I mentioned before. If we can’t continue, it might be because we’re facing some choices. A simple way to make sure that’s the case is to simply check the&lt;code&gt;_story.currentChoices&lt;/code&gt; property. Let’s update the &lt;code&gt;DisplayNextLine()&lt;/code&gt;function now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void DisplayNextLine()
{
  if (_story.canContinue)
  {
    string text = _story.Continue(); // gets next line

    text = text?.Trim(); // removes white space from text

    _textField.text = text; // displays new text
  }
  else if (_story.currentChoices.Count &amp;gt; 0)
  {
    DisplayChoices();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go ahead and press play, you should be able to play through the whole story!&lt;/p&gt;

&lt;p&gt;You might notice that ink will treat the choice text as part of the story and so we display it in the text view. If you don’t want it to do that, you can, for example, run &lt;code&gt;_story.Continue()&lt;/code&gt; within &lt;code&gt;OnClickChoiceButton()&lt;/code&gt; to skip that line.&lt;/p&gt;

&lt;h1&gt;
  
  
  Thoughts
&lt;/h1&gt;

&lt;p&gt;We’re almost done now, but I want to introduce one more small concept before we call it a day. In ink, you can add hashtags to your story to mark lines of text with some additional properties. It works similar to comments in that they won’t be read as part of the story. The difference is, that we can actually access hashtags for each line from the code. They can be used in any way you want. An example I used in my ink file is to differentiate dialogue lines from main character’s thoughts, like so:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Me: Hi!&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;John: Hey...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Me: He looks upset. I’ll ask him what’s wrong. #thought&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Me: What’s wrong? Did something happen?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you’re using my ink file, you’ll notice some thoughts are already there. If you’re not, go ahead and add a few lines like that into your file and hit play. You shouldn’t see any difference, but they are there.&lt;/p&gt;

&lt;p&gt;Now, let’s use a different color and font style for thoughts, to make sure it’s distinct from the normal text. First, we’re going to add new properties to the InkManager class to make it easy to pick text colors within the editor. Don’t forget to actually set those different values to whatever you want!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[SerializeField]
private Color _normalTextColor;

[SerializeField]
private Color _thoughtTextColor;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can access hashtags by using &lt;code&gt;_story.currentTags&lt;/code&gt;. It will return a list of strings, so you can use multiple hashtags per line. Let’s add a new function to style the text based on whether the ‘thought’ tag is there or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private void ApplyStyling()
{
  if (_story.currentTags.Contains("thought"))
  {
    _textField.color = _thoughtTextColor;
    _textField.fontStyle = FontStyle.Italic;
  }
  else
  {
    _textField.color = _normalTextColor;
    _textField.fontStyle = FontStyle.Normal;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All that’s left is to call our new function before we display each line in &lt;code&gt;DisplayNextLine()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void DisplayNextLine()
{
  if (_story.canContinue)
  {
    string text = _story.Continue(); // gets next line

    text = text?.Trim(); // removes white space from text

    ApplyStyling();

    _textField.text = text; // displays new text
  }
  else if (_story.currentChoices.Count &amp;gt; 0)
  {
    DisplayChoices();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you press play now, you should see different styling for your thoughts!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsujbuddlem6ekb1fhb7y.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsujbuddlem6ekb1fhb7y.gif" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Architecture afterthoughts
&lt;/h1&gt;

&lt;p&gt;For the sake of simplicity, I keep most of the tutorial code in one file - InkManager.cs. However, I want to point out that in your game you might want to separate the code into different classes. You could, for example, have one or two classes just to handle displaying text (TextManager) and have InkManager communicate with them through events. Or perhaps you could separate logic code from the UI code by introducing additional classes to handle those things independently. Whatever you do, just make sure you don’t end up with one blown up class with all sorts of responsibilities, it’s never a good idea.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;That’s all for today, I hope you enjoyed it and that you now have a working version of your very own visual novel! If something didn’t work or if you’d just like to see how my project looked at the end of this tutorial, feel free to &lt;a href="https://github.com/KlaudiaBronowicka/VisualNovelTutorial/tree/master/Visual%20Novel%20Tutorial%20Part%202" rel="noopener noreferrer"&gt;download it here&lt;/a&gt;. I also added some visuals and fonts in there to give you an idea of what you could change in your game. Next time we’re going to focus on the characters and their emotions, which should make our game much more interesting and engaging!&lt;/p&gt;

&lt;p&gt;Happy Coding!&lt;/p&gt;

&lt;h3&gt;
  
  
  More in this series
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/k_bronowicka/making-a-visual-novel-with-unity-1-5-introduction-to-ink-2i5b"&gt;Making a Visual Novel with Unity 1/5 - Introduction to ink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Making a Visual Novel with Unity 2/5 - Integration with ink&lt;/li&gt;
&lt;li&gt;Making a Visual Novel with Unity 3/5 - Characters and emotions &lt;em&gt;(Coming out 8th Dec)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Making a Visual Novel with Unity 4/5 - State handling &lt;em&gt;(Coming out 15th Dec)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Making a Visual Novel with Unity 5/5 - Localisation &lt;em&gt;(Coming out 22nd Dec)&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
